本文旨在向你介绍法术施放(spellcast)的概念。每个脚本编写者都有必要大致了解其运作方式,因为法术是游戏中物体之间主要的"交互渠道"之一。无论你是作为一名潜行者偷窃别人的口袋、与另一名玩家发起交易、使用一件物品,还是单纯地用魔法"烧"别人的屁股——都涉及法术。每个法术都通过它在 spell.dbc 中的 spellId 条目来标识。SpellInfo 类是该表(以及其他相关的 dbc 和 sql 表)的 C++ 接口。施放过程的大部分处理都在 Spell 类中进行,该类代表了每次施放的动态状态。该类的行为可以通过 SpellScript 类对象来影响。这样一个对一切都有巨大影响的机制非常复杂,因此为了简化我们对所发生事情的理解,让我们把它分成几个阶段。在游戏中,它们的发生顺序与本文中的顺序一致。
这个阶段负责处理来自施法者(发起施放的对象)的"请求"。请求可以来自玩家客户端(见 SpellHandler.cpp),也可以来自游戏物体本身(GameObject::CastSpell、Unit::CastSpell 等)。在这里,worldserver 会验证并准备法术所指向的显式目标(explicit target)。
简单来说,显式法术目标就是受法术影响的目标。例如,战士的英勇打击(Heroic Strike)的显式法术目标就是当前选中的单位,无论它是玩家还是非玩家角色。
服务端的显式目标由 Unit::CastSpell 函数的第一个参数指定。
然而,并非所有法术都需要显式目标!例如范围效果(area of effect)法术,无论你在游戏中选中什么,周围的物体都会受到影响。因此,显式法术目标可以为null,这是很正常的。
在许多其他法术的情况下,显式目标也可以是地面上的某个位置,法师的暴风雪(blizzard)法术就是这种情况。
正如你可能在 Spell system 中读到的,数据库决定了法术的大部分行为,这里也不例外。Spell.dbc 中的 target、implicitTargetA、implicitTargetB 字段决定了这一点。简而言之——target 字段中的标记,或将 implicitTargetX 设置为 TARGET_XXX_TARGET(例如 TARGET_UNIT_TARGET)之类的目标模式,就意味着法术需要一个显式目标。这最重要的结果是:你不能随意把任何法术施放在任何东西上,一切都是由客户端数据库精确决定的,你唯一能做的就是遵守这些规则。除了显式法术目标的验证和准备之外,还会对施放请求的正确性进行一些预检查——会调用 Spell::CheckCast 函数(它使用数据库数据进行检查),此外在核心中也有散落各处的施放请求函数中进行一些检查。如果失败,会发送错误信息;否则法术会被排队等待在下一个物体更新时执行,或者在触发法术(triggered spell)的情况下立即处理。触发是一种"系统"施放,它究竟意味着什么并没有精确定义,因为这个术语被过度使用而失去了特定含义,但设置触发施放标记会极大地改变法术的行为和数据库数据的解释方式。例如,以触发标记施放的法术(CastSpell() 的一个参数)会忽略 dbc 中设置的道具需求、某些施法者状态需求等。法术成功准备后就开始施放,你会在游戏中看到施法条。你可以通过搜索核心中 Spell::Prepare 函数的出现位置来追踪这个阶段的代码。
一旦施法条满了(对于瞬发法术则是立即),施放阶段就开始了。在这个阶段会做几件事,其中最重要的是:法术隐式目标(implicit target)的选择。法术隐式目标是指会受到你法术影响的游戏内物体。它们的选择基于 Spell.dbc 的 implicitTargetX 列、conditions 表以及 SpellScript 的 OnObject(Area)TargetSelect 钩子。这个阶段不会对目标做任何操作(请在使用脚本时不要破坏这条规则——这是不符合官方(blizzlike)行为的),它们只是被选中供后续处理。在这个阶段的开始还会做另一件重要的事:第二次调用 Spell::CheckCast(你可以通过使用 OnCheckCast 法术脚本钩子和 conditions 数据库表来影响它)。此外,这个阶段还会从施法者身上扣除所需的道具和能量(power)。你可以使用 BeforeCast、OnCast、AfterCast 钩子来介入这个阶段。整个阶段在 Spell::cast 中处理。
发射在施放完成后立即进行,它是法术"离开施法者之手"的时刻。你可以注意到某些法术会飞行一段时间后才到达目标。一些法术效果在这个阶段处理,例如:来自施法者的伤害必须在法术发射的那一刻计算,而不是命中那一刻;冲锋必须在发射时开始,等等。当(且如果)你的法术到达目标时(对于没有延迟的法术则是立即),下一个(命中)阶段就开始了。发射在 Spell::HandleLaunchPhase 中处理。你可以使用 OnEffectLaunch 和 OnEffectLaunchTarget 钩子来影响这个阶段中法术效果的执行。两者的区别在于:OnEffectLaunch 对法术的每个效果各调用一次,不论其目标如何;而 OnEffectLaunchTarget 则对法术的每个可用隐式目标(在前面的施放阶段中选定的)各调用一次。
命中是处理法术大部分效果的阶段。法术效果是对施法者或法术隐式目标产生作用的小操作。每个法术条目都有 EffectX 列(在 dbc 中),其中包含法术对目标施加的效果 ID。每个效果都有自己独立的目标列表,因此一个法术可以同时作用于许多不同的物体。例如,一个法术可以同时对盟友施加治疗效果,并给自己施加一个增益光环(aura)。与发射阶段类似,你可以在这个阶段通过 OnEffectHit 和 OnEffectHitTarget SpellScript 钩子来覆盖法术效果的行为。你还可以使用 BeforeHit、OnHit 和 AfterHit 钩子。这个阶段的一个重要事情是法术触发(proc)——一种特殊的事件触发系统,它会根据施法者和法术隐式目标上施加的光环来做相应的事情。proc 系统的某些部分本应在其他法术施放阶段中由 proc 系统处理,但目前尚未实现。
结束阶段在所有法术目标都被处理完毕后发生,它只是一个清理过程,这个阶段只发生少数相关事件。