From 2c43aba4bfd3f0fead808c03256f73259c103050 Mon Sep 17 00:00:00 2001 From: gechangfu Date: Wed, 20 Aug 2025 19:29:16 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- RUNE_STATE_WINDOW_README.md | 147 +++++++ excel/Rune.xlsx | Bin 10668 -> 10667 bytes excel/ability.xlsx | Bin 17460 -> 17557 bytes src/ReplicatedStorage/Json/Ability.json | 3 +- src/ReplicatedStorage/Json/Attributes.json | 2 +- src/ReplicatedStorage/Json/Rune.json | 2 +- src/Server/ServerMain/init.server.luau | 8 + src/ServerStorage/Base/Rune.luau | 69 +++- .../Modules/Behaviours/IceCoffine.luau | 8 +- .../Modules/Runes/RuneFireDamage.luau | 6 +- .../Modules/Runes/RuneIceCoffin.luau | 6 +- .../Proxy/PlayerFightProxy/PlayerAI.luau | 2 +- .../Proxy/PlayerFightProxy/init.luau | 2 - .../Proxy/RuneProxy/RuneCalculation.luau | 52 ++- src/ServerStorage/Proxy/RuneProxy/init.luau | 12 +- .../ClientMain/PerformanceClient/init.luau | 1 + .../ClientMain/TestRuneStateWindow.luau | 70 ++++ src/StarterPlayerScripts/UI/UIManager.luau | 6 + .../UI/Windows/RuneStateWindow/RuneShow.luau | 151 ++++++++ .../UI/Windows/RuneStateWindow/init.luau | 361 ++++++++++++++++++ 20 files changed, 867 insertions(+), 41 deletions(-) create mode 100644 RUNE_STATE_WINDOW_README.md create mode 100644 src/StarterPlayerScripts/ClientMain/TestRuneStateWindow.luau create mode 100644 src/StarterPlayerScripts/UI/Windows/RuneStateWindow/RuneShow.luau create mode 100644 src/StarterPlayerScripts/UI/Windows/RuneStateWindow/init.luau diff --git a/RUNE_STATE_WINDOW_README.md b/RUNE_STATE_WINDOW_README.md new file mode 100644 index 0000000..a7bbc01 --- /dev/null +++ b/RUNE_STATE_WINDOW_README.md @@ -0,0 +1,147 @@ +# RuneStateWindow 符文状态窗口 + +## 📋 功能概述 + +RuneStateWindow是一个用于显示当前穿戴符文状态的UI窗口,主要功能包括: + +1. **符文栏显示** - 根据当前穿戴装备显示符文列表 +2. **小丑牌动画** - 接收服务端符文执行记录后播放类似小丑牌的动画效果 + +## 🏗️ 文件结构 + +``` +src/StarterPlayerScripts/UI/Windows/RuneStateWindow/ +├── init.luau # 主窗口文件 +└── RuneShow.luau # 符文显示组件 + +src/StarterPlayerScripts/ClientMain/ +├── RuneExecutionClient.luau # 客户端接收符文执行记录 +└── TestRuneStateWindow.luau # 测试脚本 +``` + +## 🎮 使用方法 + +### 1. 打开符文状态窗口 + +```lua +-- 通过UIManager打开窗口 +UIManager:OpenWindow("RuneStateWindow", { + runeData = { + { + runeName = "RuneFireDamage", + runeUniqueId = 1, + level = 5, + icon = "fire_icon" + }, + { + runeName = "RuneIceCoffin", + runeUniqueId = 2, + level = 3, + icon = "ice_icon" + } + } +}) +``` + +### 2. 接收符文执行记录 + +窗口会自动接收服务端发送的符文执行记录,并播放动画: + +```lua +-- 服务端发送的数据格式 +local executionRecords = { + { + runeName = "RuneFireDamage", + runeUniqueId = 1, + index = 1, + timestamp = tick(), + action = "Execute", + attributesBefore = {attack = 100, hp = 1000}, + attributesAfter = {attack = 150, hp = 1000}, + behaviorListBefore = {"Attack"}, + behaviorListAfter = {"Attack", "FireDamage"}, + nextIndex = 2 + } +} +``` + +### 3. 测试功能 + +使用测试脚本可以快速验证功能: + +- **按 R 键** - 打开符文状态窗口 +- **按 T 键** - 模拟接收符文执行记录 +- **按 C 键** - 关闭符文状态窗口 + +## 🎨 动画效果 + +### 小丑牌动画特点 + +1. **金色光效** - 符文激活时显示金色发光效果 +2. **缩放动画** - 符文图标会放大并旋转 +3. **粒子效果** - 中心产生黄色粒子扩散效果 +4. **顺序播放** - 多个符文按执行顺序依次播放动画 + +### 动画参数 + +- **动画时长**: 0.3秒 +- **缩放比例**: 1.5倍 +- **旋转角度**: 360度 +- **颜色**: 金色(#FFD700)和黄色(#FFFF00) + +## 🔧 技术实现 + +### 核心组件 + +1. **RuneStateWindow** - 主窗口管理 + - 管理符文执行记录队列 + - 控制动画播放顺序 + - 处理窗口生命周期 + +2. **RuneShow** - 符文显示组件 + - 显示符文图标、名称、等级 + - 处理符文激活动画 + - 管理组件生命周期 + +3. **RuneExecutionClient** - 客户端接收器 + - 监听服务端符文执行记录 + - 转发数据到窗口组件 + +### 数据流程 + +``` +服务端符文执行 → RemoteEvent发送 → 客户端接收 → 窗口处理 → 动画播放 +``` + +## 🎯 扩展功能 + +### 可扩展的动画效果 + +1. **音效支持** - 添加符文激活音效 +2. **特效增强** - 添加更多视觉特效 +3. **交互功能** - 点击符文查看详情 +4. **状态显示** - 显示符文冷却时间 + +### 自定义动画 + +可以通过修改`CreateJokerCardEffect`方法来自定义动画效果: + +```lua +function RuneStateWindow:CreateJokerCardEffect(runeItem, record) + -- 自定义动画逻辑 + -- 可以添加更多特效、音效等 +end +``` + +## 🐛 注意事项 + +1. **UI预制体** - 需要创建对应的UI预制体`ui_w_rune_state` +2. **符文数据** - 需要正确配置符文JSON数据 +3. **图标资源** - 需要上传对应的符文图标资源 +4. **性能优化** - 大量符文时注意动画性能 + +## 📝 更新日志 + +- **v1.0.0** - 初始版本,支持基础符文显示和小丑牌动画 +- 支持符文执行记录接收和播放 +- 支持测试脚本快速验证功能 diff --git a/excel/Rune.xlsx b/excel/Rune.xlsx index 7198895555795868112892192dbd11651afb30cf..7bed530a354d02a77022a417b20467209d4c7516 100644 GIT binary patch delta 3008 zcmZ8jc{tSX7M}%!v5YZxWjFS0S;xLJc4IAM$xelglp!H|d~HRtOv;u}6A=xvwOJzD zSd#2Z8I>$u{qFC%_jm96*XKRY=W{;KbKd7Y=hWKQ*gqbiqwVu#Z8QYl)A18R!Yb4d zN3p|)u+Lj4Y@GPR2pnWt_-Re`R8WUQHUEWF++%fb-s;h#AACh^%0(lRAGLDF$CWS` zr5$|pPetXYyT-n~G+Aug;n_yfLgqalUeu*)q1M@>lARWIa4T=Bl_quZ+mBhfG5vPA zMEp}wT$Dm}ZlFl-N@}VKQIo(fyXfh{FoB@8*!vVwMcig z&#|gKreo+p9E~TEr`j2?CiQ{NLPluERi@Ir*;Lzu@kn`XQ}Tu41j5Lzt0u#$l|jwk zVqMmaj8V?>Cz)D1(v$RDjiuQ{w*Y@~W>K=-DaYDM)`|$(VUmYgg)~m2M*Qx8d8I#3 zMYwb7X-b%`Lt_|FGU~Zo8P6rWbP2uW#sGhKh^u?}*>I?3j*&b;U? z^Ebt*d&%+F7R5@mEIOy}?!|8U&9KrNC)o;_)>m7eJ{^&MRf0gtUP)qdgI}&F$&2Bg zrM&fB+k0>>Xuk|Pc5N+wDDqi-tGjEL$ijW+-oBobG`q3QXZRLGq;a&Jw_CKE(_;uW z-|fKO{McRbWjCd?A#g>6Pi~j*+I(F0PJGVFE#0?oI903L*VGe@YhxamDfp`mhuum; z79m3gq)g9Fn_Cd9doCuO4Xe~zE`6lwdw!4;dTBhW{#_XpsmKyCb?+S}X4M)Kv-!ZC zv)C^npXRy7^A3~WCIw2!ERq1r+eTqKiiKTU=hBzVN66v$;-0v;Et5twdCO@A8)|s* zly*tW<(wPsCy0BZYP~BM_%|(=6Gy9n*7?=K2a>j0{8rcW<=$!xd$&(eLLT*GSmv=< zxknVNwuO}(srH5;u31_~yc4jBC?>pU=};f~q0vf+eUowPNWrz&!63`n;2kNcfB*%u zp{OPdqY{M0Y$rp}4+a7iqCp^T5TGZ)ODN8r%AAx(ByCCWo)De|y1@{`8@Q*^4n%hc z%E?C=3kbMoB}u(%wKZy;zf|AVDD9<6=F+|&Mn~qJoE;6ilG!OT^8$B8+^7Rb+sxu} z?~h&oOo+GHSwvZ2eucEsa=`A_u5Y!}8He-<5dG7BTi*?#G+_g2@f3#9_|a`5!NC;? z_t=$FguOMLPaxjRQL_>;!)Ng7S0>9lxLg`=M`@WmPSKU$7UcY<%Ar%3DZYXz7QI5i zq(97o=?1sVeP1n;xTQR`9(cdsQJ2Dn5mIjU$D_A-q7-9)>zQ4u&a=JG9M6ART_?Ze zCMR2&)=I;X9ycaMS@Yt@mwZyu1P^okWK#}b@PU={WY=`$%6eZK|6$71NBZ;YNk3I0 z-XE2ZHqH&6bHbIn+^(=z+gChW>i?8V_p5(mV!94$L8`~f2lCwXM5z%%^Cnvp_{LMg z0SDk@(GTC)W6tY;_Fs@Wno4_#q$ziwkEK-Ay{PC|E%g*rXWXytKIJ|xMKFPX*tllD z^gew{3SNcnnm`Ri9rdTK!IeYig3*p1;jnIj}9xJg7<>QNdq^TXj+8!b`($Hw10U zEn1?eT}$Bbf?k>8R<~e<*W8)c^YN%RCsu#&^JjK$e;lXMamvb&=YPu8_s=0O&Mrq_ zk{{>80=|oBh+K&}fHu8ixd4X3-IxJ2gghbT@K>#2x2y)yRDm+0Fc~r8dit!;1ef+& zz;x?qTh|NK*cpYZCC_@J3)Z|kYKQM=Z^46#r0*KIo)V9QOcIA()g{pR!i zw>>RsI|spjU&>|53(GOyX>n{`9h|0s^Ms#bP&k !q80ME{TdU$PV+iy4Q|A5*cU zZO^~bDo%7J$i^YqYXdDwYL(Ie7|98|VbfOv#OG zSpkGxMWf3%)rTj&!-ho~>vdVb{Y= z4mgabc_|~x-(d<)bmxJHaF^^vW#EH_*d26WPrL9kdGpq+4=ACS{CC*d5}J(# ze6Yg8R@;|_vYS&fkdOGsKUwR}vYwipH}Z20`HDvO9cfQeVH=(`auoygzorf*EZPZ9 zKeDbMKR=2>pL8=%&B{8tlf_*1Wk|+dq&u6utqGl(UOHsi`hGAUGZSWIwXYb*CpMy2 zD-0#Ojcx~7cca35^&w^;Um0$87e1!FZl2mkv1)Gl>YdL>W3 zO{4_Q17?eGp8DDfYuh_Rw<;u4=&NstaWS7ye%rq?K`28}R{H(32+4zrmSBbOyK2i1PkCgfhuV&>Yq%Bm*j6VHK{YguZB_M8_jSF%fG3(yXx zK#4DH5+EBVDj8nsxU)L8=m<~;x~rrJisv4LPfNO`>hV)doHBYyA;9IaUiv-BSc*1g zx27j=GgJ)3aT|qoo@;MNWaBBxrK|*Q9;lp&c8YIP=kVg9`2CF1vH3M<0A=(+c|8`Q zKKrKF`a)Xup((@NULQ6f+RQ0k&c%G@c>~v0tFwW@-Q?dESm;HL2SiZ|6?D1`x;spF zXsP{x@f8=SNl(-R`uB=_A8kh?%LoDiSVmreLju9~zcU{6kK+mGN*F;7*@0vU9WWW_ zl@R3r&tE@Y3Q+%`f+K&SvGz$1y^`}1~YU=YaZxGAvx52*(*l+*?r0|}BMbR5S! H1qk#nX2W7& delta 3029 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 diff --git a/src/ReplicatedStorage/Json/Ability.json b/src/ReplicatedStorage/Json/Ability.json index 8eef569..c24fb01 100644 --- a/src/ReplicatedStorage/Json/Ability.json +++ b/src/ReplicatedStorage/Json/Ability.json @@ -1,4 +1,5 @@ [ {"id":20000,"type":1,"icon":1,"nameId":20000,"behaviourName":"Attack","upgradeCost":[30000,5,0],"upgradeValue":[10,0],"recycle":[30000,0],"isInPool":null}, -{"id":20001,"type":1,"icon":1,"nameId":20001,"behaviourName":"SwordWave","upgradeCost":[30000,5,10],"upgradeValue":[10,200],"recycle":[30000,100],"isInPool":1} +{"id":20001,"type":1,"icon":1,"nameId":20001,"behaviourName":"SwordWave","upgradeCost":[30000,5,10],"upgradeValue":[10,200],"recycle":[30000,100],"isInPool":1}, +{"id":61000,"type":1,"icon":1,"nameId":61000,"behaviourName":"IceCoffine","upgradeCost":[30000,5,0],"upgradeValue":[10,0],"recycle":[30000,0],"isInPool":null} ] \ No newline at end of file diff --git a/src/ReplicatedStorage/Json/Attributes.json b/src/ReplicatedStorage/Json/Attributes.json index 1786872..c9efaa9 100644 --- a/src/ReplicatedStorage/Json/Attributes.json +++ b/src/ReplicatedStorage/Json/Attributes.json @@ -30,5 +30,5 @@ {"id":53,"type":1,"specialType":null,"effectAttribute":"elementNumber","battleValue":[1,10],"iconId":29,"nameId":253}, {"id":54,"type":1,"specialType":null,"effectAttribute":"elementDefNumber","battleValue":[1,10],"iconId":30,"nameId":254}, {"id":55,"type":1,"specialType":null,"effectAttribute":"gemNumber","battleValue":[1,10],"iconId":31,"nameId":255}, -{"id":56,"type":1,"specialType":null,"effectAttribute":"runeNumber","battleValue":[1,10],"iconId":31,"nameId":256} +{"id":56,"type":1,"specialType":null,"effectAttribute":"runeNumber","battleValue":[1,10],"iconId":32,"nameId":256} ] \ No newline at end of file diff --git a/src/ReplicatedStorage/Json/Rune.json b/src/ReplicatedStorage/Json/Rune.json index 320bce9..1b012ae 100644 --- a/src/ReplicatedStorage/Json/Rune.json +++ b/src/ReplicatedStorage/Json/Rune.json @@ -1,5 +1,5 @@ [ {"id":60000,"quality":1,"type":1,"icon":1,"nameId":60000,"runeName":"RuneFireDamage","behaviorName":null,"recycle":[],"isInPool":1}, -{"id":61000,"quality":2,"type":1,"icon":1,"nameId":61000,"runeName":"RuneIceCoffin","behaviorName":"IceCoffin","recycle":[],"isInPool":1}, +{"id":61000,"quality":2,"type":1,"icon":1,"nameId":61000,"runeName":"RuneIceCoffin","behaviorName":"IceCoffine","recycle":[],"isInPool":1}, {"id":62000,"quality":3,"type":1,"icon":1,"nameId":62000,"runeName":null,"behaviorName":null,"recycle":[],"isInPool":1} ] \ No newline at end of file diff --git a/src/Server/ServerMain/init.server.luau b/src/Server/ServerMain/init.server.luau index 578f158..4fc9f83 100644 --- a/src/Server/ServerMain/init.server.luau +++ b/src/Server/ServerMain/init.server.luau @@ -60,6 +60,14 @@ local Temporary = CreateFolder("Temporary", workspace) local ProjectileCache = CreateFolder("ProjectileCache", Temporary) local Characters = CreateFolder("Characters", workspace) +-- 初始化ReplicatedStorage目录 +local EventsFolder = CreateFolder("Events", ReplicatedStorage) + +-- 创建符文执行记录RemoteEvent +local RE_RuneExecutionRecord = Instance.new("RemoteEvent") +RE_RuneExecutionRecord.Name = "RE_RuneExecutionRecord" +RE_RuneExecutionRecord.Parent = EventsFolder + -- Initially require all server-sided & shared modules -- 加载模块 ReplicatedStorage.Modules, ServerStorage.Modules for _, Location in {ReplicatedStorage.Modules, ServerStorage.Modules} do diff --git a/src/ServerStorage/Base/Rune.luau b/src/ServerStorage/Base/Rune.luau index 53de154..4a029ed 100644 --- a/src/ServerStorage/Base/Rune.luau +++ b/src/ServerStorage/Base/Rune.luau @@ -7,20 +7,55 @@ local ServerStorage = game:GetService("ServerStorage") --> Dependencies local TypeList = require(ServerStorage.Base.TypeList) -local Communicate = require(ServerStorage.Modules.Tools.Communicate) local Utils = require(ReplicatedStorage.Tools.Utils) - - function Rune:Init(PlayerAI: Player, Character: TypeList.Character, ScriptName: string) local self = {} setmetatable(self, Rune) self.PlayerAI = PlayerAI self.Character = Character self.ScriptName = ScriptName + self.Player = self.Character.Player self.TriggerSlot = 0 self.WearingSlot = 0 self.TriggerTime = 0 + self.executionRecords = {} + self.isRecording = false -- 记录开关 + return self +end + +-- 开始记录 +function Rune:StartRecording() + self.isRecording = true + self.executionRecords = {} +end + +-- 停止记录 +function Rune:StopRecording() + self.isRecording = false +end + +-- 记录执行信息 +function Rune:RecordExecution(index: number, attributesBefore: table?, attributesAfter: table?, behaviorListBefore: table?, behaviorListAfter: table?, nextIndex: number?) + if not self.isRecording then return end + + table.insert(self.executionRecords, { + runeName = self.ScriptName, + runeUniqueId = self.TriggerSlot, + index = index, + timestamp = tick(), + action = "Execute", + attributesBefore = attributesBefore or {}, + attributesAfter = attributesAfter or {}, + behaviorListBefore = behaviorListBefore or {}, + behaviorListAfter = behaviorListAfter or {}, + nextIndex = nextIndex + }) +end + +-- 获取执行记录 +function Rune:GetExecutionRecords() + return self.executionRecords end -- 触发最开始事件 @@ -62,14 +97,25 @@ function Rune:Check(index: number, AttributesData: table?, BehaviorNameList: tab -- index: 当前符文在列表中的位置 -- AttributesData: 属性数据,符文可以修改 -- BehaviorNameList: 行为名称列表,符文可以修改 - return false -- 默认返回false,表示不可以执行 + -- 返回值: true表示可以执行,false表示跳过 + return false end -- 执行事件 function Rune:Execute(index: number, AttributesData: table?, BehaviorNameList: table?) - -- 使用PlayerAI的专用方法,性能更好 + -- index: 当前符文在列表中的位置 + -- AttributesData: 属性数据,符文可以修改 + -- BehaviorNameList: 行为名称列表,符文可以修改 + -- 返回值: 下一个要执行的index,或者nil表示继续下一个 + + -- 记录执行前的状态 + local attributesBefore = Utils:DeepCopyTable(AttributesData or {}) + local behaviorListBefore = Utils:DeepCopyTable(BehaviorNameList or {}) + + -- 使用PlayerAI的专用方法 local beginNextIndex = self.PlayerAI:TriggerAllRunesExcept(self, "OnTriggerBeginEvent", index, AttributesData, BehaviorNameList) if type(beginNextIndex) == "number" then + self:RecordExecution(index, attributesBefore, AttributesData, behaviorListBefore, BehaviorNameList, beginNextIndex) return beginNextIndex end @@ -77,32 +123,35 @@ function Rune:Execute(index: number, AttributesData: table?, BehaviorNameList: t local nextIndex = self:OnExecute(index, AttributesData, BehaviorNameList) if type(nextIndex) == "number" then self.PlayerAI:TriggerAllRunesExcept(self, "OnTriggerEndEvent", index, AttributesData, BehaviorNameList) + self:RecordExecution(index, attributesBefore, AttributesData, behaviorListBefore, BehaviorNameList, nextIndex) return nextIndex end local endNextIndex = self.PlayerAI:TriggerAllRunesExcept(self, "OnTriggerEndEvent", index, AttributesData, BehaviorNameList) if type(endNextIndex) == "number" then + self:RecordExecution(index, attributesBefore, AttributesData, behaviorListBefore, BehaviorNameList, endNextIndex) return endNextIndex end + self.TriggerTime = self.TriggerTime + 1 - return nextIndex -- 返回下一个要执行的index + self:RecordExecution(index, attributesBefore, AttributesData, behaviorListBefore, BehaviorNameList, nextIndex) + + return nextIndex end -- 子类可以重写这个方法 function Rune:OnExecute(index: number, AttributesData: table?, BehaviorNameList: table?) - -- 默认实现,子类可以重写 -- index: 当前符文在列表中的位置 -- AttributesData: 属性数据,符文可以修改 -- BehaviorNameList: 行为名称列表,符文可以修改 -- 返回值: 下一个要执行的index,或者nil表示继续下一个 - return nil -- 默认继续下一个符文 + return nil end -- 销毁 function Rune:OnDestroy() + self.executionRecords = nil self = nil end - - return Rune \ No newline at end of file diff --git a/src/ServerStorage/Modules/Behaviours/IceCoffine.luau b/src/ServerStorage/Modules/Behaviours/IceCoffine.luau index d3911c3..5dccc76 100644 --- a/src/ServerStorage/Modules/Behaviours/IceCoffine.luau +++ b/src/ServerStorage/Modules/Behaviours/IceCoffine.luau @@ -28,7 +28,7 @@ function IceCoffine:Init(PlayerAI, Character: TypeList.Character, Player: Player self.Mobs = nil setmetatable(self, IceCoffine) self.OrgCooldown = COOLDOWN - self:StartCooldownTask() + -- self:StartCooldownTask() -- 客户端表现 self:SendPerformanceEvent("Init", self.Player, script.Name, true, {}) @@ -40,8 +40,8 @@ function IceCoffine:Check(CheckInfo: table) self:CheckClean() -- 当前血量<=20%时触发技能 - local maxHp = self.Character.Stats.MaxHp - local currentHp = self.Character.Stats.Hp + local maxHp = self.Character.Config.maxhp + local currentHp = self.Character.Config.hp local recoverHp = maxHp * 0.2 if currentHp <= recoverHp then self.CheckData = {} @@ -58,7 +58,7 @@ function IceCoffine:Execute() -- cd放前面之后发送事件才能正常记录cd self:StartCooldownTask() - local maxHp = self.Character.Stats.MaxHp + local maxHp = self.Character.Config.MaxHp local recoverHp = math.floor(maxHp * (self.PlayerAI:GetSharedData("IceCoffin_MaxRecover") / 100)) DamageProxy:Heal(self.Character, self.Character, recoverHp) task.wait(0.5) diff --git a/src/ServerStorage/Modules/Runes/RuneFireDamage.luau b/src/ServerStorage/Modules/Runes/RuneFireDamage.luau index 3e5d225..a19216f 100644 --- a/src/ServerStorage/Modules/Runes/RuneFireDamage.luau +++ b/src/ServerStorage/Modules/Runes/RuneFireDamage.luau @@ -1,17 +1,19 @@ --> Services local ReplicatedStorage = game:GetService("ReplicatedStorage") +local ServerStorage = game:GetService("ServerStorage") --> Dependencies local Utils = require(ReplicatedStorage.Tools.Utils) +local TypeList = require(ServerStorage.Base.TypeList) +local Rune = require(ServerStorage.Base.Rune) local RuneFireDamage = {} RuneFireDamage.__index = RuneFireDamage setmetatable(RuneFireDamage, {__index = Rune}) -function RuneFireDamage:Init(PlayerAI, Character: TypeList.Character, Player: Player) +function RuneFireDamage:Init(PlayerAI, Character: TypeList.Character) local self = Rune:Init(PlayerAI, Character, script.Name) - self.Player = Player setmetatable(self, RuneFireDamage) return self diff --git a/src/ServerStorage/Modules/Runes/RuneIceCoffin.luau b/src/ServerStorage/Modules/Runes/RuneIceCoffin.luau index 040e688..4a40c2e 100644 --- a/src/ServerStorage/Modules/Runes/RuneIceCoffin.luau +++ b/src/ServerStorage/Modules/Runes/RuneIceCoffin.luau @@ -1,17 +1,19 @@ --> Services local ReplicatedStorage = game:GetService("ReplicatedStorage") +local ServerStorage = game:GetService("ServerStorage") --> Dependencies local Utils = require(ReplicatedStorage.Tools.Utils) +local TypeList = require(ServerStorage.Base.TypeList) +local Rune = require(ServerStorage.Base.Rune) local RuneIceCoffin = {} RuneIceCoffin.__index = RuneIceCoffin setmetatable(RuneIceCoffin, {__index = Rune}) -function RuneIceCoffin:Init(PlayerAI, Character: TypeList.Character, Player: Player) +function RuneIceCoffin:Init(PlayerAI, Character: TypeList.Character) local self = Rune:Init(PlayerAI, Character, script.Name) - self.Player = Player setmetatable(self, RuneIceCoffin) return self diff --git a/src/ServerStorage/Proxy/PlayerFightProxy/PlayerAI.luau b/src/ServerStorage/Proxy/PlayerFightProxy/PlayerAI.luau index 685b939..5e315c0 100644 --- a/src/ServerStorage/Proxy/PlayerFightProxy/PlayerAI.luau +++ b/src/ServerStorage/Proxy/PlayerFightProxy/PlayerAI.luau @@ -95,7 +95,7 @@ end -- 动态添加行为 function PlayerAI:AddBehaviour(BehaviourName: string, WearingSlot: number?) - if not Behaviours[BehaviourName] then warn("Behaviour not found") return end + if not Behaviours[BehaviourName] then warn("Behaviour not found", BehaviourName) return end local newBehaviour = Behaviours[BehaviourName]:Init(self, self.Character, self.Player) newBehaviour.WearingSlot = WearingSlot or 4 self.BehaviourList[BehaviourName] = newBehaviour diff --git a/src/ServerStorage/Proxy/PlayerFightProxy/init.luau b/src/ServerStorage/Proxy/PlayerFightProxy/init.luau index 305a5a4..d4898ad 100644 --- a/src/ServerStorage/Proxy/PlayerFightProxy/init.luau +++ b/src/ServerStorage/Proxy/PlayerFightProxy/init.luau @@ -185,7 +185,6 @@ function PlayerFightProxy:UpdatePlayerFightData(Player: Player) end -- 更新玩家属性 - -- print(AttributesData) for AttributeName, AttributeValue in AttributesData do -- TODO:这里可能涉及到战斗时更换装备的属性处理,还需要再函数内部再根据剩余百分比数值变化 PlayerRole:ChangeAttributeValue(AttributeName, AttributeValue) @@ -205,7 +204,6 @@ function PlayerFightProxy:UpdatePlayerFightData(Player: Player) playerAI:AddBehaviour(behaviorName) end playerAI:AddBehaviour("Move") - -- playerAI:AddBehaviour("SwordWave") playerAI:AddBehaviour("Attack") diff --git a/src/ServerStorage/Proxy/RuneProxy/RuneCalculation.luau b/src/ServerStorage/Proxy/RuneProxy/RuneCalculation.luau index 8ad3573..c1d64b2 100644 --- a/src/ServerStorage/Proxy/RuneProxy/RuneCalculation.luau +++ b/src/ServerStorage/Proxy/RuneProxy/RuneCalculation.luau @@ -1,5 +1,11 @@ local RuneCalculation = {} +--> Services +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +--> Events +local RE_RuneExecutionRecord = ReplicatedStorage.Events:WaitForChild("RE_RuneExecutionRecord") + -- 处理符文机制主函数 function RuneCalculation:GetRuneAttributes(Player: Player, PlayerAI: table, AttributesData: table, BehaviorNameList: table) if not Player and not PlayerAI then warn("Player or PlayerAI not found") return end @@ -19,25 +25,52 @@ function RuneCalculation:GetRuneAttributes(Player: Player, PlayerAI: table, Attr PlayerAI:ClearSharedData() PlayerAI:ClearAllRune() for wearingSlot, runeName in wearingRuneName do PlayerAI:AddRune(runeName, wearingSlot) end + + -- 开始记录 + for runeName, rune in pairs(PlayerAI.RuneList) do + rune:StartRecording() + end -- 触发开始事件,并检查是否有符文要控制流程 local startNextIndex = PlayerAI:ForEachRune(function(runeName, triggerSlot, rune) local result = rune:OnStartEvent(runeName, triggerSlot, AttributesData, BehaviorNameList) if type(result) == "number" then - return result -- 返回要跳转的index + return result end end) local index = 1 - local maxSteps = 1000 -- 防止无限循环 + local maxSteps = 1000 - -- 如果开始事件指定了起始位置,使用它 if type(startNextIndex) == "number" then index = startNextIndex end -- 执行符文循环 - self:ExecuteRuneLoop(PlayerAI, wearingRuneName, wearingRuneUniqueId, index, maxSteps) + self:ExecuteRuneLoop(PlayerAI, wearingRuneName, wearingRuneUniqueId, index, maxSteps, AttributesData, BehaviorNameList) + + -- 收集所有符文的执行记录 + local allExecutionRecords = {} + for runeName, rune in pairs(PlayerAI.RuneList) do + local records = rune:GetExecutionRecords() + if records and #records > 0 then + for _, record in ipairs(records) do + table.insert(allExecutionRecords, record) + end + end + rune:StopRecording() + end + + -- 统一发送执行记录到前端 + if #allExecutionRecords > 0 then + local success, error = pcall(function() + RE_RuneExecutionRecord:FireClient(Player, allExecutionRecords) + end) + + if not success then + warn("RuneCalculation: 发送符文执行记录失败:", error) + end + end end -- 执行符文循环的辅助函数 @@ -49,31 +82,28 @@ function RuneCalculation:ExecuteRuneLoop(PlayerAI: table, wearingRuneName: table if result then local nextIndex = PlayerAI:TriggerRune(wearingRuneName[index], "Execute", index, AttributesData, BehaviorNameList) - -- 根据返回值决定下一个index if type(nextIndex) == "number" then - index = nextIndex -- 符文指定了下一个位置 + index = nextIndex else - index += 1 -- 默认继续下一个 + index += 1 end else index += 1 end - maxSteps -= 1 -- 防止无限循环 + maxSteps -= 1 end -- 触发结束事件,并检查是否有符文要控制流程 local endNextIndex = PlayerAI:ForEachRune(function(runeName, triggerSlot, rune) local result = rune:OnEndEvent(runeName, triggerSlot, AttributesData, BehaviorNameList) if type(result) == "number" then - return result -- 返回要跳转的index + return result end end) - -- 如果结束事件指定了要跳转的位置,重新开始循环 if type(endNextIndex) == "number" and endNextIndex <= #wearingRuneUniqueId and maxSteps > 0 then self:ExecuteRuneLoop(PlayerAI, wearingRuneName, wearingRuneUniqueId, endNextIndex, maxSteps, AttributesData, BehaviorNameList) end end - return RuneCalculation \ No newline at end of file diff --git a/src/ServerStorage/Proxy/RuneProxy/init.luau b/src/ServerStorage/Proxy/RuneProxy/init.luau index e302b27..a7b2302 100644 --- a/src/ServerStorage/Proxy/RuneProxy/init.luau +++ b/src/ServerStorage/Proxy/RuneProxy/init.luau @@ -258,16 +258,16 @@ function RuneProxy:GetPlayerWearingRuneData(Player: Player) for _, RuneData in ArchiveProxy.pData[Player.UserId][STORE_NAME] do if tonumber(RuneData.wearing) > 0 and table.find(wearingEquipments, RuneData.wearing) then table.insert(wearingRuneUniqueId, RuneData.id) - local RuneData = Utils:GetIdDataFromJson(JsonRune, RuneData.orgId) - if RuneData.runeName then - wearingRuneName[RuneData.wearingSlot] = RuneData.runeName + local JsonRuneData = Utils:GetIdDataFromJson(JsonRune, RuneData.orgId) + if JsonRuneData and JsonRuneData.runeName then + wearingRuneName[RuneData.wearingSlot] = JsonRuneData.runeName end - if RuneData.behaviorName then - table.insert(wearingRuneBehaviorName, RuneData.behaviorName) + if JsonRuneData and JsonRuneData.behaviorName then + table.insert(wearingRuneBehaviorName, JsonRuneData.behaviorName) end end end - return wearingRuneUniqueId, wearingRuneName + return wearingRuneUniqueId, wearingRuneName, wearingRuneBehaviorName end -- 获取对应装备槽位上的技能 diff --git a/src/StarterPlayerScripts/ClientMain/PerformanceClient/init.luau b/src/StarterPlayerScripts/ClientMain/PerformanceClient/init.luau index f62a98c..cf5c3a7 100644 --- a/src/StarterPlayerScripts/ClientMain/PerformanceClient/init.luau +++ b/src/StarterPlayerScripts/ClientMain/PerformanceClient/init.luau @@ -123,5 +123,6 @@ end) -- 打开默认界面 UIManager:OpenWindow("MainWindow") UIManager:OpenWindow("TipsWindow") +UIManager:OpenWindow("RuneStateWindow") return PerformanceClient \ No newline at end of file diff --git a/src/StarterPlayerScripts/ClientMain/TestRuneStateWindow.luau b/src/StarterPlayerScripts/ClientMain/TestRuneStateWindow.luau new file mode 100644 index 0000000..ca20a23 --- /dev/null +++ b/src/StarterPlayerScripts/ClientMain/TestRuneStateWindow.luau @@ -0,0 +1,70 @@ +--> Services +local Players = game:GetService("Players") +local UserInputService = game:GetService("UserInputService") + +--> Dependencies +local UIManager = require(script.Parent.Parent.UI.UIManager) + +-------------------------------------------------------------------------------- + +local LocalPlayer = Players.LocalPlayer + +-- 测试执行记录 +local testExecutionRecords = { + { + runeName = "RuneFireDamage", + runeUniqueId = 1, + index = 1, + timestamp = tick(), + action = "Execute", + attributesBefore = {attack = 100, hp = 1000, atkSpeed = 100}, + attributesAfter = {attack = 150, hp = 1000, atkSpeed = 120}, + behaviorListBefore = {"Attack"}, + behaviorListAfter = {"Attack", "FireDamage"}, + nextIndex = 2 + }, + { + runeName = "RuneIceCoffin", + runeUniqueId = 2, + index = 2, + timestamp = tick(), + action = "Execute", + attributesBefore = {attack = 150, hp = 1000, atkSpeed = 120}, + attributesAfter = {attack = 150, hp = 1200, atkSpeed = 120, fireAtk = 50}, + behaviorListBefore = {"Attack", "FireDamage"}, + behaviorListAfter = {"Attack", "FireDamage", "IceCoffin"}, + nextIndex = nil + } +} + +-- 键盘输入测试 +UserInputService.InputBegan:Connect(function(input, gameProcessed) + if gameProcessed then return end + + if input.KeyCode == Enum.KeyCode.R then + -- 打开符文状态窗口 + print("打开符文状态窗口") + UIManager:OpenWindow("RuneStateWindow") + + elseif input.KeyCode == Enum.KeyCode.T then + -- 模拟接收符文执行记录 + print("模拟符文执行记录") + local runeStateWindow = UIManager:GetWindow("RuneStateWindow") + if runeStateWindow then + runeStateWindow:OnRuneExecutionRecord(testExecutionRecords) + end + + elseif input.KeyCode == Enum.KeyCode.C then + -- 关闭符文状态窗口 + print("关闭符文状态窗口") + UIManager:CloseWindow("RuneStateWindow") + end +end) + +print("符文状态窗口测试脚本已启动") +print("按 R 键打开符文状态窗口") +print("按 T 键模拟符文执行记录") +print("按 C 键关闭符文状态窗口") + +-- 返回一个空表作为模块返回值 +return {} diff --git a/src/StarterPlayerScripts/UI/UIManager.luau b/src/StarterPlayerScripts/UI/UIManager.luau index 19365f2..f206713 100644 --- a/src/StarterPlayerScripts/UI/UIManager.luau +++ b/src/StarterPlayerScripts/UI/UIManager.luau @@ -9,6 +9,7 @@ UIManager.ExcludeFromStack = { -- 不受堆栈影响的窗口列表 "TipsWindow", -- 主窗口 "AbilityStateWindow", "LevelStageWindow", + "RuneStateWindow", -- 符文状态窗口 -- 可以继续添加其他窗口名称 } @@ -129,6 +130,11 @@ function UIManager:IsOpened(WindowName: string) return UIManager.Instances[WindowName] ~= nil end +-- 获取窗口实例 +function UIManager:GetWindow(WindowName: string) + return UIManager.Instances[WindowName] +end + function UIManager:SetData(WindowName: string, Data: table) if not UIManager.Instances[WindowName] then warn("UIManager:SetData() 窗口不存在:" .. WindowName) diff --git a/src/StarterPlayerScripts/UI/Windows/RuneStateWindow/RuneShow.luau b/src/StarterPlayerScripts/UI/Windows/RuneStateWindow/RuneShow.luau new file mode 100644 index 0000000..e870f7a --- /dev/null +++ b/src/StarterPlayerScripts/UI/Windows/RuneStateWindow/RuneShow.luau @@ -0,0 +1,151 @@ +local RuneShow = {} +RuneShow.__index = RuneShow + +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local Utils = require(ReplicatedStorage.Tools.Utils) +local Localization = require(ReplicatedStorage.Tools.Localization) +local JsonRune = require(ReplicatedStorage.Json.Rune) +local Signal = require(ReplicatedStorage.Tools.Signal) + +local showRuneSignal = Signal.new(Signal.ENUM.SHOW_RUNE) + +function RuneShow:Init(data: table) + local self = {} + self.Data = data + self.Variables = { + ["_imgIcon"] = 0, + ["_imgMask"] = 0, + ["_tmpName"] = 0, + ["_tmpLevel"] = 0, + } + self.Task = nil + self.SignalConnections = {} + + local con = showRuneSignal:Connect(function(RuneName, RuneUniqueId) + if RuneName == self.Data.runeName and RuneUniqueId == self.Data.runeUniqueId then + self:Refresh() + end + end) + table.insert(self.SignalConnections, con) + + setmetatable(self, RuneShow) + return self +end + +-- 初始化完成后的回调 +function RuneShow:OnInitFinish() +end + +function RuneShow:Refresh() + if self.Task then + task.cancel(self.Task) + self.Task = nil + end + + if self.Data.runeName ~= nil then + -- 获取符文数据 + local runeData = Utils:GetSpecialKeyDataFromJson(JsonRune, "runeName", self.Data.runeName) + + -- 设置符文图标 + if runeData and runeData.icon then + self.Variables._imgIcon.Image = Localization:GetImageData(runeData.icon) + else + -- 使用默认图标 + self.Variables._imgIcon.Image = "rbxassetid://0" + end + + -- 设置符文名称 + if runeData and runeData.nameId then + -- 通过nameId获取本地化文本 + self.Variables._tmpName.Text = Localization:GetLanguageData(runeData.nameId) or "未知符文" + else + -- 使用符文名称作为显示文本 + self.Variables._tmpName.Text = self.Data.runeName or "未知符文" + end + + -- 设置符文等级 + if self.Data.level then + self.Variables._tmpLevel.Text = "Lv." .. tostring(self.Data.level) + else + self.Variables._tmpLevel.Text = "Lv.1" + end + + -- 设置符文背景颜色 + if runeData and runeData.quality then + local bgColor = Localization:GetRuneQualityBgColor(runeData.quality) + if bgColor and self.Variables._imgIcon then + self.Variables._imgIcon.BackgroundColor3 = bgColor + end + end + end + + self.Variables._imgIcon.Visible = true + self.Variables._imgMask.Visible = false +end + +-- 播放激活动画 +function RuneShow:PlayActivateAnimation() + -- 创建激活效果 + local effect = Instance.new("Frame") + effect.Size = UDim2.new(1, 0, 1, 0) + effect.Position = UDim2.new(0, 0, 0, 0) + effect.BackgroundColor3 = Color3.fromRGB(255, 215, 0) -- 金色 + effect.BackgroundTransparency = 0.3 + effect.BorderSizePixel = 0 + effect.Parent = self.UIRoot + + -- 创建粒子效果 + local particle = Instance.new("Frame") + particle.Size = UDim2.new(0.1, 0, 0.1, 0) + particle.Position = UDim2.new(0.45, 0, 0.45, 0) + particle.BackgroundColor3 = Color3.fromRGB(255, 255, 0) -- 黄色 + particle.BackgroundTransparency = 0 + particle.BorderSizePixel = 0 + particle.Parent = effect + + -- 动画效果 + local tweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.Out) + + -- 主效果动画 + local mainTween = game:GetService("TweenService"):Create(effect, tweenInfo, { + Size = UDim2.new(1.5, 0, 1.5, 0), + BackgroundTransparency = 1 + }) + + -- 粒子动画 + local particleTween = game:GetService("TweenService"):Create(particle, tweenInfo, { + Size = UDim2.new(0.5, 0, 0.5, 0), + BackgroundTransparency = 1 + }) + + -- 播放动画 + mainTween:Play() + particleTween:Play() + + -- 动画结束后清理 + mainTween.Completed:Connect(function() + effect:Destroy() + end) +end + +function RuneShow:Destroy() + if self.Task then + task.cancel(self.Task) + self.Task = nil + end + + -- 清理信号连接 + for _, connection in pairs(self.SignalConnections) do + if connection then + connection:Disconnect() + end + end + + for k, v in pairs(self) do + self[k] = nil + end + self = nil +end + +return RuneShow diff --git a/src/StarterPlayerScripts/UI/Windows/RuneStateWindow/init.luau b/src/StarterPlayerScripts/UI/Windows/RuneStateWindow/init.luau new file mode 100644 index 0000000..5909d86 --- /dev/null +++ b/src/StarterPlayerScripts/UI/Windows/RuneStateWindow/init.luau @@ -0,0 +1,361 @@ +--> Services +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +--> Dependencies +local UIWindow = require(ReplicatedStorage.Base.UIWindow) +local UIEnums = require(ReplicatedStorage.Base.UIEnums) +local Localization = require(ReplicatedStorage.Tools.Localization) +local Utils = require(ReplicatedStorage.Tools.Utils) + +--> Json +local JsonAttributes = require(ReplicatedStorage.Json.Attributes) + +--> Components +local RuneShow = require(script.RuneShow) + +-------------------------------------------------------------------------------- + +local RuneStateWindow = {} +RuneStateWindow.__index = RuneStateWindow +setmetatable(RuneStateWindow, {__index = UIWindow}) + +function RuneStateWindow:Init(UIManager: table, Data: table?) + local self = UIWindow:Init(UIManager, Data) + setmetatable(self, RuneStateWindow) + self.Variables = { + ["__listRune"] = 0, + } + self.UIRootName = "ui_w_rune_state" + self.UIParentName = UIEnums.UIParent.UIRoot + + -- 符文执行记录队列 + self.runeExecutionQueue = {} + self.isPlayingAnimation = false + + -- 事件连接 + self.runeExecutionConnection = nil + + return self +end + +function RuneStateWindow:OnOpenWindow() + UIWindow.OnOpenWindow(self) + + self.Variables["__listRune"]:AddComponent(RuneShow) + + -- 连接符文执行记录事件 + self:ConnectRuneExecutionEvent() + + -- 初始化符文列表 + self:RefreshRuneList() +end + +function RuneStateWindow:OnCloseWindow() + UIWindow.OnCloseWindow(self) + + self.Variables["__listRune"]:Clean() + + -- 断开符文执行记录事件连接 + self:DisconnectRuneExecutionEvent() +end + +-- 刷新符文列表 +function RuneStateWindow:RefreshRuneList() + -- 这里需要从服务端获取当前穿戴的符文数据 + -- 暂时使用模拟数据 + local runeData = self:GetWearingRuneData() + self.Variables["__listRune"]:SetData(runeData) + print("刷新符文列表", runeData) +end + + + +-- 获取穿戴的符文数据 +function RuneStateWindow:GetWearingRuneData() + -- 从传入的数据中获取符文信息 + if self.Data and self.Data.runeData then + return self.Data.runeData + end + + -- 从ReplicatedStorage获取当前穿戴的符文 + local runeData = {} + local Players = game:GetService("Players") + local LocalPlayer = Players.LocalPlayer + + -- 获取玩家数据文件夹 + local PlayerDataFolder = ReplicatedStorage:WaitForChild("PlayerData") + local PlayerFolder = PlayerDataFolder:FindFirstChild(LocalPlayer.UserId) + if not PlayerFolder then return runeData end + + -- 获取符文文件夹 + local RuneFolder = PlayerFolder:FindFirstChild("Rune") + if not RuneFolder then return runeData end + + -- 遍历所有符文,找到穿戴中的 + for _, runeInstance in pairs(RuneFolder:GetChildren()) do + local wearing = runeInstance:GetAttribute("wearing") + if wearing and tonumber(wearing) > 0 then + local runeInfo = { + runeName = runeInstance:GetAttribute("runeName"), + runeUniqueId = tonumber(runeInstance.Name), + level = runeInstance:GetAttribute("level") or 1, + wearingSlot = runeInstance:GetAttribute("wearingSlot") or 1 + } + table.insert(runeData, runeInfo) + end + end + + -- 按穿戴槽位排序 + table.sort(runeData, function(a, b) + return a.wearingSlot < b.wearingSlot + end) + + return runeData +end + +-- 接收符文执行记录 +function RuneStateWindow:OnRuneExecutionRecord(executionRecords: table) + -- 自动刷新符文列表(获取最新的穿戴符文数据) + self:RefreshRuneList() + + -- 将执行记录添加到队列 + for _, record in ipairs(executionRecords) do + table.insert(self.runeExecutionQueue, record) + end + + -- 开始播放动画 + if not self.isPlayingAnimation then + self:PlayRuneAnimation() + end +end + +-- 播放符文动画 +function RuneStateWindow:PlayRuneAnimation() + if #self.runeExecutionQueue == 0 then + self.isPlayingAnimation = false + return + end + + self.isPlayingAnimation = true + local record = table.remove(self.runeExecutionQueue, 1) + + -- 播放小丑牌动画 + self:PlayJokerCardAnimation(record) +end + +-- 播放小丑牌动画 +function RuneStateWindow:PlayJokerCardAnimation(record: table) + -- 找到对应的符文UI元素 + local runeItem = self:FindRuneItem(record.runeName, record.runeUniqueId) + if not runeItem then + -- 如果找不到符文,直接播放下一个 + task.wait(0.5) -- 短暂延迟 + self:PlayRuneAnimation() + return + end + + -- 创建动画效果 + self:CreateJokerCardEffect(runeItem, record) +end + +-- 查找符文UI元素 +function RuneStateWindow:FindRuneItem(runeName: string, runeUniqueId: number) + -- 在符文列表中查找对应的符文 + local runeList = self.Variables["__listRune"] + if runeList and runeList.Instances then + for _, component in pairs(runeList.Instances) do + if component.Data and component.Data.runeName == runeName and component.Data.runeUniqueId == runeUniqueId then + return component + end + end + end + return nil +end + +-- 创建小丑牌动画效果 +function RuneStateWindow:CreateJokerCardEffect(runeItem: table, record: table) + -- 获取符文UI根节点 + local runeUI = runeItem.UIRoot + if not runeUI then + return + end + + -- 创建动画效果 + local effect = Instance.new("Frame") + effect.Size = UDim2.new(1, 0, 1, 0) + effect.Position = UDim2.new(0, 0, 0, 0) + effect.BackgroundColor3 = Color3.fromRGB(255, 215, 0) -- 金色 + effect.BackgroundTransparency = 0.5 + effect.BorderSizePixel = 0 + effect.Parent = runeUI + + -- 创建发光效果 + local glow = Instance.new("Frame") + glow.Size = UDim2.new(1.2, 0, 1.2, 0) + glow.Position = UDim2.new(-0.1, 0, -0.1, 0) + glow.BackgroundColor3 = Color3.fromRGB(255, 255, 0) -- 黄色 + glow.BackgroundTransparency = 0.8 + glow.BorderSizePixel = 0 + glow.Parent = effect + + -- 创建属性变化显示 + self:CreateAttributeChangeDisplay(runeUI, record) + + -- 动画效果 + local tweenInfo = TweenInfo.new(0.3, Enum.EasingStyle.Quad, Enum.EasingDirection.Out) + + -- 缩放动画 + local scaleTween = game:GetService("TweenService"):Create(effect, tweenInfo, { + Size = UDim2.new(1.5, 0, 1.5, 0), + BackgroundTransparency = 0.9 + }) + + -- 旋转动画 + local rotationTween = game:GetService("TweenService"):Create(effect, tweenInfo, { + Rotation = 360 + }) + + -- 播放动画 + scaleTween:Play() + rotationTween:Play() + + -- 动画结束后清理 + scaleTween.Completed:Connect(function() + effect:Destroy() + -- 播放下一个动画 + task.wait(0.2) + self:PlayRuneAnimation() + end) +end + +-- 创建属性变化显示 +function RuneStateWindow:CreateAttributeChangeDisplay(runeUI: Instance, record: table) + -- 收集所有属性变化 + local attributeChanges = {} + + -- 检查属性变化 + if record.attributesBefore and record.attributesAfter then + for attrName, afterValue in pairs(record.attributesAfter) do + local beforeValue = record.attributesBefore[attrName] or 0 + local change = afterValue - beforeValue + if change ~= 0 then + -- 通过effectAttribute查找对应的nameId + local attributeData = Utils:GetSpecialKeyDataFromJson(JsonAttributes, "effectAttribute", attrName) + local attributeName = attrName -- 默认使用原始名称 + + if attributeData and attributeData.nameId then + -- 通过nameId获取本地化文本 + attributeName = Localization:GetLanguageData(attributeData.nameId) or attrName + end + + table.insert(attributeChanges, { + name = attributeName, + change = change + }) + end + end + end + + -- 如果没有属性变化,直接返回 + if #attributeChanges == 0 then + return + end + + -- 创建统一的属性变化显示 + self:CreateAttributeChangeContainer(runeUI, attributeChanges) +end + +-- 创建属性变化容器 +function RuneStateWindow:CreateAttributeChangeContainer(runeUI: Instance, attributeChanges: table) + -- 计算容器高度 + local containerHeight = #attributeChanges * 25 + 10 + + -- 创建属性变化显示容器 + local changeContainer = Instance.new("Frame") + changeContainer.Size = UDim2.new(0, 160, 0, containerHeight) + changeContainer.Position = UDim2.new(0.5, -80, 0, -30) + changeContainer.BackgroundColor3 = Color3.fromRGB(20, 20, 20) + changeContainer.BackgroundTransparency = 0.2 + changeContainer.BorderSizePixel = 0 + changeContainer.Parent = runeUI + + -- 创建圆角 + local corner = Instance.new("UICorner") + corner.CornerRadius = UDim.new(0, 8) + corner.Parent = changeContainer + + -- 创建边框 + local stroke = Instance.new("UIStroke") + stroke.Color = Color3.fromRGB(255, 215, 0) + stroke.Thickness = 1.5 + stroke.Parent = changeContainer + + -- 创建属性变化文本 + for i, change in ipairs(attributeChanges) do + local changeText = Instance.new("TextLabel") + changeText.Size = UDim2.new(1, -10, 0, 20) + changeText.Position = UDim2.new(0, 5, 0, 5 + (i-1) * 25) + changeText.Text = string.format("%s +%d", change.name, change.change) + changeText.TextColor3 = change.change > 0 and Color3.fromRGB(0, 255, 100) or Color3.fromRGB(255, 100, 100) + changeText.TextScaled = true + changeText.Font = Enum.Font.SourceSansBold + changeText.BackgroundTransparency = 1 + changeText.Parent = changeContainer + end + + -- 创建向上移动动画 + local moveTween = game:GetService("TweenService"):Create(changeContainer, TweenInfo.new(2, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), { + Position = UDim2.new(0.5, -80, 0, -150), + BackgroundTransparency = 1 + }) + + -- 播放动画 + moveTween:Play() + + -- 动画结束后清理 + moveTween.Completed:Connect(function() + changeContainer:Destroy() + end) +end + +-- 连接符文执行记录事件 +function RuneStateWindow:ConnectRuneExecutionEvent() + -- 确保只连接一次 + if self.runeExecutionConnection then + return + end + + -- 等待RemoteEvent + local RE_RuneExecutionRecord = ReplicatedStorage.Events:WaitForChild("RE_RuneExecutionRecord") + + -- 连接事件 + self.runeExecutionConnection = RE_RuneExecutionRecord.OnClientEvent:Connect(function(executionRecords: table) + print("收到符文执行记录:", executionRecords) + + -- 发送执行记录到窗口 + self:OnRuneExecutionRecord(executionRecords) + end) +end + +-- 断开符文执行记录事件 +function RuneStateWindow:DisconnectRuneExecutionEvent() + if self.runeExecutionConnection then + self.runeExecutionConnection:Disconnect() + self.runeExecutionConnection = nil + end +end + +-- 销毁窗口 +function RuneStateWindow:Destroy() + -- 清理动画队列 + self.runeExecutionQueue = {} + self.isPlayingAnimation = false + + -- 断开事件连接 + self:DisconnectRuneExecutionEvent() + + -- 调用父类销毁方法 + UIWindow.Destroy(self) +end + +return RuneStateWindow