[Lua]浅谈战斗记录事件

这篇文章其实是教你如何做一个“技能成功才喊话”的宏的。


前言


先说说“事件驱动(Event-driven)”这个概念吧。事件驱动指的是任何动作都是由某一个特定事件所引发

先假想一个场景:

你有点感冒所以去医院看病,挂了号之后发现居然还要排队,然后护士开始一个一个喊病人的名字……

如果我们把护士喊病人名字作为一个事件(Event),称它为NURSE_CALL_PATIENT好了,那么,当这个事件发生的时候,你会接收到至少一个参数(Argument):她喊了谁的名字(病人名字,我们把它叫做patientName好了)。于是对于你来说,整个处理过程如下:

如果 她喊的名字 就是 我的名字 那么
我进去看病
否则
我接着等
以上

直接逐字逐句翻译我们就可以得到一段Lua代码:

--如果 她喊的名字 就是 我的名字 那么
if patientName == myName then
--我进去看病
    self:Kiss(nurse)
--否则
else
--我接着等
    self:Hug(nurse)
--以上
end

没错,写WOW插件其实就这么简(fu)单(za)。


事件的注册与处理


那么,接下来的问题,就在于你这些动作是根据什么来反应的:没错,护士喊名字。我们继续填充这部分代码:

我会对 护士喊名字 做出反应
我把这个过程叫做 看病

设置对某个事件做出反应,在WOW里称为“注册该事件(Register Event)”。
而这个“过程”其实就是个“函数(Function)”,如同函数的英文直译,就是个“功能”。函数需要一些输入值,并且对这些输入值做出反应,这里自然还是护士喊的病人名字啦。于是翻译到Lua:

--我会对 护士喊名字 做出反应
You:RegisterEvent('NURSE_CALL_PATIENT')
--我把这个过程叫做 看病(即“在事件发生时设置处理脚本‘看病’(Set Script 'kanbing' on Event)”)
You:SetScript('OnEvent', kanbing)

--看病 函数
local function kanbing(self, event, patientName)
--输入值的前两个,self就是你,event就是发生了什么事件,这是WOW对于事件脚本的通用参数。后面patientName就是这个事件产生的参数了。
--然后是我们刚刚写好的
    if patientName == myName then
        self:Kiss(nurse)
    else
        self:Hug(nurse)
    end
end

到此为止,一个标准的事件处理插件就基本完工了。就这样。接下来就是进入实战的阶段了。


战斗事件处理


事件载体

没错……无论我们要执行什么,总得是由某种东西执行的(比如你),对于WOW而言,最常用的方式,就是用一个“窗体(Frame)”来承载。

其实Frame这玩意儿在游戏里随处可见——你看到的一切界面,什么C键O键L键出来的,那都是Frame。只不过,我们要用的Frame没有提供任何外观设计,所以看不见摸不着。

嗯……在这里还要插入一句,那就是为什么插件代码里有很多“local”:local用于定义局部变量,先不管它到底是什么,总之local变量是效率更高的变量,除非万不得已否则一定要用。

通用的Frame载体事件驱动编程的格式如下,跟上面那个例子如出一辙:

--f就是我们的Frame,CreateFrame自然就是“创建Frame”,后面的参数分别是Frame的类型、Frame在游戏/fstack命令显示的名字,我们直接不填,用nil代表“空”、Frame的属主:UIParent,也就是Alt+Z会出现消失的那堆东西,如果你不加这个,会导致Frame不会在Alt+Z时隐藏,当然对于我们这个看不见的Frame来说倒是……不是太有所谓。
local f = CreateFrame('Frame', nil, UIParent)
--还是注册事件,后面还会继续说
f:RegisterEvent('COMBAT_LOG_EVENT_UNFILTERED')
--设置事件处理函数
f:SetScript('OnEvent', eventHandler)

--事件处理函数
local function eventHandler(self, event, ...)
--函数内容
end

注:对于SetScript也有个简写的方法,即直接把函数写进去,这个在做宏的时候很实用,因为宏限制字数:

f:SetScript('OnEvent', function(self, event, ...) 
--函数内容
end)

战斗记录事件

我们这里要响应的事件自然是战斗记录事件。看标题就知道了。

战斗记录事件有两个,COMBAT_LOG_EVENT和COMBAT_LOG_EVENT_UNFILTERED。前一个只返回根据你当前设置的战斗记录过滤方式过滤之后的信息(比如“我的动作”啊,“我发生了什么?”啊),如果单纯的只是想要检测自己的法术,那么这个事件就足够了。后一个事件会返回所有你接收到的战斗记录,比如周围队友的,这样我们可以拿来做DPS统计什么的。

说起来不简单,这两个事件的返回值最多有25个……为什么有这么多,想一下就知道了:这条战斗信息是什么时候产生的、是肉搏还是法术还是别的什么、谁的法术、谁的什么法术、打的是谁、造成了多少伤害、暴击了没、溅射了没、格挡了没、免疫了没……

如果我们只是单纯的打算做“施法成功喊话”,那么不需要知道这么多事情,只要告诉我施法成功了没就可以了。

那么,现在一个小问题是,这么多参数,还不知道每次到底是几个,该怎么写?在别的编程语言里也许很头痛,对于Lua,用一个省略号(…)代替就可以了。

施工

好,接下来进入实战阶段,我们来做一个打断施法成功才喊话的小插件。以前,你也许是用这样一个宏:

#showtooltip
/cast 脚踢
/stopmacro [noexists][noharm] /y 我已脚踢<<%t>>,下一个接上

这个宏最大的问题莫过于就算你没脚踢上也会喊话,给人一种你很傻X的错觉。我们现在就用高端大气上档次的手法来终结这个状况。

现在,我们先理清思路,这个到底该怎么做:

需要监视的事件:施法被打断
是谁打断施法:玩家
玩家用了什么技能打断:比如说脚踢

嗯,就这些。

好了,先处理第一个问题,怎么看出是施法被打断

COMBAT_LOT_EVENT(戳此去看英文版的事件文档)可以返回25个参数,其中前11个参数是永远都一样的:时间戳、类型、(第三个别管了)、施法来源GUID(这个也别管了)、施法来源的名字、施法来源的标记(flags)、施法来源的团队标记(raidflags)(这两个标记也别管了)、施法目标的GUID、施法目标的名字、施法目标的标记、施法目标的团队标记。

我们对于这11个参数只需要管“类型”、“施法来源的名字”。

接下来,去那篇英文文档找一下对应的类型:SPELL_INTERRUPT(法术_打断)。由于战斗记录肯定只有打断成功的记录,所以乱踢是不会产生这个信息的。

之后,这是个法术(SPELL),所以会带上3个参数:法术id、法术名称、法术派别(物理啊奥术啊什么的)。判断id还是名字你可以随意。

再之后,这是SPELL_INTERRUPT事件,后面还会有3个额外的参数:被打断的法术id、被打断的法术名称、被打断的法术派别,你可以用也可以不用……

好的,现在我们有17个参数了……按照我们的思路先来一段:

如果 战斗记录类型 是 打断法术 而且 施法来源的名字 是 你的名字 而且 法术名字 叫 脚踢 那么
喊话:我已打断 施法目标的名字 ,下一个接上
以上

逐字逐句翻译到Lua:

--如果 战斗记录类型 是 打断法术 而且 施法来源的名字 是 你的名字 而且 法术名字 叫 脚踢 那么 (UnitName是取得目标名字的函数,比如取得当前焦点的名字就是UnitName('focus'))
if eventType == 'SPELL_INTERRUPT' and sourceName == UnitName('player') and spellName == '脚踢' then
--喊话:我已打断 施法目标的名字 ,下一个接上
    SendChatMessage('我已打断 ' .. destName .. ' ,下一个接上', 'YELL')
--以上
end

下面我们来处理如何取得所需的这几个参数,稍微数一下,我们需要第2、5、9和第13个参数:

--下划线"_"代表我们不需要这个参数,我们从省略号...里取第2 5 9 和第13个
local _, eventType, _, _, sourceName, _, _, _, destName, _, _, _, spellName = ...

--注意,如果没有这么多参数,那么有些参数会取不回来变成“nil(空)”,如果你对nil做一些数学运算什么的,记住,会报错的……
--当然了,就我们这一个事件一定会有第13个参数,就没问题了,否则,你只能先取前11个这些肯定会有的,再根据事件类型判断取后面哪几个。

这时候,你突然发现,没错,我们好像已经把该做的都做了,现在我们组合一下:

local f = CreateFrame('Frame', nil, UIParent)
f:RegisterEvent('COMBAT_LOG_EVENT_UNFILTERED')
f:SetScript('OnEvent', eventHandler)

local function eventHandler(self, event, ...)
	local _, eventType, _, _, sourceName, _, _, _, destName, _, _, _, spellName = ...
	if eventType == 'SPELL_INTERRUPT' and sourceName == UnitName('player') and spellName == '脚踢' then
		SendChatMessage('我已脚踢 ' .. destName .. ' ,下一个接上', 'YELL')
	end
end

完工。保存成lua文件,根据addons文件夹里其它插件的格式,照葫芦画瓢建立个文件夹放进去,然后改一个toc文件放一起就能用了!或者,干脆找任意一个插件的lua文件,把这段粘贴进去即可。


后记


其实这段代码还有很多可优化的空间,比如,可以不判断是不是脚踢,反正只要是打断就可以了:

local f = CreateFrame('Frame', nil, UIParent)
f:RegisterEvent('COMBAT_LOG_EVENT_UNFILTERED')
f:SetScript('OnEvent', eventHandler)

local function eventHandler(self, event, ...)
	local _, eventType, _, _, sourceName, _, _, _, destName= ...
	if eventType == 'SPELL_INTERRUPT' and sourceName == UnitName('player') then
		SendChatMessage('我已打断 ' .. destName .. ' ,下一个接上', 'YELL')
	end
end

再比如像我刚才说的,非常狠毒地缩写一下然后写成个宏:

/run R=R or CreateFrame('Frame') R:RegisterEvent('COMBAT_LOG_EVENT') R:SetScript('OnEvent', function(_, _, _, e, _, _, _, _, _, _, d) if e == 'SPELL_INTERRUPT' then SendChatMessage('我已打断 ' .. d .. ' ,下一个接上', 'YELL') end end)

对于这个宏,直接用不带_UNFILTERED那个事件,连是不是玩家自己都省得判断了,前面R=R or CreateFrame则是防止你多按几次之后一次打断喊好几条……取消这个宏用/run R:UnregisterAllEvents()或者直接重载界面就可以了。

好了,你可能还有很多想问的东西,比如关于这个事件所有的细节。暂时我懒得写了,有什么问题直接提就好……

分享到: