.. SPDX-License-Identifier:	CC-BY-NC-SA-4.0

解析：事件栈
============

fk和神杀的一大区别就是拥有事件栈。下面对事件栈稍作解说并说明如何利用它让DIY更便捷。

何谓事件
--------

相信各位对 *触发时机* 这个概念不会陌生。拿伤害举例吧，就涉及 *造成伤害时* 、 *受到伤害后* 等等时机。那么 *伤害* 本身呢？换句话说， ``room:damage`` 里面发生了啥？

在神杀中，伤害就是直接执行一系列代码。Fk在这方面则是有区别的——它新建了一个伤害 *事件* ，然后执行这个事件（也就是正常的伤害流程）。

为什么要做一个事件机制呢？答案是为了更好的处理插结状况，以及实现老朱然（划掉

事件有以下几个特点：

- 能携带额外信息: 这一点就让代码在插结的情况下也能正确运行
- 能被随时中断而不影响游戏运行: 以胆为守！
- 即使被中断了也能清理现场: 后面会详细说说关于这个的注意事项

当然了，本文不会讨论事件机制的具体实现方法，而只会说明它在DIY能派上哪些用场。

初步观察事件栈
--------------

Fk提供了一个方便的函数： ``GameLogic:dumpEventStack()`` ，它能输出当前的事件栈。你可以直接在代码中调用它，或者在用dbg大法调试代码时现场调用它。

总之来举个例子，我去洛神里面加一行给大伙看看效果好了：

.. code:: lua

  on_use = function(self, event, target, player, data)
    local room = player.room
    room.logic:dumpEventStack() -- 打印此时事件栈
    room:obtainCard(player.id, data.card, false)
  end,

玩一下游戏，人机对我们使用了【杀】，我们掉血发动奸雄，此时可以看到这样的输出：（仅用作例子，实际上随着游戏的更新结果不一定相同）

::

  ===== Start of event stack dump =====

  Stack level #7: GameEvent.SkillEffect
  Stack level #6: GameEvent.Damage
  Stack level #5: GameEvent.SkillEffect
  Stack level #4: GameEvent.UseCard
  Stack level #3: GameEvent.Phase
  Stack level #2: GameEvent.Turn
  Stack level #1: GameEvent.Round

  ===== End of event stack dump =====

来看看吧。栈顶层的就是当前的事件——SkillEffect，即技能生效事件。此时生效的技能是奸雄。然后往上一层就是伤害事件了，再往上是【杀】的生效事件，再往上是使用卡牌【杀】的事件，再往上是执行阶段事件（这里执行出牌阶段），再往上是执行回合，最底层的是执行轮次。

是不是很清晰的列出了所有插结情况呢？顺便一提，dumpEventStack还接收一个布尔型参数。如果你传一个true进去的话，能看到更加详细的输出，不过可能详细过头就是了。这种简略版输出是默认情况，刚好也便于说明。

如何调查事件栈
--------------

Fk提供了一系列用来调查事件栈的函数。

首先是获取当前的游戏事件—— ``GameLogic:getCurrentEvent()`` 。这个函数会把事件栈栈顶的事件返回。

然后的东西就是根据事件来找比他更早的事件了，或者说找自己的父事件。

我们可以用 ``event.parent`` 获取这个游戏事件event的父事件。也就是刚好比他早一点的事件，在栈中处于它下层紧挨着的事件。比如前面的栈中，Damage事件的父事件是SkillEffect事件。

由于父事件也是事件，所以它也有parent属性，也就是说可以写出诸如 ``event.parent.parent...`` 的代码。当然了这样写不好看，也可能出bug，所以有一个方便的函数—— ``GameEvent:findParent(eventType)`` 。该函数像遍历链表一般的找到比这个事件更早的、类型符合参数指定的事件。比如 ``event:findParent(GameEvent.Turn)`` 就返回距离此事件最近的回合事件。

如何给事件附加数据
------------------

事件虽是类的实例，但本质上是一张表。所以直接用 ``event.xxx = xxx`` 就能给他附加数据了。

获得数据也是一样的简单，用 ``event.xxx`` 就行了。

这样做的最大好处就是解决了插结问题。下面举个插结的例子以便大家能更加了解。

没有！

如何终止事件
------------

用 ``GameEvent:shutdown()`` 。这个函数能终止一切结算并结束这个事件。如果该事件不是栈顶的事件的话，那么会先逐一终止比他更晚发生的所有事件，然后再终止这个事件。

GameLogic里面也提供了方便的函数。 ``GameLogic:breakEvent()`` 可以终止当前事件。 ``GameLogic:breakTurn()`` 是个专为老朱然封装的函数，能终止一切结算并结束本回合。

还有个可能也是非常常用的：只要发生了任何错误，那么当前事件也会被终止。比如你不小心操作了本不该为nil的对象，或者直接手动调用error函数，或者发生assert failed等等报错后。这种情况下，事件终止更像是一种错误处理方式，而不是你真的想终止这个事件了。

.. note::

   在fk比较早期的时候，还没有事件机制一说。因此只要发生任何错误，都会整局游戏立刻停止（因为Lua报错导致自己停止运行了）。这是不是很糟糕呢？不小心写了个bug，结果整个游戏都卡住不动了。而有了事件机制后，bug只会影响当前事件而不至于威胁到整个游戏的进行。这也为拓展开发带来了更大的容错率啊。

关于事件的灾后清理
------------------

在事件意外终止后，有一些事情也是要做的，并不是真正的直接终止结算走人了。比如在使用牌事件终止后，被使用的牌会从处理区进入弃牌堆，以及诸如此类的。

大家在制作DIY的时候应该会经常用到标记保存技能的相关数据。为了让事件即使被中断也能清理好这些标记，就要去熟悉这些事件终止时是如何执行清理的。

.. hint::

   这里提一下refresh only。所谓refresh only的触发方式，就是只会执行触发技的can_refresh和on_refresh，而不去管can_trigger之类的。结合refresh的定位，这是不是用来清理各种标记的绝好时机呢？
   下面的各种触发，如无特殊说明，都是refresh only的。

首先是阶段被终止后：

- 清除所有玩家本阶段的卡牌/技能使用记录。
- 清除所有玩家所有以-phase结尾的标记。
- 触发EventPhaseEnd时机。

然后是回合被终止后：

- 清除所有玩家本回合卡牌/技能使用记录以及-turn结尾的标记
- 触发“结束阶段开始时”
- 触发“结束阶段结束时”
- 触发EventPhaseChanging：从结束阶段到NotActive
- 触发“NotActive开始时”
- 触发“回合结束时”

以上就是常见的要注意的点，建议在编写用于清理标记的on_refresh时也考虑一下。
