第九章 使用战斗日志建立冷却监视器(魔兽世界Lua插件开发...

[复制链接]

该用户从未签到

2380

主题

2433

帖子

9139

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
9139
QQ
跳转到指定楼层
楼主
发表于 2023-12-22 10:59:28 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

想要查看内容赶紧注册登陆吧!

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
第九章 使用战斗日志建立冷却监视器(Using the Combat Log to Build a Cooldown Monitor)
使用战斗日志事件(Working with Combat Log Events)
战斗日志参数(Combat Log Arguments)
创建一个事件处理器(Building an Event Handler)
使用GUID和单位(Working with GUIDs and Unit)
NPC和载具GUIDs(NPC and Vehicle GUIDs)
玩家和宠物的GUID(Player and Pet GUIDs)
GUID的例子(GUID Examples)
单位ID(Unit IDs)
单位标记和法术类型(Unit Flags and Spell Schools)
位域(Bit fields)
阅读比特位(Reading bit)
设置比特位(Setting Bits)
重置比特位(Restting Bits)
切换比特位(Toggling Bits)
位域限制(Limits of Bit Fields)
位域和布尔值(Bit Fields vs. Booleans)
法术类型(Spell Schools)
单位标志位(Unit Flags)
建立一个冷却监视器(Building a Cooldown Monitor)
检测法术(Detecting Spells)
在聊天框中使用转义序列(Using Escape Sequences in Chat Frame)
彩色文本(Colored Texts)
超链接(Hyperlink)
纹理(Texture)
语法转义序列(Grammatical Escape Sequence)
建立状态栏计时器(Building Status Bar Timer)
建立一个模板(Building a Templat)
处理定时器(Handing the Timers)
状态栏计时器的构造(The Status Bar Timer Constructor)
定位(Positioning)
更新计时器(Updating Timers)
取消计时器(Canceling Timers)
移动计时器(Moving the Timers)
修复上一个错误(Fixing the Last Bug)
总结(Summary)
第九章 使用战斗日志建立冷却监视器
(Using the Combat Log to Build a Cooldown Monitor)
我们已经使用了很多的事件,但有两个特别有趣的事件我们还没有提及:战斗日志事件(COMBAT_LOG_EVENTS)和未过滤的战斗日志事件(COMBAT_LOG_EVENT_UNFILTERED)。两种事件都与战斗日志有关,并且能接收大量的参数(arguments)(高达20个)。我们将在本章中看到这些参数的意思。所有与战斗相关的事件都会触发战斗日志事件,例如当你附近的任何人攻击一个目标或任何人被攻击。这些事件参数会告诉你到底发生了什么。
  在本章中,我们将写一个被称为冷却监视器(Cooldown Monitor)的插件,这是一个显示来自你团队成员的重要技能冷却的插件,例如,英勇、嗜血或重生。这意味着当你的团队中有人施放了这样的法术时,我们必须找到相应的战斗日志事件类型。
使 用 战 斗 日 志 事 件
(Working with Combat Log Events)

  我提到过两个战斗日志事件,分别是:战斗日志事件(COMBAT_LOG_EVENTS)和未过滤的战斗日志事件(COMBAT_LOG_EVENT_UNFILTERED),两者接收相同的参数。唯一的区别是所有战斗日志事件都会触发未过滤事件。战斗日志事件使用过滤器过滤某些事件。这个过滤器是你的战斗日志设置,并且每个战斗日志事件都被显示在你聊天框中的战斗日志里。修改战斗日志的聊天框设置会更改此过滤器。这也意味着你的大多数时间都是在使用事件的未过滤版本。在你的插件中,你不会希望让你的事件使用用户定义的过滤器。被过滤的版本只有在你想用你的插件去替代默认的战斗日志聊天框的时候才有用。

○ 战 斗 日 志 参 数(Combat Log Arguments)
  对于所有类型的战斗日志事件来说,一个战斗日志事件的前八个参数总是相同的。表9-1列出了它们。

参数

描述

时间戳(timestamp)

事件发生的确切时间的时间戳。它能被用在例如函数date(fmt,time)的第二个参数。

事件(event)

子事件(The subevent)

源GUID(sourceGUID)

生成事件的实体的GUID(globally unique identifier,全局唯一标识符)。我们将在后面介绍如何从GUID中获取信息。

源名称(sourceName)

发生该事件的实体的名称。例如,如果你施放了一个法术,则该参数就是你的名称。

源标志位(sourceFlags)

该标志位是包含了生成此事件的实体的额外信息。这是一个位域(bit field),它在第六章中被提到过,之后我们将看到如何从这个位域提取额外信息。

目标GUID(destGUID)

目标的GUID

目标名称(destName)

目标的名称

目标标志位(destFlags)

目标的标志位
 最有趣的参数是第二个。不要把这个和真实的战斗日志事件混淆了。这个事件参数被充当识别战斗日志动作的子事件。例如,如果你施放了一个法术,事件(event)将是SPELL_CAST_START;如果你完成了施法,它将是SPELL_CAST_SUCCESS.如果该法术击中了你的目标,事件是SPELL_DAMAGE。你可以在附录B中找到所有战斗日志事件的子事件。
  这个子事件也决定了所有事件处理程序接收的额外参数。如果子事件是以SPELL或RANGE开头,像前面举例的事件一样,则接下去的三个参数将会像表9-2所示那样。

参数

描述

法术ID(spellID)

法术的ID。它的ID与许多魔兽世界数据库网站所使用的ID相同。例如Wowhead。你可以使用以下方法构建一个URL,例如从以下网站搜索关于法术ID的详细信息:http://www.wowhead.com/?spell=<spellID>

法术名称(spellName)

法术的名称

法术类型(spellSchool)

该法术的类型。这是一个位域(bit field),我们将在稍后详细讨论这个。
如果这个事件以ENVIRONMENTAL开头(例如ENVIRONMENTAL_DAMAGE),则下一个参数是环境类型(environmental type),它是标识环境类型的字符串(例如岩浆“LAVA”)。
  表9-3中所示的参数取决于战斗日志事件的后缀。这些后缀不会再添加额外的参数,例如在前面例子中的SPELL_CAST_START和SPELL_CAST_SUCCESS。这两个事件接收八个标准参数和额外三个法术参数。这些参数提供了所有你需要的信息:谁对谁施放了何种法术。但这个事件SPELL_DAMAGE明显需要更多的参数,因为你还需要知道这个法术到底造成了多少的伤害,它是不是爆击(critical hit),等等。表9-3展示了所有例如SPELL_DAMAGE或SWING_DAMAGE(一个白色字体的近战攻击)以DAMAGE结尾的事件的额外参数。

参数

描述

数值(amount)

攻击造成的伤害

溢出伤害(overkill)

如果目标死于攻击,则为超出部分的伤害

类型(school)

伤害的类型。这是一个位域,就像法术的类型。我们将在后面学习如何从中获取信息。

被反击(resisted)

伤害被反击的数值

被格挡(blocked)

伤害被阻挡的数值

被吸收(absorbed)

伤害被吸收的数值

爆击(critical)

如果这次攻击是爆击的则为1,否则为0

偏斜(glancing)

如果是一次偏斜攻击则为1,否则为0

致命(crushing)

如果它是一次致命攻击则为1,否则为0
附录B包含一个列出了所有战斗日志事件参数的表,那里有更多的后缀和更多的参数。
  现在,让我们看看这些事件的实际用途。

○ 创建一个事件处理器(Building an Event Handler)
  处理子事件基本上与处理一个普通事件没什么不同,所以你只需要使用普通事件处理程序。但有时候把子事件当作真实事件来处理可能会有用。下面的代码展示了执行此操作的事件处理程序。
Code lua:
  1. MyMod = {}

  2. local function onEvent(self, event, ...)
  3.   if event == “COMBAT_LOG_EVENT_UNFILTERED” then
  4.     return onEvent(self, select(2, ...), ...)
  5.   elseif MyMod[event] then
  6.     return MyMod[event](...)
  7.   end
  8. end

  9. local frame = CreateFrame(“Frame”)
  10. frame:RegisterEvent(“COMBAT_LOG_EVENT_UNFILTERED”)
  11. frame:SetScript(“onEvent”, onEvent)
复制代码
这个事件处理程序的技巧是,如果事件是战斗日志事件,它只调用它自己。它在尾部的调用使用了子事件替代战斗日志事件。例如,我们现在可以使用下面的函数去打印输出所有SPELL_CAST_START子事件到聊天框:
Code lua:
  1. function MyMod.SPELL_CAST_START(...)
  2.   print(string.join(“,”, tostringall(...)))
  3. end
复制代码
tostringall是一个由默认UI创建的辅助函数;它的工作原理与tostring类似,但有很多参数,string.join函数常被用于连接所有参数,用逗号隔开。每当你附近的人开始施放一个法术时,它将输出子事件的所有参数到你的聊天框。所以在一座主城或副本里使用该函数时,将发送大量的事件信息到聊天框。下面这个输出例子是在我施放一个快速治疗的时候产生的:
  1. 12346219351.848,SPELL_CAST_START,0x00000000001517FB,Tandanu,1297,0x0000000000000000,nil,-2147483648,48071,Flash Heal,2
复制代码
如果你曾经读过原始的战斗日志,那么这个输出的格式可能看起来很熟悉。你可以通过输入/combatlog来创建战斗日志;这会将所有COMBAT_LOG_EVENT_UNFILTERED事件转储到\World of Warcraft\Logs\WoWCombatLog.txt文件夹中。此文件中的每一行保存一个事件,使用逗号分隔所有的参数。
  第一个参数是当前时间,第二个是子事件,比较有意思的是第三个参数,它是我的GUID。需要注意,这是一个字符串,因为Lua的数字不能表示如此大的数据。一个GUID在服务器上总是唯一的,这意味着没有其他玩家或NPC的GUID与我的相同。注意,即使是两个在同一个生成位置的同类型的怪物(mobs),也始终具有不同的GUID。这意味着你在战斗日志插件中可以使用这个ID跟踪一个NPC或玩家。例如像DamageMeters这样的伤害统计插件可以使用这个ID来区分同名的两个怪物。
  第四个参数是我的名字。第五个参数是存储有关施法者(自己)的额外信息。在下一节中,你将看到我们如何从这个位域中提取信息。接下来三个参数,被称为destGUID,destName和destFlags,它们均指向动作的目标。事件SPELL_CAST_START不提供目标信息,因此你不能预测一个法术是对谁施放。这意味着为GUID为0,名字为空(nil),标志位看起来是一堆毫无用处的数字。当法术击中目标时,这个信息将会被显示出来。
  接下来的三个参数是spellId,spellName和spellSchool。最高级的快速治疗(Flash Heal)的ID是48071,你可以通过Wowhead来查找法术(http://www.wowhead.com/?spell=48071)。这个名字明显是快速治疗,伤害类型(school)是2,这表示这个法术是神圣法术(holy spell);我们将在本章后面看到关于spellSchool的其他值。
使 用 GUID 和 单 位
(Working with GUIDs and Unit)

  游戏中的每个单位都有一个唯一的GUID来标识对象。单位(unit)这个词是指诸如怪物、玩家、NPC或载具之类的对象。服务器上的每个单独的NPC或玩家都有一个唯一的GUID。
  玩家或宠物的GUID只有在你重命名或转移到其他服务器时才会改变。NPC的GUID被用到死亡;所有重生的NPC们都有会有一个全新的不同的GUID。
  GUID总是以十六进制的字符串表示。你不能把它赋值给一个Lua数值。因为这个数字太大了。这个数值对其本身来说包含有具有价值的信息。例如,这里是一个怪物的GUID:
0xF1300070BB00004A
  前两位数值“F”和“1”的用途未知(注意这里“0”和“x”不作为数字计算在内,它只是表示这是一个十六进制数)。第三位数值“3”表示该单位的类型,3代表NPC。其他可能的值为:“4”是宠物,“5”是载具,“0”是玩家。所有其他的数值取决于该单位的类型。

○ NPC和载具GUIDs(NPC and Vehicle GUIDs)
  接下来的三位数字的用途是未知的,但它们总是0。再接下来的四位数字(在我们的例子中是70BB)包含我们可以能从GUID中提取的最有价值的信息。这部分是NPC或载具的ID。我们可以使用下面的函数来从GUID提取NPC的ID:
Code lua:
  1. local function GetNPCId(guid)
  2.   return tonumber(guid:sub(9,12),16)
  3. end

  4. print(GetNPCId(“0xF1300070BB00004A”)) -->28859
复制代码
这里我们使用tonumber(str, base)的第二个参数,这可以用来告诉tonumber给定字符串在底层系统中的位数。需要注意的是,对于前缀为0x的十六进制数字来说这不是必须的。
  在例子中,我们调用函数来提取GUID,并且得到的结果是28859。这个生物的ID就像法术ID一样,也被几乎所有的魔兽世界数据库网站所使用。我们可以通过打开URL使用Wowhead去获取关于NPC的名字和额外信息(http://www.wowhead.com/?npc=28859)。这网页告诉我们例子中的GUID取自玛里苟斯。
  我们也可以查找名字,它也是作为一个参数进行传递的,即使这样看起来有点毫无意义。这样做的真正目的在于,NPC们不会取决于本地。如果你编写了一个查找某些NPC的ID的插件,那么它将在任何客户端语言下运行。在本地化魔兽世界客户端怪物的名字肯定会有所不同。这同样适用于法术ID。
小贴士:使用ID来识别一个生物或法术,或使用sourceName,destName和spellName去获取输出它的本地名称。
  剩下的6位数字是一个“衍生计数器”(spawn-counter),为每个相同类型的新怪物增加1,去保证GUID的唯一性。当服务器重启的时候,这个计数器被重置,所以NPC(非玩家角色)和载具的GUID只有当周是自己的。

○ 玩家和宠物的GUID(Player and Pet GUIDs)
  整个玩家的GUID只是一个计数器,每当有人创建一个新角色时就会增加。这意味着你可以通过GUID来比较两个角色的创建时间。注意,服务器转移或改名也会给你一个新的GUID。
  宠物和玩家非常相似,它们也有一个计数器,每当有人驯服了一只新宠物时,这个计数器就会增加。这个计数器是以七位数的形式存储,从第四位开始,后面六位是这个特定的宠物衍生计数器,每次你重新召唤你的宠物时它就会增加。

○ GUID的例子(GUID Examples)
  一个一直使用GUID的插件是Deadly Boss Mods,它不依赖于客户端语言。下面这段代码稍微展示了一部分DBM的UNIT_DIED处理程序的简化版本。此程序的目的是检测你对抗的boss的死亡。每当一名玩家或NPC死亡,该程序就会被调用;destGUID是死亡单位的GUID。该方法检测它是否是一个NPC或载具,并且提取NPC的ID,传递给OnMobKill方法:
Code lua:
  1. function DBM:UNIT_DIED(_,_,_,_,_, destGUID)
  2.   if destGUID:sub(5, 5) == ”3” or destGUID:sub(5, 5) == “5”then
  3.     self:OnMobKill(tonumber(destGUID:sub(9, 12),16))
  4.   end
  5. end
复制代码
OnMobKill做些繁杂困难的工作。它使用一个循环来遍历所有当前战斗中的boss模块(处理boss战的DBM小模块)。这些模块(mods)被存储在局部变量inCombat中。例如一个boss模块对象包含了文件combatInfo,这是一个表,包含了所有关于如何去检测拉怪(pull)并击杀boss的所有信息。这个mob文件在表中包含了boss的NPC ID,它也被用于拉怪检测(pull detection)。下面的代码段展示了一个关于OnMobKill处理程序的稍微简化的版本。完整的方法有很多额外的代码,boss有多个怪物需要被杀死。
Code lua:
  1. function DBM:OnMobkill(cId, synced)
  2.   for i = #inCombat, 1,-1, do
  3.     local v  inCombat[i]
  4.     if cId == v.combatInfo.mob then
  5.       if not synced then
  6.         sendSync(“DBMv4-Kill”, cId)
  7.       end
  8.       v:EndCombat()
  9.     end
  10.   end
  11. end
复制代码
该功能还将此事件同步到副本组(raid group),以告诉玩家们谁超出了射程,或战斗已经结束。该同步会再次调用OnMobKill,第二个参数synced = true用来防止同步循环。
  代码调用给定NPC ID的所有boss模块的EndCombat方法。这个方法打印输出胜利消息到聊天框,结束所有计时器,并保存一些统计数据。
  GUID的另一个重要用途是非常明显的:像识别码一样使用它们。毕竟它们是全局唯一的标识符。这种用法的一个例子是玛里苟斯的Boss模块。在战斗的第三阶段,所有玩家都使用了一种载具(一条龙),并在载具身上施放类似电涌(Power Surge)的法术,而不是在玩家身上。然而,一个Boss模块仍然需要去确定玛里苟斯的法术目标。但它得到的唯一信息是这个受影响载具的名称和GUID。通过使用名字取获到龙的主人是不可能的,因为所有的龙都具有相同的名字。为此我们需要去使用GUID。
  这个点子是使用GUID作为表中的键值,并存储载具所有者的名称作为值。下面代码展示了函数buildGuidTable(),在第三阶段被调用几秒后。它使用函数GetNumRaidMembers(),返回在副本中的玩家数量。它建立了一个识别副本成员和它们的宠物的字符串。在下一节中,我们将看到更多关于单位ID的内容。UnitGUID常被用于提取单位的GUID,而UnitName被用于提取名字。
Code lua:
  1. local guids = {}

  2. local function buildGuidTable()
  3.   for i = 1, GetNumRaidMembers() do
  4.     guids[UnitGUID(“raid”..i..”pet”) or “”] = UnitName(“raid”..i)
  5.   end
  6. end
复制代码
这个函数创建了表guids,允许我们去使用龙的GUID取获取它的拥有者。下面的例子展示了一个代码段,它负责在玛里苟斯对你施放一个电涌(Power Surge)时,显示一个巨大的警告文本。specWarnSurge是一个警告对象,它的方法Show()显示了这个警告。
Code lua:
  1. local target = guids[destGUID]
  2. if target == UnitNme(“player”) then
  3.   specWarnSurge:Show()
  4. end
复制代码
这个例子从大量的单位ID中循环建立了表guids。让我们看看它们是如何工作的。

○ 单位ID(Unit IDs)
  单位ID是定义了一个例如玩家或NPC的短小的字符串。我们只是看到了一些复杂的单位ID,例如raid1target。但我们在这本书的前面提到了一个更简单的单位ID:player。player总是指你,它可以像所有的单位ID一样,传递给API函数,例如UnitName。有许多这样的函数可以获得一个单位的各种属性,比如它的当前法力值(UnitMana),生命值(UnitHealth),或GUID(UnitGUID)。附录B包含所有单位相关函数的引用。
  单位ID总是由一个前缀组成,后面可以跟着很多个后缀。表9-4列出了单位ID的可能前缀。

前缀
描述
焦点(focus)

你的焦点目标

玩家(player)


宠物(pet)

你的宠物

队伍(partyn)

第n个队伍成员。注意,你不是你自己队伍的成员,意味着你不能使用单位ID引用你自己,n介于0到5之间。

团队(raidn)

第几个团队成员。对你来说不像队伍partyn那样。

目标(target)

你的当前目标

鼠标目标(mouseover)

你当前鼠标指向单位

空(none)

未指向单位

NPC(npc)

你当前正在打交道的NPC。打交道意味着你有一个打开的窗口,例如飞行点地图或NPC任务对话框。

这个前缀后面可以跟着后缀。只有两个可用的后缀:target和pet。前者总是指着被前缀所标识的目标单位。后者为他的宠物。一个单位ID的例子是raid1targetpettarget,它指向第一个团队成员的目标的宠物的目标。第一个副本成员是组建团队的玩家,第二个玩家是第一个加入的玩家,以此类推。下面循环遍历所有副本成员并打印输出它们的名字和当前目标。它是从前面构建玛里苟斯检查表的例子中一个非常简单的循环遍历。
Code lua:
  1. for i=1, GetNumRaidMembers() do
  2.   print(UnitName(“raid”...i), UnitName(“raid”)...i...”target”))
  3. end
复制代码
注意在永远不会有一个副本单位ID上有空缺(hole),而且没有一个玩家将得到一个单位ID超过GetNumRaidMembers(),因为当有一名玩家离开团队组时,副本ID会下移。
  其他战斗日志事件的重要参数是单位标志位(the unit flags)和法术类型(school),我们已经看过了法术类型2(school2),它告诉我们那是一个神圣法术。但这个数值的意思是什么呢?
单 位 标 记 和 法 术类 型
(Unit Flags and Spell Schools)

  单位标志位和法术类型是位域,我在第六章提到过一些比特库(bit library)。现在让我们仔细看看这些位域。一个位域基本上就是个保存一系列布尔值的数字。我们不得不看一下一个数字的二进制表示去理解它是如何保存布尔值的。

○ 位域(Bit fields)
  以数字20为例子。它的二进制表示是10100。每个二进制位都可以是“1”或者“0”,而“1”对应true,“0”对应false。这意味我们在带有n位的二进制数中,最多可以存储n位布尔值。这些布尔值也被称为flags(标志位)。例子中20的值存储了5个布尔值。我们将最右边的比特位称为第0位,在它左边的位称为第1位,以此类推。这意味着我们在例子中的数字20的第0位布尔值所存储的意思为false。第1位也是false,第3位为true,第4位为false,第5位是true。
  如你所见,一个相对较小的数字能存储大量的布尔值,所以使用位域是为了节省空间。在本节最后的单位标志位(unit flags),我们将看见只需一个数字就能存储25位布尔值的位域。想象这所有的25位布尔值作为额外参数传递给战斗日志事件。这当然是可能的,但使用代码会变得非常痛苦。

  · 阅读比特位(Reading bit)
  我们现在将使用比特库(bit library)来提取这些信息。如果你想在一个普通的Lua解释器中测试下面这个例子,请使用下面代码去装载这个函数库。
Code lua:
  1. require(“bit”)
复制代码
这在魔兽世界中并不是必要的,因为这个函数库已经被装载到了里面。require()函数在游戏中甚至不是可用的。
  逐位操作(The bitwise operation)是需要去对读取一个特殊二进制比特操作AND,它可以在函数bit.band中运行。表9-5列出了该操作的真值表(truth table),它展示了所有可能的操作组合结果。你可以通过在左列中查找第一个操作数并在上一行中查找第二个操作数来读取表,结果然后在此行和列中。

AND

1

0

1

1

0

0

0

0
回想一下这些按位操作是如何工作的。它们使用二进制AND来对两个数的每个比特位进行操作,从最右边的比特位开始。根据表格中的结果中设置相应的比特。这意味着只有当两个比特位都为“1”时,它才为1;其他情况都为0。
  例如,如果我们想去检查在前面例子中的第4个比特位(请记住,从最右边的最低有效位开始以0计数)是否被置1,则我们可以使用二进制数10000(十进制的16)作为第二个操作数给二进制AND。第二个操作数也被称为掩码(bit mask),并且运算的结果将清除我们在测试的数字中所有被设置的比特位,除了已经被置1的比特位。在下面的代码中,20是我们的位域,16是我们的掩码。
Code lua:
  1. print(bit.band(20, 16))
复制代码
它打印输出了16,因为10100 band 10000等于10000。如果第4个比特位没被置1,该函数返回0,例如,在数字15(二进制位01111)。在给定位域下,你可以使用下面表达式去检查第n位比特位是否在被设置。
Code lua:
  1. local isBitSet = bit.band(bf, 2^n) ~= 0
复制代码
用要检查的第几个比特位替换n。别忘记0是最不重要的比特。如果该比特位被置1,则表达式的值位true,其他情况则为0。
  注意,表达式 2^n总是计算成一个二进制表达式100...00来表示。另外可能的方法,是使用函数bit.lshift(bitfield,n)去创建一个掩码。这是左移操作,它将位域中所有的位左移n位。新数字的右边将由0填充。下面的代码依然可以检查第n位是否被设置。
Code lua:
  1. local isBitSet = bit.band(20, bit.lshift(1, n)) ~= 0
复制代码
小贴士:熟悉十六进制数和位域的人通常直接使用创建十六进制数来创建位域。在十六进制中一个数经常用四位来表示,这使得从二进制与十六进制之间的转化变得非常容易。在位域中最重要的十六进制数是0,2,4和8。他们的二进制位表示为0000,0010,0100和1000。
也可以使用具有多个设置位的掩码。如果未设置任何比特位,则此类掩码将返回0。这意味着它可用于检查一组比特位中至少由一位是否被设置。

  · 设置比特位(Setting Bits)
  你也需要在位域中去存储信息。我们需要进行逐位OR操作。表9-6展示了二元操作OR的真值表。如果两者其中之一是1,则它返回1,如果两者都是0,则返回0。

OR

1

0

1

1

1

0

1

0

 这个被用来设置的掩码与被用来读取比特位的掩码相同。着意味着我们可以使用下列代码去设置字段中的第三个比特位设置为20(10100)。
Code lua:
  1. local field = 20 --10100
  2. local mask = 2^3 --01000

  3. print(bit.band(field, mask) ~= 0) --> false, bit 3 not set
  4. field = bit.bor(field, mask) -- set bit 3
  5. print(bit.band(field, mask) ~= 0) --> true,bit 3 is now set
  6. print(field) --> 28 (binary:11100)
复制代码
在此例子中,逐位OR操作成功的设置了第三位比特。注意,一个真实的插件你可能想在一个变量中存储掩码。所以,你可以让变量在读和设置操作中带有一个具有意义的名字。在下一章中,你将看到真正的插件是如何处理位域的。
我们现在可以在一个位域中,将比特位设为1。但我们如何设置一个比特位为0呢?

  · 重置比特位(Restting Bits)
  重置比特我们还需要逐位AND。但是这里需要的掩码在我们要重置的位的位置还需要为0,并且所有的其他比特位需要是1。因为我们不想去改变任何其他的比特位。我们需要另一种操作来从原始掩码创建新的掩码:逐位NOT。
  二进制操作NOT是个一元操作,意味着它只需要一个操作数。如果一个操作数是1则它返回0,如果是0则返回1。也就是说,它反转了比特位。在一个位域中逐位NOT,反转了比特位。所以我们可以在原掩码中使用函数bit.bnot(bitfield)。该函数被用于读取或设置比特,去获取能被重置比特的掩码。
  下面的代码显示了如何使用这些操作来重置我们代码中的第4位,示例位域位20(10100):
Code lua:
  1. local field = 20 -- 10100
  2. local mask = 2^4  10000

  3. print(bit.band(field, mask) ~= 0) --> true, bit 4 is set
  4. field = bit.band(field, bit.bnot(mask)) -- reset bit 4
  5. print(bit.band(field, mask) ~= 0) --> false, the bit is no longer set
  6. print(field) --> 4(binary 00100)
复制代码
代码按预期操作。所以我们现在可以在位域中读取比特位,设置比特位和重置比特位。另一个可以派得上的操作是切换特定的比特位。

  · 切换比特位(Toggling Bits)
  我们这里需要的操作是逐位XOR。此操作是异OR(异或),意味着如果其中一个操作数是1,它就返回1,如果两个操作数都是1或都是0,它就是返回0,表9-7显示二进制异或的真值表。

XOR

1

0

1

0

1

0

1

0

我们可以使用这个操作去切换在带有掩码的位域中的一个比特。该掩码中为0的所有字段保持不变,位1的字段被切换。下面的代码将切换所示位域的第四位,20:
Code lua:
  1. local field = 20 -- 10100
  2. local mask = 2^4 -- 10000

  3. print(bit.band(field, mask) ~= 0) --> true,bit 4 is set
  4. field = bit.bxor(field, mask) --> toggle bit 4
  5. print(bit.band(field, mask) ~= 0) --> false,the bit is no longer set
  6. field = bit.bxor(field, mask) -- toggle bit 4
  7. print(bit.band(field, mask) ~= 0) --> true, bit 4 is set again
复制代码
我们现在可以对字段执行所有必要的操作来提取信息,或者在其中存储信息。单一个位域中可以包含多少位呢?

  · 位域限制(Limits of Bit Fields)
  在带有比特库(bitlib)的Lua中,位域的大小被限制为32位。所有较大的数字将被解释为:-2147483648(二进制:1后面跟着31个0;十六进制:0x80000000)。这也是我们在前面例子中获取的destFlags的值,在那里我们的战斗日志事件还没有一个目标。

  · 位域和布尔值(Bit Fields vs. Booleans)
  你应该在只具有真正意义的情况下才使用位域。例如,如果你只有几个布尔值,在大多数情况下最好使用变量。当有很多布尔值(四个或更多)属于一起,并且经常出现在你的代码中时,应该考虑使用字段。
  但是请记住,你最重要的目标应该始终是编写可读代码。使用位域的代码通常比使用普通变量的代码更难以理解。你至少应该使用有意义的变量名为你的位掩码(bit masks)命名,而不仅仅是在你代码中使用魔法数字(无任何注释命名修饰的数字)。你将在下一节看到,当使用战斗日志时,暴雪为所有你需要的重要掩码提供了有意义的变量名。

○ 法术类型(Spell Schools)
  你在战斗日志事件中看到的位域之一是法术类型。同样的位域也被用于伤害类型,但不一定和法术属于相同类型。例如,这两个值因施法者的魔杖攻击而不同。这里的法术通常是物理法术“射击(Shoot)”,但伤害类型取决于施法者正在使用的魔杖。
  前面的例子中展示了一个快速治疗(Flash Heal)的SPELL_CAST_START事件,它的法术类型是2(二进制为:10)。所以这个字段中的这个比特位代表着神圣法术。在全局变量中有可以用于针对法术类型的掩码。表9-8中列出了所有这些掩码及它们的值。变量的名称是不言而喻的(self explanator)。

变量

位掩码

空类型(SCHOOL_MASK_NONE)

00000000

物理类型(SCHOOL_MASK_PHYSICAL)

00000001

神圣类型(SCHOOL_MASK_HOLY)

00000010

火焰类型(SCHOOL_MASK_FIRE)

00000100

自然类型(SCHOOL_MASK_NATURE)

00001000

冰霜类型(SCHOOL_MASK_FROST)

00010000

暗影类型(SCHOOL_MASK_SHADOW)

00100000

魔法类型(SCHOOL_MASK_ARCANE)

01000000
请注意法术类型并不是相互排斥的;有些法术属于不止一个类型。例如这样一个例子是“冰火球”(Frostfire Bolt),它既是冰系也是火系,因此它的法术和伤害类型是冰系和火系。当某人使用冰火球时,在战斗记录中出现的位域为:0000010100(十进制位20)。
  我们现在可以使用这些掩码和我们的逐位操作(bitwise operations)来创建一个小插件。每当你的目标开始施放一个神圣法术时候,这个插件会在你的聊天框中显示消息。
Code lua:
  1. MyMod = {}

  2. local function onEvent(self, event, ...)
  3.   if event == “COMBAT_LOG_EVENT_UNFILTERED” then
  4.     return onEvent(self, select(2, ...), ...)
  5.   elseif MyMod[event] then
  6.     return MyMod[event](...)
  7.   end
  8. end

  9. local frame = CreateFrame(“Frame”)
  10. frame:RegisterEvent(“COMBAT_LOG_EVENT_UNFILTERED”)
  11. frame:SetScript(“OnEvent”, onEvent)

  12. function MyMod.SPELL_CAST_START(_, _, srcGUID, srcName, _, _, _, _, _, spellName, spellSchool)
  13.   if srcGUID == UnitGUID(“target”) then
  14.     if bit.band(spellSchool, SCHOOL_MASK_HOLY) ~= 0 then
  15.       print(scrName..” begins to cast ”..sellName..”!”)
  16.     end
  17.   end
  18. end
复制代码
 它使用函数UnitGUID(unitID)去获取当前目标的GUID,并将其与SPELL_CAST_START事件中的scrGUID进行比较。单位ID——target,总是指向你当前的目标,这里另一个可能的单位ID是focus,它总是指向你的焦点目标。附录B列出了所有单位ID和所有单位相关的API函数。
  在法术类型的位掩码中,你可以使用bit.bor同时检查多个法术类型:
Code lua:
  1. bit.band(spellSchool, bit.bor(SCHOOL_MASK_HOLY, SCHOOL_MASK_SHADOW)) ~=0
复制代码
前面我还提到了另一个位域参数——单位标志位(the unit flags)。让我们看一下这个位域。

○ 单位标志位(Unit Flags)
  这个位域包含关于单位的阵营(affiliation)、反应(reaction)、所有权(ownership)、类型(type)和副本角色(raid role)。还有一些包含掩码的全局变量,可以从给定的字段读取所有标志位。表9-9以十六进制表示列出了这些变量和每个变量的值。需要注意,这些位域几乎使用了所有的32位,这使得二进制表示有些模糊和冗长。位掩码的含义由变量的名称来解释。
变量

掩码

COMBATLOG_OBJECT_AFFILIATION_MINE

0x00000001

COMBATLOG_OBJECT_AFFILIATION_PARTY

0x00000002

COMBATLOG_OBJECT_AFFILIATION_RAID

0x00000004

COMBATLOG_OBJECT_AFFILIATION_OUTSIDER

0x00000008

COMBATLOG_OBJECT_REACTION_FRIENDLY

0x00000010

COMBATLOG_OBJECT_REACTION_NEUTRAL

0x00000020

COMBATLOG_OBJECT_REACTION_HOSTILE

0x00000040

COMBATLOG_OBJECT_CONTROL_PLAYER

0x00000100

COMBATLOG_OBJECT_CONTROL_NPC

0x00000200

COMBATLOG_OBJECT_TYPE_PLAYER

0x00000400

COMBATLOG_OBJECT_TYPE_NPC

0x00000800

COMBATLOG_OBJECT_TYPE_PET

0x00001000

COMBATLOG_OBJECT_TYPE_GUARDIAN

0x00002000

COMBATLOG_OBJECT_TYPE_OBJECT

0x00004000

COMBATLOG_OBJECT_TARGET

0x00010000

COMBATLOG_OBJECT_FOCUS

0x00020000

COMBATLOG_OBJECT_MAINTANK

0x00040000

COMBATLOG_OBJECT_MAINASSIST

0x00080000

COMBATLOG_OBJECT_RAIDTARGET1

0x00100000

COMBATLOG_OBJECT_RAIDTARGET2

0x00200000

COMBATLOG_OBJECT_RAIDTARGET3

0x00400000

COMBATLOG_OBJECT_RAIDTARGET4

0x00800000

COMBATLOG_OBJECT_RAIDTARGET5

0x01000000

COMBATLOG_OBJECT_RAIDTARGET6

0x02000000

COMBATLOG_OBJECT_RAIDTARGET7

0x04000000

COMBATLOG_OBJECT_RAIDTARGET8

0x08000000

COMBATLOG_OBJECT_NONE

0x80000000
这些位掩码可以分为五类:阵营(affiliation)、反应(reaction)、控制(或称位所有权,the ownership)、类型(type)、副本角色(raid role)。前四个类别的标志位是相互排斥的,在一个单位标志位中,绝不会有两个相同类别的比特位。
注意:在团队中,与你在同一团队中的成员没有设置其团队的从属位。他们只有队伍的比特位被设置。此外,你自己的单位标志位将始终在此类别中被设置,而永远不会设置队伍或团队标志位。
我们现在可以使用掩码来更新函数MyMod.SPELL_CAST_START。我们通过更改这个函数来提醒我们敌对玩家施放的神圣施法。掩码COMBATLOG_OBJECT_REACTION_HOSTILE和COMBATLOG_OBJECT_TPYE_PLAYER可以被用在这里。
Code lua:
  1. function MyMod.SPELL_CAST_START(_, _, srcGUID, srcName, srcFlags, _, _, _, _, spellName, spellSchool)
  2.   if bit.band(spellSchool, SCHOOL_MASK_HOLY) ~= 0
  3.   and bit.band(srcFlags, COMBATLOG_OBJECT_REACTION_HOSTILE) ~= 0
  4.   and bit.band(srcFlags, COMBATLOG_OBJECT_TPYE_HOSTILE) ~= 0 then
  5.     print(srcName..” begins to cast “..spellName..”!”)
  6.   end
  7. end
复制代码
  1. 与副本角色相关的掩码可以用来检查目标是你的主坦克(main tank),主协助(main assist)还是被标记的目标,它们也提供了有趣的东西。编写一个只处理与标记目标或主坦克相关的事件函数是非常容易的。让我们在此改变这个函数,让他提醒你每一个被标记的单位所施放的神圣法术。这意味着我们需要检查是否设置了团队目标位。我们也可以用8位bit.band调用(每个比特调用1位)或使用掩码来设置所有的团队目标。构建一个掩码显然是更好的解决方案,实际上我们甚至不需要构建它。文件Interface\FrameXML\Constants.lua已经定义了可以使用的变量COMBATLOG_OBJECT_RAIDTARGET_MASK。
  2. Code lua:
复制代码
  1. COMBATLOG_OBJECT_RAIDTARGET_MASK = bit.bor(
  2.   COMBATLOG_OBJECT_RAIDTARGET1,
  3.   COMBATLOG_OBJECT_RAIDTARGET2,
  4.   COMBATLOG_OBJECT_RAIDTARGET3,
  5.   COMBATLOG_OBJECT_RAIDTARGET4,
  6.   COMBATLOG_OBJECT_RAIDTARGET5,
  7.   COMBATLOG_OBJECT_RAIDTARGET6,
  8.   COMBATLOG_OBJECT_RAIDTARGET7,
  9.   COMBATLOG_OBJECT_RAIDTARGET8
  10. )
复制代码
 我们可以在if语句中使用这个掩码来检查施法者是否标记位副本目标。
Code lua:
  1. function MyMod.SPELL_CAST_START(_, _, srcGUID, srcName, srcFlags, _, _, _, _, spellName, spellSchool)
  2.   if bit.band(spellSchool, SCHOOL_MASK_HOLY) ~= 0
  3.   and bit.band(srcFlags, COMBATLOG_OBJECT_RAIDTARGET_MASK) ~= 0 then
  4.     print(srcName..” begins to cast ”..spellName..”!”)
  5.   end
  6. end
复制代码
有许多插件类型可以明智地使用这些标志,并从中获益。例如,PVP(玩家vs.玩家)插件可以使用单位标志位来识别敌对和友好的玩家,而副本插件可以使用它们来识别主坦克和标记的怪物。
  我告诉过你,我们会写一个插件例子,但是你在这一节看到的一切都是关于战斗日志事件的高度理论性的描述。让我们做一些联系,并构建一个真正的插件。










分享到:  QQ好友和群QQ好友和群
收藏收藏
回复

使用道具 举报

该用户从未签到

2380

主题

2433

帖子

9139

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
9139
QQ
沙发
 楼主| 发表于 2023-12-22 11:27:22 | 只看该作者
建 立 一 个 冷 却 监 视 器
(Building a Cooldown Monitor)
我们将建立一个插件,显示你的团队成员的法术冷却时间,如英勇/嗜血或重生。我们将这个插件称为冷却监视器(CooldownMonitor)。为它创建一个文件夹,并创建一个包含有以下条目的.toc文件。
Code c:
  1. ## Interface:30100
  2. ## Title: Cooldown Monitor
  3. CooldownMonitor.xml
  4. CooldownMonitor.lua
  5. CooldownBars.lua
复制代码
你现在还应该创建XML和Lua文件,这样当我们以后使用这些文件时,你就不用重新启动你的游戏了。让我们从构建主要Lua文件CooldownMonitor.lua开始。

○ 检测法术(Detecting Spells)
  插件需要检测某些法术并对它们做出反应。最简单的解决方案是在处理程序中使用一串长长的if-then-elseif-end代码块来检查该事件是否是我们的法术之一。一个更好的解决方案是使用一个表(table)来存储我们想要使用的所有法术信息。我们需要存储的只是这个法术的事件、法术ID和冷却时间。下表存储了一些有趣的法术及其事件信息。
Code lua:
  1. local spell = {
  2.   SPELL_CAST_SUCCESS = {
  3.   [29166] = 360, -- Druid: Innervate (6 min cooldown)
  4.   [32182] = 600, -- Shaman: Heroism (Alliance)
  5.   [2825] = 600, -- Shaman: Bloodlust (Horde)
  6.   [22700] = 600, -- Field Repair Bot 74A (10 min duration)
  7.   [44389] = 600, -- Field Repair Bot 110G (10 min duration)
  8.   },

  9.   SPELL_RESURRECT = {
  10.   [20748] = 1200, -- Druid: Rebirth (20 min cooldown)
  11.   }

  12.   SPELL_CREATE = { -- SPELL_CREATE is used for portals, and they vanish after 1 min
  13.   [53142] = 60, -- Portal: Dalaran (Alliance/Horde)
  14.   [33691] = 60, -- Portal: Shattrath (Alliance)
  15.   [35717] = 60, -- Portal: Shattrath (Horde)
  16.   [11416] = 60, -- Portal: Ironforge
  17.   [10059] = 60, -- Portal: Stormwind
  18.   [49360] = 60, -- Portal: Theramore
  19.   [11419] = 60, -- Portal: Darnassus
  20.   [32266] = 60, -- Portal: Exodar
  21.   [11417] = 60, -- Portal: Orgrimmar
  22.   [11418] = 60, -- Portal: Undercity
  23.   [11420] = 60, -- Portal: Thunder Bluff
  24.   [32667] = 60, -- Portal: Silvermoon
  25.   [49361] = 60, -- Portal: Stonard
  26.   },
  27. }
复制代码
你可以很容易地添加新的法术,只需要在Wowhead或你的战斗记录中查找法术ID,然后把它添加到正确的事件中。当你运行斜杠指令/combatlog时,你可以使用创建的战斗日志文件来确定一个正确的法术事件。为了达到测试的目的,添加一个简单的法术是个不错的主意。我添加了下表作为测试法术的子表,48071是快速治疗(11级)的法术ID。
Code lua:
  1. SPELL_HEAL = {
  2.   [48071] = 60  Flash Heal (Test)
  3. }
复制代码
事件处理程序现在需要读取该表。下面的代码使用可变参数(vararg)上的select来检索感兴趣的参数。你还可以将我们需要的所有10个参数添加到头函数中,但这种处理不是很清楚。我们只需要子事件,施法者的名字、施法者的标志位(flags)、法术ID和法术名称。然后我们可以使用事件和法术ID来检查表中是否包含法术/事件组合。
  施法者的标志位(flags)可以被用来检查玩家是否在我们团队中,如果没有设置从属(affiliation):局外人(outsider)标志是否被设置。还可以检查是否是我的(mine)、阵营(party)、或副本标志(raid flags)被设置。但是这四个标志位总是相互排斥的。每个单位总是恰好只有其中之一。这意味着,如果没有设置外部标志位,则必须设置其他标志位中的一个。
  我们还将使用一个被称为CooldownMonitor的表,放置所有需要从外部访问的所有函数。我们需要做的另一件事是创建局部变量onSpellCast,它被我们的事件处理程序访问。稍后我们将在其中存储一个函数。
Code lua:
  1. CooldownMonitor = {}
  2. local onSpellCast

  3. local function onEvent(self, event, ...)
  4.   if event == “COMBANT_LOG_EVENT_UNFILTERED” then
  5.     local event = select(2, ...)  get the subevent
  6.     local sourceName, sourceFlags = select(4, ...)  caster’s nameand flags
  7.     local spellID, spellName = select(9, ...)  spell ID and name
  8.     --check if we need the event and spell ID
  9.     -- and check if the outsider bit is not set, meaning the unit is in our group
  10.     if spells[event] and spells[event][spellId]
  11.     and bit.band(sourceFlags, COMBATLOG_OBJECT_AFFILIATION_OUTSIDER) == 0 then
  12.       local cooldown = spells[event][spellId]
  13.       onSpellCast(cooldown, sourceName, spellId, spellName)
  14.     end
  15.   end
  16. end

  17. local frame = CreateFrame(“Frame”)
  18. frame:RegisterEvent(“COMBAT_LOG_EVENT_UNFILTERED”)
  19. frame:SetScript(“OnEvent”, onEvent)
复制代码
每当有人施放了我们前面定义的法术,就会调用onSpellCast函数。onSpellCast函数调用startTimer(我们稍后会写这个函数)来显示一个施法条,显示玩家的名字和法术的冷却时间。onSpellCast的另一个功能是在聊天框中生成和显示一个简短的通知。例如这样一条信息。
  1. [player] just cast [spell] (Cooldown: x Minutes)
复制代码
其中的[player]和[spell]应该是可以点击的链接。我们在构建DKP插件的时候使用了聊天链接。但我们还没有创建任何链接。在这样的消息中使用一些颜色来突出显示链接会更好。我们需要转义序列来创建聊天框中的链接和彩色文本。
在聊天框中使用转义序列(Using Escape Sequences in Chat Frame)
  转义序列(具有特殊含义的序列,如颜色代码)可以在游戏中的所有聊天框和字体字符串中使用。转义序列是管道符号(pipe symbol):“|”。我们可以用一个简单的例子来测试这一点,该例子应该打印输出一些红色文本(更多颜色将在下节中介绍):
Code lua:
  1. print(“|cFFFF0000red text!”)
复制代码
 只有当您将其写入一个插件的Lua文件时,这才能工作。尝试通过/script或TinyPad这样的游戏编辑器来执行它,只会打印没有颜色的原始代码。这样做的原因是,游戏会自动将用户输入的“|”替换成“||”。这两个管道符号“||”是用于表示没有特殊含义的普通管道符号,就像Lua字符串中的反斜杠一样。
  但是有一个Lua转义代码允许我们向游戏中输入的Lua字符串注入一个普通管道:“T”。尝试在游戏中输入以下内容:
Code lua:
  1. print(“TCffff0000red text!”)
复制代码
这显示了一条红色消息“red text!”在聊天框中。但为什么是红色的,这些十六进制数是什么意思呢?

  · 彩色文本(Colored Texts)
  颜色代码的格式是“|cAARRGGBB”。它是由四个值组成:alpha(透明度A)、red(红R)、green(绿G)和blue(蓝B)。所有这些值都是用两个十六进制数写入的,这意味着可能最低值是00,最高值是FF(十进制的255)。alpha的值实际上是被游戏忽略了,你可以在这里使用任何有效的十六进制值。但你应该在这里使用FF(完全可见),以防某个补丁添加了对它的支持。然后根据红、绿、蓝三原色的值来构建颜色。(如果你不熟悉RGB颜色模型,维基百科有一个很好的解释。http://en.wikipedia.org/wiki/RGB_color_model)
  举个例子,|cFF00FF00生成绿色文本、|cFF0000FF生成蓝色文本、|cFF111111生成灰色文本、|cFF000000是黑色、|cFFFFFFFF是白色。颜色代码不区分大小写,所以你也可以使用小写字母。
  一旦定义,颜色将一直使用到下一个颜色定义或直到遇到游戏的颜色重置代码“|r”。颜色重置后的文本将是该文本元素的默认颜色。通过使用print添加的一行的默认颜色总是白色。但是也可以通过使用聊天框对象的AddMessage方法,向聊天框中添加另一种默认颜色的行。
  当我们之前写ChatlinkTooltips插件的时候,你看到了聊天框对象。它们存储在全局变量ChatFrame1到ChatFrameN中。但哪个聊天框是默认的呢?着很可能是ChatFrame1,但你不能确定。因此UI定义了另一个全局变量,它始终代表默认的聊天框:DEFAULT_CHAT_FRAME。这意味着我们需要使用以下参数,来调用此框架的方法AddMseeage,以打印使用默认颜色的消息:
Code lua:
  1. DEFAULT_CHAT_FRAME:AddMessage(message, r, g, b)
复制代码
参数r、g、b代表颜色红、绿、蓝的值。注意,这里的最大值是1,最小值是0。所以,r=1,g=1,b=1得到了默认的白色。
  如果你想给整个聊天信息上色,使用AddMessage应用默认颜色总是比使用|c转义序列更好。就像我们在第六章中写的迷你插件,这个默认的颜色也会被添加到聊天框的时间戳的钩子使用。使用print总是会得到一个白色的时间戳。
注意:不可能在聊天框中嵌入颜色代码。被传递给SendChatMessage的字符串中使用颜色代码会生成一条错误信息。
许多插件为AddMessage方法使用了一个包装函数(wrapper function)来添加插件的名称作为前缀,并设置颜色。让我们为冷却监视器编写这样一个函数。对于像我们这样的小插件,将这个函数保存在一个局部变量种就足够了。较大的插件应该将其打印处理程序(print handler)存储在表中(作为插件的命名空间),因为许多文件都可以在表中访问该函数。我们将简单地在文件CooldownMonitor.lua的作用域中创建一个名为print的本地变量,这样我们就可以像在以前的插件中所做的那样使用print了。此函数应防止在文件的开头,以确保本地变量在文件中的所有其他函数中都可见。
Code lua:
  1. local chatPrefix = “|cffff7d0a<|r|cffffd200CooldownMonitor|r|cffff7d0a|r”
  2. local function print(...)
  3.   DEFAULT_CHAT_FRAME:AddMessage(chatPrefix..string.join(“ ”, tostringall(...)), 0.41, 0.8, 0.94)
  4. end
复制代码
该函数利用了tostringall和string.join,因此,它的表现就像原始的打印处理程序。但它给我们的消息添加了应该漂亮的彩色前缀。

  · 超链接(Hyperlink)
  使用以下格式的转义序列创建链接:
Code lua:
  1. |Hlink|htext|h
复制代码
 “|H”标记链接的开始,下面的“link”是该链接的数据。数据字符串的末尾是“|H”,后面是显示的可点击的“text”,后面跟着“|H”。在前面创建DKP插件时,我们看到了一些链接类型;链接基本上只是一个字符串,它被传递给处理超链接事件(如OnHyperLinkClicked)的事件处理程序。让我们来看一个完整的条目链接。我们已经看到了商品的链接,它由描述商品的长串数字组成。例如,一个制作符文铜棒(Runed Titanium Rod)的背后代码是这样的:
Code lua:
  1. |cff0070dd|Hitem:44452:0:0:0:0:0:0:0:1352691344:80|h[Runed Titanium Rod]|h|r
复制代码
我们可以清楚地看到开头链接的颜色、链接ID和显示的文本,最后,后面是链接(|h)和颜色的结束代码(|r)。
注意:不可能将一个未知的项目链接到服务器。这些项目可能是从你的战斗群组(battlegroup)中还没被杀死的boss。服务器还会检查聊天信息中链接的名称和颜色。如果名称或颜色不正确,该链接将从聊天信息中删除。
链接中第一个冒号之前的子字符串用于指示链接的类型。这个例子的类型是一个物品链接,但是我们需要一个玩家和一个法术链接。一个玩家的链接看起来是这样的:
Code lua:
  1. |Hplayer:name|htext|h
复制代码
并且一个法术链接是这样的:
Code lua:
  1. |Hspell:spellId|htext|h
复制代码
 我们现在可以创建一个函数onSpellcast。添加该函数到文件CooldownMonitor.lua的末尾:
Code lua:
  1. local castInfo = “|Hplayer:%1$s|h[%1$s]|h cast |cFF71D5FF|Hspll:%d|h[%s]|h|r(cooldown: %d minutes)”
  2. function onSpellCast(timer, player, spellId, spellName)

  3.   print(castInfo:format(player, spellId, spellName, timer / 60))
  4.   CooldownMonitor.StartTimer(timer, player, spellName, texture)
  5. end
复制代码
这里使用的格式字符串可能看起来比实际要困难得多。第一个是玩家链接,它需要两次玩家的名字:作为链接和作为显示文本。我们可以通过使用“%1$s”启动替换指令来选择传递给string.format的一个参数,其中n是所需参数的数目(s也可以是任何其他格式指令)。这允许我们在这里两次使用第一个参数。第二个链接是一个拼写连接,其默认颜色取值默认UI。
  另一个不错的功能是在信息中加入一个法术的小图标。我们也可以用这些转义代码来创建。

  · 纹理(Texture)
  表示纹理的转义序列是这样的:
Code lua:
  1. |Tfile:height:width:xoffset:yoffset|t
复制代码
 除文件和高度以外的所有参数都是可选的。默认宽度与高度相同,默认偏移量为0。如果高度(height)设置为0,则使用字体高度。你应该始终将高度设置为0。因为错误的值会扭曲字体。如果高度为0且宽度被设置,它将被理解为相对于字体高度的一个值。
  例如,代码“|Tfoo.tga:0|t”显示的纹理文件foo.tga缩放到一个正方形文本宽度相同的高度,“|Tfootga:0:2|t”显示相同的纹理,高度设置为字体高度,宽度设置为字体高度的两倍,“|Tfootga:10:20|t”显示大小为10x20的foo.tga。
  1. 注意:出于安全原因,不可能将纹理嵌入到聊天信息中。
复制代码
现在我们有法术ID,但我们需要它的纹理。有一个可用的API函数可以返回几乎所有我们需要的关于法术的信息。这个函数名称是GetSpellInfo,它的参数可以是一个法术的ID、一个法术名称或法术链接。
  1. name, rank, iconTexture, cost, isFunnel,
  2. powerType, castTime, minRange, maxRange
  3. -  GetSpellInfo(spell)
复制代码
我们只需要第三个返回值,它包含与这个法术相关的纹理的文件名。将旧版本的onSpellCast功能替换为这个新的改进的功能,它还会在您的聊天中显示一个图标。
Code lua:
  1. local castInfo = “|Hplayer:%1$s|h[%1$s]|h cast |T%s:0|t|cFF1D5FF|Hspell:%d|h[%s]|h|r (Cooldown: %d minutes)”
  2. function onSpellCast(timer, player, spellId, spellName)
  3.   local texture - select(3, GetSpellInfo(spellId))
  4.   print(castInfo:format(player, texture, spellId, spellName, timer / 60))
  5.   CooldownMonitor.StartTimer(timer, player, spellName, texture)
  6. end
复制代码
该代码在拼写链接前面添加了一个|T转义序列,并将纹理作为附加参数添加到string.format。这将在信息消息中添加一个小图标。纹理的路径现在也被传递到startTimer,因为纹理在时间上也是有用的。
  还有一个小问题:如果我们的法术冷却时间只有1分钟会发生什么?文字仍然会说:“Cooldown: 1 minutes”。这个游戏为我们提供了一种处理这种情况的简单方法。

  · 语法转义序列(Grammatical Escape Sequence)
  有三种转义序列可用来处理格式化文本时可能出现的语法问题。第一种格式如下。
Code lua:
  1. digit |1singular;plural1;plural2;
复制代码
 如果前面的数字是1,它将显示singular,否则为plural1。plural2是可选的,如果提供的数字是2,他将被用来代替plural1。第二种形式的复数在俄语等语言中很有用。
  这个转义序列只考虑它左边的数字。所以11 |1singular;plural;将导致11 singular。很少需要这个转义序列,下面这个更有用,因为它考虑了整个数字。
Code lua:
  1. number |4singular:plural1:plural2;
复制代码
它的工作原理与|1类似,但是使用整体来确定是使用singular还是plural1/2。plural2文本也是可选的。这是我们在格式字符串castInfo中需要的。让我们把那个改字符串改为下面的字符串:
Code lua:
  1. local castInfo = “|Hplayer:%1$s|h[%1$s]|h cast |T%s:0|t|cFF71D5FF|Hspell:%d|h[%s]|h|r (Cooldown: %d |4minute:minutes;)”
复制代码
注意:|1使用分号分隔不同的形式,而|4需要冒号作为分隔符,但在末尾使用分号
第三个转义序列是|2。格式就是|2sometext。如果sometext以元音开头,则显示为d’sometext,否则显示为de sometext。
注意:这些语法转义序列不能在聊天信息中使用。
现在我们可以将漂亮的消息打印到聊天框中,functiononSpellCast也完成了。我们的下一个任务是构建CooldownMonitor.StartTimer函数,它将创建一个可视化计时器并将其显示在屏幕上。我们将在一个单独的文件中创建这个函数及其所有辅助函数(你可能已经猜到了它的名字:CooldownBars.lua),以便在下一章中用一个库轻松替换它。对于这种冷却计数器来说,一个好的框架类型是状态栏。它代表一个进度条,可以被你在屏幕上看到的施法条、生命条和法力条使用。状态栏与滑块(sliders)非常相识,因为它们有最小值、最大值和当前设置的值。因为框架类型的名称,使用状态栏显示的计时器通常被称为“状态栏计时器”。
建立状态栏计时器(Building Status Bar Timer)
  让我们首先为这样的计时器构建一个XML模板。创建模板的第一个问题总是,是否已经有一个由暴雪定义的模板?答案是肯定的,文件InterfaceFrameXMLCastingbarFrame.xml定义了模板CastingBarFrameTemplate,它适合于我们的目的。

  · 建立一个模板(Building a Templat)
  在使用这个模板之前,我们需要对它做一些修改。默认的施法栏不显示计时器,所以我们必须添加计时器,最好是在框架右边作为一个小字体字符串。另一个问题是在这个施法条框体架中居中。我们想要的锚点在它的左边,因为我们需要额外的空间在我们的冷却计时器的右边半个框体。第三个问题是模板定义了一些特定于施法栏的脚本处理程序。我们将不得不改写它们。
  我们将从这个已存在的模板派生我们的模板,因为它仍然比重写整个模板更容易。下面的代码显示了我们的新模板。在查看以下代码时,请在文件InterfaceFrameXMLCastingbarFram.xml中打开模板CastingBarFrameTemplate。着使得理解完整的模板看起来更容易。
Code xml:

  1.   
  2.     
  3.       
  4.     
  5.     
  6.       
  7.         
  8.         
  9.           
  10.             
  11.               
  12.                 
  13.               
  14.             
  15.           
  16.         
  17.       
  18.     
  19.     
  20.       
  21.       
  22.       
  23.         _G[self:GetName()..”Text”]:ClearAllpoints()
  24.         _G[self:GetName()..”Text”]:SetPoint(“LEFT”,6,3)
  25.         _G[self:GetName()..”Text”]:SetWidth(155)
  26.         _G[self:GetName()..”Text”]:SetJustifyH(“LEFT”)
  27.         _G[self:GetName()..”Text”]:ClearAllPoints()
  28.         _G[self:GetName()..”Text”]:SetPoint(“RIGHT”, self, “LEFT”, -5, 2)
  29.         _G[self:GetName()..”Text”]:SetWidth(20)
  30.         _G[self:GetName()..”Text”]:SerHeight(20)
  31.       
  32.       
  33.         self.obj:Update(elapsed)
  34.       
  35.     
  36.   
复制代码
我们的模板CooldownBarTemplate现在有以下几个子节点(children):
Text:固定在框架中心的字体字符串。这是由原始模板定义的,我们修改它的唯一方法是使用Onload处理程序。
Timer:剩下的时间,这个字体字符串是由模板添加的。
Border:框架周围的边框,由原始模板定义。
Icon:定时器左边的一个16x16纹理,它稍后会显示法术的图标。这也是由原始模板定义的。这个图标的大小在处理程序中更改为20x20,它的位置稍微调整了一下。
Spark:一个需要移动到状态栏当前位置的发光效果纹理。这也是由原始模板创建的。
Flash:可以用来在工具条上添加flash效果的纹理。该子组件是由原来模板创建的。
我们不会在XML文件中使用这个模板,因为我们不会用XML创建框体。没有办法预测插件需要多少定时器,可能同时存在10个CD或只有1个。我们将使用CreateFrame动态地从这个模板创建计时器。注意,它将生成应该错误消息,因为我们还没有实现OnUpdate方法。忽略此错误,或通过将其标记为注释来禁用OnUpdate方法。
Code lua:
  1. local f = CreateFrame(“StatusBar”, “TestTimer”, UIParent, “CooldownBarTemplate”)
  2. _G[f:GetName()..”Text”]:SetText(“Text Timer”)
  3. _G[f:GetName()..”Timer”]:SetText(“1:00”)
  4. _G[f:GetName()..”Flash”]:Hide()
  5. f:SetPoint(“CENTER”)
复制代码
第一行创建一个StatuBar类型的新框体,它继承自模板。它的名字是TestTimer,它的父框体是UIParent。第二行访问框架的子Text,并调用此字体字符串上的SetText来设置测试文本。第三行对计时器执行相同的操作。第四行隐藏默认显示的纹理Flash。最后一行设置了一个点,这样我们的框架在屏幕中央就可见了。你可以试着和各种各样的孩子一起玩,就像图标一样。另一个有趣的测试是尝试将一个很长的字符串设置为文本,这样检查它是否被正确截断。
  但定时器还不起作用。它不会倒计时,而且火花(spark)在中间而不是末尾。为此,我们将需要OnUpdate方法。它将通过调用其方法SetValue来更新状态栏的位置,将火花的位置设置为与该栏和计时器的文本相匹配。它也会在时间为0时隐藏它。
  该功能存储在对象的方法更新中,对象的方法更新存储在状态栏的键self(the key self)下。也可以使用状态栏框架作为对象,但它已经有了自己的元表。我们必须将调用转发给这个原始的元表,因此在表示帧的表中存储对象会更容易一些。

  · 处理定时器(Handing the Timers)
  表示一组状态栏计时器的良好数据结构是双向链表。它允许我们通过访问表中的字段来获取上一个计时器和下一个定时器。这允许我们在帧集中移除计时器。这个双向链表中的每一个条目(entry)都是一个计时器对象。
  下面所有的Lua函数都应该放在文件CooldownBar.lua中。我们首先需要创建一些稍后使用的局部变量。
Code lua:
  1. -- the first and last timer objects in the double-linked list
  2. local firstTimer, lastTimer

  3. -- prototype for the timer object
  4. local timer = {}
复制代码
 现在我们需要构建实际的计时器对象。我们的构造函数是startTimer函数。它创建一个状态栏对象并显示它。实际对象也存储在这个框架(frame)中。这个对象持有一个框架的引用,因此我们可以很容易地获得属于这个对象的框架。这个对象被添加到表示一组计时器的双向链表中。
  但是如果计时器过期了,框体被隐藏了,会发生什么呢?没有办法删除现有的框体(垃圾回收器不能删除框体),因此重用它是一个好主意。我们需要将当前所有未使用的框体存储在某个地方,如果存在旧框体,构造函数将回收旧框体。堆栈是存储未使用框体的良好数据结构。我们将过期计时器的对象推入堆栈。这里的弹出操作与我们在前面示例中看到的堆栈略有不同。它永远不会返回nil,因为它只是创建一个新的框体,并在堆栈为空时返回它。我们将调用函数popOrCreateFrame来表示这种行为。
  每个框体都需要一个名字,因为我们需要这个名字来获得框体的子框体。我们只是在这里使用了一个计数器,它计算每次弹出操作创建新框体的次数。
  下面的代码片段展示了这样一个堆栈。它在这里被实现为一个链表,这意味着我们只将当前在堆栈顶部的对象存储在一个变量中(在本例中为frameStack)。然后这个对象有next字段,它指向堆栈上的下一个对象。
Code lua:
  1. local popOrCreateFrame, pushFrame
  2. do
  3.   local id = 1
  4.   local frameStack -- the object on the top of the stack
  5.   
  6.   -- pops a frame form the stack or creates a new frame
  7.   function popOrCreateFrame()
  8.     local frame
  9.     if frameStack then -- old frame exists
  10.       frame = frameStack -- re-use it...
  11.       -- ...and remove it from the stack by changing the object on the top
  12.       frameStack = frameStack.next
  13.       frame:Show() -- make sure that it’s shown as it might be hidden
  14.     else -- stack is empty...
  15.       -- ...so we have to create a new frame
  16.       frame = CreateFrame(“StatuBar”, “CooldownMonitor_Bar”..id, CooldownMonitor_Anchor, “CooldownBarTemplate”)
  17.       id = id + 1 -- increase the ID
  18.     end
  19.     return frame
  20.   end
  21.   
  22.   -- pushes a frame on the stack
  23.   function pushFrame(frame)
  24.     -- delete the reference to the object to allow
  25.     -- the garbage collector to collect it
  26.     frame.obj = nil
  27.     -- the next object on the stack is the one that is currently on the top
  28.     frame.next = frameStack
  29.     -- the new object on the top is the new one
  30.     frameStack = frame
  31.   end
  32. end
复制代码
我们在pop函数中引用一个当前未定义的框体。这个框体的名字是CoolownMonitor_Anchor。它将是第一个bar的锚点,也是所有bar的父节点。第二个bar将被固定在第一个bar上,以此类推。这个锚点的位置是通过使用框架的SetUserPlaced方法保存的。将以下框架放置在CooldownMonitor.xml文件的<UI>标记中。把这些代码放在模板之前还是之后都没有关系。不要把它放在模板中,因为它是一个全新的框架,与我们的状态栏模板没有任何关系。
Code xml:
  1. <Frame name=”CooldownMonitor_Anchor” movable=”true” parent=”UIParent”>
  2.   <Anchors>
  3.     <Anchor point=”CENTER”>
  4.       <Offset>
  5.         <AbsDimension x=”300” y=”0”/>
  6.       </Offset>
  7.     </Anchor>
  8.   </Anchors>
  9.   <Size> <!-- required in order to make the frame visible -->
  10.     <AbsDimension x=”1” y=”1”/> <!-- 0x0 would not work here!-->
  11.   </Size>
  12.   <Scripts>
  13.     <OnLoad>
  14.       self:SetUserPlaced(1)
  15.     </OnLoad>
  16.   </Scripts>
  17. </Frame>
复制代码
注意:没有有效大小的框体不仅是不可见的,它们也不能被其他框体作为锚点使用。0x0不是有效的大小。也可以使用多个跨越框架的锚点。
如果我们能够在Lua中创建框体并将其存储在一个局部变量中,那么它将变得更短。但是SetUserPlaced方法要求我们的框体有一个名称,因为该名称用于表示框体。因此,无论如何,我们需要一个全局变量。在使用Lua创建框架时,我们还需要注意默认的位置。在调用SerPoint创建框体后,我们不能覆盖保存的位置。这意味着在设置默认位置之前,我们必须检查是否已经有一个保存的位置。由于这些问题,在这里使用XML创建框架更容易。
  现在我们有了框体模板和锚点,我们的下一个任务是构建框体对象和构造函数。

  · 状态栏计时器的构造(The Status Bar Timer Constructor)
  这个构造函数创建一个带有相应框体的计时器对象,并将这个对象添加到双向链表中。它还是设置计时器上显示的文本、法术图标的纹理和计时器的颜色。我们将使用为计时器施放法术的玩家的职业颜色。我们需要一个助手函数,它接受一个玩家的名称并返回这个玩家的类。这个功能需要迭代整个团队(party)或副本(raid)来找到玩家。
  下面的代码应该放在文件CooldownMonitor.lua的末尾:
Code lua:
  1. -- gets a class of a player in your party or raid
  2. function CooldownMonitor.GetClassByName(name)
  3.   -- check if we are looking for ourselves
  4.   if UnitName(“player”) == name then
  5.     -- the first return value of UnitClass is localized, the second one is not
  6.     return select(2, UnitClass(“player”))
  7.   end
  8.   -- iterate over the party (if we are in a party)
  9.   for i = 1, GetNumPartyMembers() do
  10.     if UnitName(“party”..i) == name then
  11.       return select(2, UnitCalss(“party”..i))
  12.     end
  13.   end
  14.   -- still no match, iterate over the whole raid (if we are in a raid)
  15.   for i = 1, GetNumRaidMembers() do
  16.     -- no need to work with the GUID here as the player’s name is unique
  17.     if UnitName(“raid”..i) == name then
  18.       return select(2, UnitClass(“raid”..i))
  19.     end
  20.   end
  21.   -- that player isn’t part of our party/raid
  22.   return “unknown”
  23. end
复制代码
现在我们可以创建CooldownMonitor.StartTimer函数,它类似于状态栏计时器对象的构造函数。它完成所有的困难功能,并创建一个新的状态栏计时器并启动它。把它放在CooldownBars.lua的末尾。
Code lua:
  1. local mt = {__index = timer} -- metastable
  2. -- the constructor
  3. function CooldownMonitor.StartTimer(timer, player, spell, texture)
  4.   local frame = popOrCreateFrame() -- create or recycle a frame
  5.   local class = CooldownMonitor.GetClassByName(player)
  6.   -- set the color the status bar by using color informations from the table
  7.   -- RAID_CLASS_COLORS that contains the default colors of all classes
  8.   if RAID_CLASS_COLORS[class] then
  9.     local color = RAID_CLASS_COLORS[class]
  10.     frame:SetStatuBarColor(color.r, color.g, color.b)
  11.   else -- this should actually never happen
  12.     frame:SetStatusBarColor(1, 0.7, 0) -- default color from the template
  13.   end
  14.   -- set the text
  15.   _G[frame:GetName()..”Text”]:SetFormattedText(“%s: %s”, player, spell)
  16.   -- and the icon
  17.   local ok = _G[frame:GetName()..”Text”]:SetTexture(texture)
  18.   if ok then
  19.     _G[frame:GetName()..”Icon”]:Show()
  20.   else -- hide the texture if it couldn’t be loaded for some reason
  21.     _G[frame:GetName()..”Icon”]:Hide()
  22.   end
  23.   -- add a short flash effect by fading out the flash texture
  24.   UIFrameFadeOut(_G[frame:GetName()..”Flash”], 0.5, 1, 0)
  25.   
  26.   local obj = setmetatable({ -- this is the actual object
  27.     frame = frame, -- the frame is stored in the object...
  28.     totalTime = timer,
  29.     timer = timer -- this is the remaining time, it will be decremented later}, mt)
  30.   frame.obj = obj -- ...and the object in the frame
  31.   -- add the object to the end of the list
  32.   if firstTimer == nil then -- our list is empty
  33.     firstTimer ==obj
  34.     lastTimer = obj
  35.   else -- our list is not empty, so append it after the last entry
  36.     -- the element in front of our object is the old last element
  37.     obj.prev = lastTimer
  38.     -- the element after the old last element is our object
  39.     lastTimer.next = obj
  40.     -- the new last element is our object
  41.     lastTimer = object
  42.   end
  43.   obj:SetPosition()
  44.   obj:Update(0)
  45.   return obj -- return the object
  46. end
复制代码
该函数从popOrCreateFrame获取框体,并通过设置其文本、图标和颜色来初始化它。然后它在纹理flash上使用UI函数UIFrameFadeOut,这导致一个flash效果。然后构造函数创建实际对象,并将其添加到双链表中。
  然后调用对象的SetPosition方法,该方法将设置框体的位置。接下来我们将实现这个方法。它还调用参数为0的Update方法。这将设置状态栏的初始值和字体字符串计时器的文本。

  · 定位(Positioning)
  让我们创建SetPosition方法(将其放在文件的末尾)。该方法只需要根据链表中框体的位置来设置点。如果它是列表中的第一个元素,它需要锚定到我们之前创建的锚点。否则,它使用前一框体作为锚点。
Code lua:
  1. function timer:SetPosition()
  2.   self.frame:ClearAllPoints()
  3.   if self == firstTimer then -- it’s the first timer
  4.     self.frame:SetPoint(“CENTER”, CooldownMonitor_Anchor, “CENTER”)
  5.   else -- it’s not the first timer, anchor it to the previous one
  6.     self.frame:SetPoint(“TOP”, self.prev.frame, “BOTTOM”, 0, -11)
  7.   end
  8. end
复制代码
这个方法说明了为什么在这里选择双向链表是明智的。我们可以只写self.prev来获取前一个计时器对象,而self.prev.frame来获取它的框体。
  我们的下一个方法是Update方法,它更新框体。

  · 更新计时器(Updating Timers)
  Update方法将对象的属性计时器减少自上次调用该对象以来经过的时间。如果剩余时间小于或等于0,该方法将取消计时器。如果不是这种情况,它将更新状态栏的值、计时器的文本和火花(spark)的位置。这里我们需要一个小的助手函数,将计时器格式化为我们可读的格式,然后显示出来。这个函数应该放在文件的末尾。
Code lua:
  1. local function stringFromTimer(t)
  2.   if t < 60 then -- less then 60 seconds --> don’t show minutes
  3.     return string.format(“%.1f”, t)
  4.   else -- 60 seconds or more remaining --> display minutes
  5.     return string.format(“%d:%0.2d”, t/60, t%60)
  6.   end
  7. end

  8. function timer:Update(elapsed)
  9.   self.timer = self.timer - elapsed
  10.   if self.timer <= 0 then -- time’s up
  11.     self:Cancel() -- cancel the timer
  12.   else
  13.     -- currentBarPos holds a value between 0 and 1
  14.     local currentBarPos = self.timer / self.totalTime
  15.     -- the min value of a status bar timer is 0 and the max value 1
  16.     self.frame:SetValue(currentBarPos)
  17.     -- update the text
  18.     _G[self.frame:GetName()..”Timer”]:SetText(stringFromTimer(self.timer))
  19.     -- set the position of the spark
  20.     _G[self.frame:GetName()..”Spark”]:SetPoint(“CENTER”, self.frame, “LEFT”, self.frame:GetWidth()*currentBarPos, 2)
  21.   end
  22. end
复制代码
  1. 注意:OnUpdate脚本处理程序仅在显示框架时调用。所以我们可以假设计时器(timer)在运行。
复制代码
我们的下一个任务是创建Cancel方法来取消计时器。这个Cancel方法必须负责从双向链表中移除对象并回收框体。

  · 取消计时器(Canceling Timers)
  Cancel方法基本上只是从列表中删除一个计时器,隐藏框体,并将框体推到堆栈上,堆栈存储当前未使用的框体。这个函数应该放在Lua文件的末尾:
Code lua:
function timer:Cancel()
  -- remove it from the list
  if self == firstTimer then
    firstTimer = self.next
  else
    node.prev.next = node.next
  end
  if self == lastTimer then
    lastTimer = self.prev
  else
    self.next.prev = self.prev
  end
  -- update the position of the next timer if there is a next timer
  if self.next then
    self.next:SetPosition()
  end
  self.frame:Hide() -- hide the frame...
  pushFrame(self.frame) -- ...and recycle it
end
插件现在可以正如我们所预期的那样正常工作。但仍有一个问题:我们目前无法将栏(bars)移动到另一个位置。

  · 移动计时器(Moving the Timers)
  我们需要一个斜杠命令处理程序,显示一个可以移动的虚拟计时器。斜杠命令“移动”或“解锁”将启动移动过程。我们的斜杠命令处理程序将解锁框体45秒,并显示计时器显示剩余时间。我们还需要我们的计时库SimpleTimngLib在45秒后锁定框体,因此请确保它被安装,并将其作为依赖项添加。我们将使用锚的解锁属性来表示它已解锁。
下面的代码显示了斜杠命令,它应该放在Lua文件的末尾。
Code lua:
local timingLib = simpleTimingLib:New()
local function lock()
  CooldownMonitor_Anchor.unlocked = false
end

SLASH_COOLDOWNMONITOR1 = “/cooldownmonitor”
SLASH_COOLDOWNMONITOR2 = “/cm”
SlashCmdList[“COOLDOWNMONITOR”] = function(msg)
  local cmd = msg:trim():lower()
  if cmd == “unlock” or cmd == “move” then
    startTimer(45, “Status”, “unlocked”)
    CooldownMonitor_Anchor.unlocked = true
    timingLib:Schedule(45, lock)
  end
end
注意:我们的插件现在依赖于SimpleTimingLib,所以您应该将他作为应该依赖项添加到TOC文件中。我们将在下一章中看到如何摆脱这种依赖,并将库嵌入到我们的插件中,这一章将详细解释库。
  但是计时器不会因为我们在这里设置了一个属性而变得可移动。我们的模板中还需要一些脚本处理程序。我们需要在状态栏上使用RegisterForDrag方法来接受拖动鼠标事件。我们可以通过XML文件中的状态栏模板CooldownBarTemplate的Onload脚本处理程序中添加以下代码来包含它:
Code xml:
  1. self:RegisterForDrag(“LeftButton”)
复制代码
  1. 然后,我们需要将OnDragStart脚本处理程序添加到状态栏模板CooldownBarTemplate的<Scripts>脚本中。
  2. Code xml:
复制代码
  1. <OnDragStart>
  2.   if self:GetParent().unlocked then
  3.     self:GetParent().StartMoving()
  4.     self.GetParent().moving = self
  5.   end
  6. </OnDragStart>
复制代码
处理程序检查是否设置了锚点的解锁(unlocked)属性(父属性),如果是这种情况,则将父属性附加到鼠标上。我们还将启动移动过程的框体保存在锚点中。
  我们需要告诉这个锚点停止移动,如果我们停止拖动或者当我们移动它的手框体被隐藏了。隐藏框体不会触发OnDragStop脚本,所以当我们移动框体的时候,如果我们点击的计时器过期了,框体会继续移动。
  OnHide处理程序仅当用户单击发起移动的框体时才停止锚点。这可以防止当我们移动另一个计时器时,定位停止。
Code xml:
  1. <OnDragStop>
  2. self:GetParent():StopMovingOrSizing()
  3. </OnDragStop>
  4. <OnHide>
  5.   if self:GetParent().moving == self then
  6.     self:GetParent():StopMovingOrSizing()
  7.   end
  8. </OnHide>
复制代码
我们现在有了一个功能齐全的冷却时间监视器。但它有一个问题,我们没有考虑到一件重要的事情,而且我们的插件目前在某些情况下不能正常工作。你注意到那个bug了吗?

  · 修复上一个错误(Fixing the Last Bug)
  我们目前依赖于框架的OnUpdate脚本处理程序计数计时器。但是谁能保证我们的状态栏计时器一直显示呢?如果它们因为任何原因被隐藏,它们将中止。例如,因为用户按下Alt+Z隐藏UIParent,它们可能被隐藏。或者用户可能在计时器运行时打开世界地图。你可以通过输入/cm move并打开世界地图来测试这个错误。计时器将在世界地图打开期间冻结。但SimpleTimingLib不会。所以在你关闭世界地图后,你仍然会看到“status:unlocked”计时器,但45秒可能已经结束,画面再次锁定。
  这里有用的是框架对象的IsVisible方法。如果框体没有显示,或者它的父框体没有显示,它返回nil。这意味着如果我们的锚显示但不可见,我们可以检测世界地图是打开的还是UI隐藏的。我们创建了一个不是UIparent的子框体,并使用它的OnUpdate方法来检查我们是否丢失了Onupdate事件。把它放在文件CooldownBars的末尾。
Code lua:
  1. local updater=CreateFrame(“Frame”)
  2. updater:SetScript(“OnUpdate”, function(self, elpased))
  3.   if CooldownMonitor_Anchor:IsShow() and
  4.   not CooldownMonitor_Anchor:IsVisible() then
  5.     local timer = firstTimer
  6.     while timer do
  7.       timer:Update(elapsed)
  8.       timer = timer.next
  9.     end
  10.   end
  11. end
复制代码
注意:我们只实现了双向链表的几个函数。我们没有实现迭代器,因为它不值得为一次循环而努力。因此,我们在这里只使用了while循环。该循环从所有框体调用Update方法。
总  结
(Summary)

  本章讨论了如何使用战斗日志。你现在可以创建响应战斗事件的插件。我们还讨论了位域,以及它们如何在《魔兽世界》的战斗日志事件中使用。许多流行的插件,如伤害统计(Damage Meters)或滚动战斗文字(Scrolling Combat Text),只处理战斗日志事件。你现在可以使用这些事件来写你自己的战斗日志插件,这里有很多令人兴奋的东西。
  我们在本章中所编写的示例插件是一个功能齐全的冷却监视器,带有相当不错的冷却时间显示。下一章是关于库(libraries)和如何使用它们。作为一个演示,我们将用库替换状态栏计时器的实现。这些库将给我们的插件带来一个全新的外观(有很多漂亮的东西),并添加一些其他令人兴奋的功能,如配置菜单。
CLEU从8.0开始就不再传入参数,需要用CombatLogGetCurrentEventInfo()
  1. local function onEvent(self, event, ...)
  2.   if event == “COMBAT_LOG_EVENT_UNFILTERED” then
  3.     return onEvent(self, select(2, CombatLogGetCurrentEventInfo()), CombatLogGetCurrentEventInfo())
  4.   elseif MyMod[event] then
  5.     return MyMod[event](...)
  6.   end
  7. end
复制代码























回复 支持 反对

使用道具 举报

快速回复高级模式
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表