Rewrite event (#60)

* doc for gameevent

* init game event

* correct other yield call

* hp event

* dying event

* move card event

* use/response event

* judge event

* remove space
This commit is contained in:
notify 2023-03-01 01:43:44 +08:00 committed by GitHub
parent b0cc1afa02
commit 3f077a6d69
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1024 additions and 634 deletions

173
doc/dev/gameevent.md Normal file
View File

@ -0,0 +1,173 @@
# Fk的游戏事件
___
在Fk中“事件”指的大约是像`room:judge`, `room:damage`之类的操作。这些操作一般和某个游戏术语挂钩(“判定”、“伤害”),然后其中包含着一系列操作,比如伤害事件包含了与伤害事件有关的各种触发时机、以及扣减等实际的动作等。
之所以要把事件单独挑出来聊聊,是因为有以下几点需求:
* 事件要能够被半路中止。
* 对于被中止的事件,要能判断它能不能中止事件栈中的更低层事件。
* 对于被中止的事件,需要做“垃圾回收”(例如将处于处理区的相关卡牌移动到弃牌堆等等)
___
## 对于如何实现的构想
(施工完成后再来修改这一节)
首先是如何实现事件类。初步构想一下,应该有以下的属性/方法:
* 事件名(也可以是枚举值)
* 事件数据(一个表,内含所有要用到的数据)
* 事件id在一局游戏中唯一标记某个事件
* 事件的函数体,也就是具体要做的事情
* 事件被中止或者正常结束时,用来清理现场的函数
问题来了,既然事件本质上还是个函数体,那要怎么才能中止呢?
比较容易想到的是利用协程。协程差不多就是一种特殊的函数调用方式但是在协程体里面可以调用yield函数立刻返回到resume调用者然后还可能可以继续调用如果“中止事件”这一事件也被中止的话之类的奇怪情况总之可以纳入实现“事件”这种复杂概念的考虑。
事件必然会发生嵌套。所以对此更要慎重考虑。
现在只是设想!假设以`room:judge`入手:
```lua
function Room:judge(judgeStruct)
local judgeEvent = GameEvent:new(GameEvent.Judge, judgeStruct)
judgeEvent:exec()
end
```
总之可能是这样的吧。exec()就是实际执行事件,可能如下:
```lua
function GameEvent:exec()
local event_f = self.event_f
local co = coroutine.create(event_f)
while true do
local yield_result = coroutine.resume(co)
if yield_result == "__handleRequest" then
-- 正常yield掉如此层层yield最后到Room的主循环然后进requestLoop协程处理事务
-- 最后返回这里继续resume
coroutine.yield(yield_result)
else
-- 事件被中止,考虑做点什么
break
end
end
end
```
现在考虑嵌套的情况event1和event2嵌套也就是event1的体里面又创建了event2并exec此时的协程调用关系如下
RoomLogic -> event1 -> event2 | RequestLoop
此时event2中调用了某个耗时的函数比如room:delay或者各种request这时候就触发了yield。然后在上面的函数中就获取了yield返回值然后判断是正常yield后就进一步yield此时协程到了event1中。event1继续yield于是到了Room主协程主协程其实也调用了exec所以被yield切回到真正的主线程然后执行requestloop的协程。乍一看似乎没问题除了这个跳跃链有够长的。
这种开销看起来应该不大吧而且在AI Random Play这种非常需要性能的场所也根本不会发生这种类型的yield画饼总之先不考虑这块。
如果事件被中止按目前的实现来说就是在特定的时机return true了那么事件的本体也应该调用yield这就如同在目前--v0.0.1实现中那些函数常见的遇到true就直接返回了一样这样相当于从函数返回了。这种yield就会进入那个else分支进行这个事件类的有关清扫工作。
___
## 构想2 类似无懈可击的事件
考虑一类特殊的事件:“取消其他事件的事件”。它和普通事件一样,能被中止之类的,而它的作用在于取消掉其他事件。
接着上面的else分支继续考虑
```lua
else
local cancelEvent = GameEvent:new(GameEvent.CancelEvent, self)
local ret = cancelEvent:exec()
if ret then break end
```
似乎也没什么非常特殊的内容啊。
exec()的返回值哪里来?这好像真的是个问题呢。可以考虑返回布尔值表示事件是否中止了?或者更详细的,返回一个状态码,毕竟本质上身为协程自然能有协程该有的种种状态。这里只是初步考虑而已,就考虑前者好了。
___
## 落实 - 手杀皇甫嵩
手杀皇甫嵩是重构整个事件体系的罪魁祸首。其技能为若blah blah你可以终止本次判定然后blah blah。
而终止本次判定是目前的体系做不到的。
考虑如下技能片段:
```lua
on_effect = function(xxx)
local judge = {}
room:judge(judge)
if judge.card.number > 5 then xxx end
end
```
皇甫嵩能终止判定就算他在fk.Judge时机返回true算了。前文已经考虑过judge了他创建了新event并执行之。而今judge事件遭到打断room:judge可能可以返回一个返回值来告诉玩家已经被中断之类的。但是Luaer特别是像我这样的Luaer懒得考虑事件的合法性之类的而既然judge已经被终止那么judge.card就不应该被使用才行。
为此可以为judge表添加__index元方法当对key="card"进行取值时就直接yield掉除此之外的就rawget。
还有更复杂的情况呢。当皇甫嵩判乐的时候如果是黑桃那么他发动技能终止了判定然后像个无事人一样出牌呢。乐都还贴在他头上。考察一下Fk里面的乐是怎么写的原来是on_effect的末尾才移走啊那没事了。也就是说如果对judge.card的非法访问使得事件被中止了那么照这个逻辑乐是下不来的符合手刹了这下。
___
## 考虑 事件为何中止?
事件是协程,因此协程中止的方法就是事件中止的方法。有这两种:
* yield, 落实到Fk就是触发技的各种返回true
* error, 这不就是我经常发生的事情吗
前面也提到过发生yield的时候会有cancelEvent产生方便玩家反悔中止这次事件但因为error而中断事件是无法恢复的。试图resume一个报错的协程的话他会立刻因为error而自动yield。这个可以在exec函数里面多加考虑如果resume函数返回了true和特定值那就是正常情况。否则就是报错输出错误信息并返回。
那前文那个judge.card怎么办呢这种严格来说得算在error的范畴因为不是人为中止本次effect的。但是error的话势必要输出到屏幕而我个人聚德直接拿judge.card算是合法行为。这种情况或许可以定一个约定好的特殊错误信息在处理错误的时候如果是这个错误的话就不输出。
___
## 考虑 有哪些事件
在最开始的时候“依赖关系”这个现象的存在使得触发技多了个on_cost消耗但是现在on_cost已经成为界定skill是否发动了的标准。而在skill的effect环节依然存在着一环扣一环的关系比如前面举的room:judge例子。
假设Room.lua里面返回void的都算事件好了或者再细一点在函数体里面用了logic:trigger的void函数是事件算了这个也不好定义反正公道自在人心。但毫无疑问最为复杂的两个事件就是——使用牌和移动牌。
真是令人头大啊这俩可不是好惹的。不过看到它们可能从room.lua分家出去我其实还是有一丝欣慰
总之事件不止room.lua里面那些。就拿前面的考虑来说由于要中断on_effect所以on_effect肯定会算成一个事件可能叫SkillEffect事件吧。
再考虑万恶之源武将——老朱然,直接结束你的回合。(他只要回合内造成了伤害就能结束回合,但没说在谁的回合造成了伤害)所以进行回合也理应算是个事件。
___
## 考虑 老朱然
对于老朱然这种人而言,他想要杀掉的是回合事件,而能发动这个技能的时候,事件栈想必已经很深了,稍微模拟一下这个情景:老朱然杀界徐盛并打掉他一滴血,此时事件栈大概如下(还没正式设计各种事件,所以可能不妥):
* 伤害事件 - room:damage - 询问技能:是否发动胆守,点确定
* 技能生效事件 - activeskill:onEffect - 【杀】的effect
* 使用牌事件 - room:useCard - 出杀
* 进行阶段事件 - ? - 在出牌阶段
* 回合事件 - ? - 在回合
我们的限制条件无法获得room:damage的返回值或者说根本没想去获得其他同理。
coroutine.yield的功能也只有挂起协程并让相应的resume调用返回而已那么该怎么办呢由于以上种种限制的存在主要还是想把Luaer惯着我们不能对杀的onEffect下手其他函数都是核心函数改改也无妨咯。
还是结合情景考虑吧。胆守点了确定此时最直接的感受应该是return true。但是return true的意思是防止伤害都已经是“造成伤害后”了怎么防止哦return true也不会有人管你的所以这里要另辟蹊径。考虑直接yield此时会处于DamageEvent的exec()中也就是处于room:damage中他在处理中止信息。正常的中止的话会使用break跳出循环那么如果我访问调用栈直接让他一路yield到我们想要的那个事件如同yield到requestLoop那样呢
没错访问事件栈确实是个解决办法的可能方案。这时候用id指示事件的重要性就出来了可以传一个id表示事件不过话说回来传那个事件本身也没有任何关系就是了咯如果yield函数返回了一个GameEvent类的实例那么就在处理环节将其和self进行比较如果不同就继续yield直到退到相应的事件中。
这种跨越很多级事件的东西怎么取消呢?差不多得了,懒得考虑了,天天防止这防止那,随便逮到个东西就想把他防止掉/取消掉,三国杀的游戏逻辑就是被你们这群人毁掉的
总之这不考虑如何防止这种直接结束回合了毕竟这种不断yield的方式无法用事件进行描述。
___
## 考虑 内存泄漏的应对
首先声明Lua没有内存泄漏。但是如果有些东西用户不想要但是又不告诉lua的话lua就会觉得用户想要然后一直保存着它这在某种意义上也相当于内存泄漏了。拿实例来说如果事件被中止了那么在很多情况下确实就不需要了但Lua会认为协程是挂起的用户可能想要恢复于是一直保存着。
当然了对于这情况Lua提供了coroutine.close用来关掉一个协程。不过我们想要让这个事件彻底删掉该怎么办呢
方法很简单将它出栈不就行了。照这么说的话事件在exec开始的时候就入栈然后等待exec结束就出栈但对于老朱然这种人他把函数直接yield掉了因此有必要手动出栈。

View File

@ -0,0 +1,59 @@
GameEvent.functions[GameEvent.Dying] = function(self)
local dyingStruct = table.unpack(self.data)
local self = self.room
local dyingPlayer = self:getPlayerById(dyingStruct.who)
dyingPlayer.dying = true
self:broadcastProperty(dyingPlayer, "dying")
self:sendLog{
type = "#EnterDying",
from = dyingPlayer.id,
}
self.logic:trigger(fk.EnterDying, dyingPlayer, dyingStruct)
if dyingPlayer.hp < 1 then
self.logic:trigger(fk.Dying, dyingPlayer, dyingStruct)
self.logic:trigger(fk.AskForPeaches, dyingPlayer, dyingStruct)
self.logic:trigger(fk.AskForPeachesDone, dyingPlayer, dyingStruct)
end
if not dyingPlayer.dead then
dyingPlayer.dying = false
self:broadcastProperty(dyingPlayer, "dying")
end
self.logic:trigger(fk.AfterDying, dyingPlayer, dyingStruct)
end
GameEvent.functions[GameEvent.Death] = function(self)
local deathStruct = table.unpack(self.data)
local self = self.room
local victim = self:getPlayerById(deathStruct.who)
victim.dead = true
table.removeOne(self.alive_players, victim)
local logic = self.logic
logic:trigger(fk.BeforeGameOverJudge, victim, deathStruct)
local killer = deathStruct.damage and deathStruct.damage.from or nil
if killer then
self:sendLog{
type = "#KillPlayer",
to = {killer.id},
from = victim.id,
arg = victim.role,
}
else
self:sendLog{
type = "#KillPlayerWithNoKiller",
from = victim.id,
arg = victim.role,
}
end
self:sendLogEvent("Death", {to = victim.id})
self:broadcastProperty(victim, "role")
self:broadcastProperty(victim, "dead")
logic:trigger(fk.GameOverJudge, victim, deathStruct)
logic:trigger(fk.Death, victim, deathStruct)
logic:trigger(fk.BuryVictim, victim, deathStruct)
end

210
lua/server/events/hp.lua Normal file
View File

@ -0,0 +1,210 @@
GameEvent.functions[GameEvent.ChangeHp] = function(self)
local player, num, reason, skillName, damageStruct = table.unpack(self.data)
local self = self.room
if num == 0 then
return false
end
assert(reason == nil or table.contains({ "loseHp", "damage", "recover" }, reason))
---@type HpChangedData
local data = {
num = num,
reason = reason,
skillName = skillName,
}
if self.logic:trigger(fk.BeforeHpChanged, player, data) then
self.logic:breakEvent(false)
end
assert(not (data.reason == "recover" and data.num < 0))
player.hp = math.min(player.hp + data.num, player.maxHp)
self:broadcastProperty(player, "hp")
if reason == "damage" then
local damage_nature_table = {
[fk.NormalDamage] = "normal_damage",
[fk.FireDamage] = "fire_damage",
[fk.ThunderDamage] = "thunder_damage",
}
if damageStruct.from then
self:sendLog{
type = "#Damage",
to = {damageStruct.from.id},
from = player.id,
arg = 0 - num,
arg2 = damage_nature_table[damageStruct.damageType],
}
else
self:sendLog{
type = "#DamageWithNoFrom",
from = player.id,
arg = 0 - num,
arg2 = damage_nature_table[damageStruct.damageType],
}
end
self:sendLogEvent("Damage", {
to = player.id,
damageType = damage_nature_table[damageStruct.damageType],
damageNum = damageStruct.damage,
})
elseif reason == "loseHp" then
self:sendLog{
type = "#LoseHP",
from = player.id,
arg = 0 - num,
}
self:sendLogEvent("LoseHP", {})
elseif reason == "recover" then
self:sendLog{
type = "#HealHP",
from = player.id,
arg = num,
}
end
self:sendLog{
type = "#ShowHPAndMaxHP",
from = player.id,
arg = player.hp,
arg2 = player.maxHp,
}
self.logic:trigger(fk.HpChanged, player, data)
if player.hp < 1 then
if num < 0 then
---@type DyingStruct
local dyingStruct = {
who = player.id,
damage = damageStruct,
}
self:enterDying(dyingStruct)
end
elseif player.dying then
player.dying = false
end
return true
end
GameEvent.functions[GameEvent.Damage] = function(self)
local damageStruct = table.unpack(self.data)
local self = self.room
if damageStruct.damage < 1 then
return false
end
damageStruct.damageType = damageStruct.damageType or fk.NormalDamage
if damageStruct.from and not damageStruct.from:isAlive() then
damageStruct.from = nil
end
assert(damageStruct.to:isInstanceOf(ServerPlayer))
local stages = {
{fk.PreDamage, damageStruct.from},
{fk.DamageCaused, damageStruct.from},
{fk.DamageInflicted, damageStruct.to},
}
for _, struct in ipairs(stages) do
local event, player = table.unpack(struct)
if self.logic:trigger(event, player, damageStruct) or damageStruct.damage < 1 then
self.logic:breakEvent(false)
end
assert(damageStruct.to:isInstanceOf(ServerPlayer))
end
if not damageStruct.to:isAlive() then
return false
end
if not self:changeHp(damageStruct.to, -damageStruct.damage, "damage", damageStruct.skillName, damageStruct) then
self.logic:breakEvent(false)
end
stages = {
{fk.Damage, damageStruct.from},
{fk.Damaged, damageStruct.to},
{fk.DamageFinished, damageStruct.from},
}
for _, struct in ipairs(stages) do
local event, player = table.unpack(struct)
self.logic:trigger(event, player, damageStruct)
end
return true
end
GameEvent.functions[GameEvent.LoseHp] = function(self)
local player, num, skillName = table.unpack(self.data)
local self = self.room
if num == nil then
num = 1
elseif num < 1 then
return false
end
---@type HpLostData
local data = {
num = num,
skillName = skillName,
}
if self.logic:trigger(fk.PreHpLost, player, data) or data.num < 1 then
self.logic:breakEvent(false)
end
if not self:changeHp(player, -num, "loseHp", skillName) then
self.logic:breakEvent(false)
end
self.logic:trigger(fk.HpLost, player, data)
return true
end
GameEvent.functions[GameEvent.Recover] = function(self)
local recoverStruct = table.unpack(self.data)
local self = self.room
if recoverStruct.num < 1 then
return false
end
local who = recoverStruct.who
if self.logic:trigger(fk.PreHpRecover, who, recoverStruct) or recoverStruct.num < 1 then
self.logic:breakEvent(false)
end
if not self:changeHp(who, recoverStruct.num, "recover", recoverStruct.skillName) then
self.logic:breakEvent(false)
end
self.logic:trigger(fk.HpRecover, who, recoverStruct)
return true
end
GameEvent.functions[GameEvent.ChangeMaxHp] = function(self)
local player, num = table.unpack(self.data)
local self = self.room
if num == 0 then
return false
end
player.maxHp = math.max(player.maxHp + num, 0)
self:broadcastProperty(player, "maxHp")
local diff = player.hp - player.maxHp
if diff > 0 then
if not self:changeHp(player, -diff) then
player.hp = player.hp - diff
end
end
if player.maxHp == 0 then
self:killPlayer({ who = player.id })
end
self.logic:trigger(fk.MaxHpChanged, player, { num = num })
return true
end

View File

@ -0,0 +1,31 @@
-- Definitions of game events
GameEvent.ChangeHp = 1
GameEvent.Damage = 2
GameEvent.LoseHp = 3
GameEvent.Recover = 4
GameEvent.ChangeMaxHp = 5
dofile "lua/server/events/hp.lua"
GameEvent.Dying = 6
GameEvent.Death = 7
dofile "lua/server/events/death.lua"
GameEvent.MoveCards = 8
dofile "lua/server/events/movecard.lua"
GameEvent.UseCard = 9
GameEvent.RespondCard = 10
dofile "lua/server/events/usecard.lua"
GameEvent.SkillEffect = 11
-- GameEvent.AddSkill = 12
-- GameEvent.LoseSkill = 13
dofile "lua/server/events/skill.lua"
GameEvent.Judge = 14
dofile "lua/server/events/judge.lua"
-- TODO: fix this
GameEvent.BreakEvent = 999

View File

@ -0,0 +1,62 @@
GameEvent.functions[GameEvent.Judge] = function(self)
local data = table.unpack(self.data)
local self = self.room
local who = data.who
self.logic:trigger(fk.StartJudge, who, data)
data.card = Fk:getCardById(self:getNCards(1)[1])
if data.reason ~= "" then
self:sendLog{
type = "#StartJudgeReason",
from = who.id,
arg = data.reason,
}
end
self:sendLog{
type = "#InitialJudge",
from = who.id,
card = {data.card.id},
}
self:moveCardTo(data.card, Card.Processing, nil, fk.ReasonPrey)
self.logic:trigger(fk.AskForRetrial, who, data)
self.logic:trigger(fk.FinishRetrial, who, data)
Fk:filterCard(data.card.id, who, data)
self:sendLog{
type = "#JudgeResult",
from = who.id,
card = {data.card.id},
}
if data.pattern then
self:delay(400);
self:setCardEmotion(data.card.id, data.card:matchPattern(data.pattern) and "judgegood" or "judgebad")
self:delay(900);
end
if self.logic:trigger(fk.FinishJudge, who, data) then
self.logic:breakEvent()
end
if self:getCardArea(data.card.id) == Card.Processing then
self:moveCardTo(data.card, Card.DiscardPile, nil, fk.ReasonPutIntoDiscardPile)
end
end
GameEvent.cleaners[GameEvent.Judge] = function(self)
local data = table.unpack(self.data)
local self = self.room
if self:getCardArea(data.card.id) == Card.Processing then
self:moveCardTo(data.card, Card.DiscardPile, nil, fk.ReasonPutIntoDiscardPile)
end
-- prohibit access to judge.card
setmetatable(data, {
__index = function(self, key)
if key == "card" then
error("__manuallyBreak")
end
return rawget(self, key)
end
})
end

View File

@ -0,0 +1,117 @@
GameEvent.functions[GameEvent.MoveCards] = function(self)
local args = self.data
local self = self.room
---@type CardsMoveStruct[]
local cardsMoveStructs = {}
local infoCheck = function(info)
assert(table.contains({ Card.PlayerHand, Card.PlayerEquip, Card.PlayerJudge, Card.PlayerSpecial, Card.Processing, Card.DrawPile, Card.DiscardPile, Card.Void }, info.toArea))
assert(info.toArea ~= Card.PlayerSpecial or type(info.specialName) == "string")
assert(type(info.moveReason) == "number")
end
for _, cardsMoveInfo in ipairs(args) do
if #cardsMoveInfo.ids > 0 then
infoCheck(cardsMoveInfo)
---@type MoveInfo[]
local infos = {}
for _, id in ipairs(cardsMoveInfo.ids) do
table.insert(infos, {
cardId = id,
fromArea = self:getCardArea(id),
fromSpecialName = cardsMoveInfo.from and self:getPlayerById(cardsMoveInfo.from):getPileNameOfId(id),
})
end
---@type CardsMoveStruct
local cardsMoveStruct = {
moveInfo = infos,
from = cardsMoveInfo.from,
to = cardsMoveInfo.to,
toArea = cardsMoveInfo.toArea,
moveReason = cardsMoveInfo.moveReason,
proposer = cardsMoveInfo.proposer,
skillName = cardsMoveInfo.skillName,
moveVisible = cardsMoveInfo.moveVisible,
specialName = cardsMoveInfo.specialName,
specialVisible = cardsMoveInfo.specialVisible,
}
table.insert(cardsMoveStructs, cardsMoveStruct)
end
end
if #cardsMoveStructs < 1 then
return false
end
if self.logic:trigger(fk.BeforeCardsMove, nil, cardsMoveStructs) then
self.logic:breakEvent(false)
end
self:notifyMoveCards(nil, cardsMoveStructs)
for _, data in ipairs(cardsMoveStructs) do
if #data.moveInfo > 0 then
infoCheck(data)
---@param info MoveInfo
for _, info in ipairs(data.moveInfo) do
local realFromArea = self:getCardArea(info.cardId)
local playerAreas = { Player.Hand, Player.Equip, Player.Judge, Player.Special }
if table.contains(playerAreas, realFromArea) and data.from then
self:getPlayerById(data.from):removeCards(realFromArea, { info.cardId }, info.fromSpecialName)
elseif realFromArea ~= Card.Unknown then
local fromAreaIds = {}
if realFromArea == Card.Processing then
fromAreaIds = self.processing_area
elseif realFromArea == Card.DrawPile then
fromAreaIds = self.draw_pile
elseif realFromArea == Card.DiscardPile then
fromAreaIds = self.discard_pile
elseif realFromArea == Card.Void then
fromAreaIds = self.void
end
table.removeOne(fromAreaIds, info.cardId)
end
if table.contains(playerAreas, data.toArea) and data.to then
self:getPlayerById(data.to):addCards(data.toArea, { info.cardId }, data.specialName)
else
local toAreaIds = {}
if data.toArea == Card.Processing then
toAreaIds = self.processing_area
elseif data.toArea == Card.DrawPile then
toAreaIds = self.draw_pile
elseif data.toArea == Card.DiscardPile then
toAreaIds = self.discard_pile
elseif data.toArea == Card.Void then
toAreaIds = self.void
end
table.insert(toAreaIds, toAreaIds == Card.DrawPile and 1 or #toAreaIds + 1, info.cardId)
end
self:setCardArea(info.cardId, data.toArea, data.to)
Fk:filterCard(info.cardId, self:getPlayerById(data.to))
local currentCard = Fk:getCardById(info.cardId)
if
data.toArea == Player.Equip and
currentCard.type == Card.TypeEquip and
data.to ~= nil and
self:getPlayerById(data.to):isAlive() and
currentCard.equip_skill
then
currentCard:onInstall(self, self:getPlayerById(data.to))
elseif realFromArea == Player.Equip and currentCard.type == Card.TypeEquip and data.from ~= nil and currentCard.equip_skill then
currentCard:onUninstall(self, self:getPlayerById(data.from))
end
end
end
end
self.logic:trigger(fk.AfterCardsMove, nil, cardsMoveStructs)
return true
end

View File

@ -0,0 +1,4 @@
GameEvent.functions[GameEvent.SkillEffect] = function(self)
local effect_cb = table.unpack(self.data)
return effect_cb()
end

View File

@ -0,0 +1,253 @@
local playCardEmotionAndSound = function(room, player, card)
if card.type ~= Card.TypeEquip then
room:setEmotion(player, "./packages/" ..
card.package.extensionName .. "/image/anim/" .. card.name)
end
local soundName
if card.type == Card.TypeEquip then
local subTypeStr
if card.sub_type == Card.SubtypeDefensiveRide or card.sub_type == Card.SubtypeOffensiveRide then
subTypeStr = "horse"
elseif card.sub_type == Card.SubtypeWeapon then
subTypeStr = "weapon"
else
subTypeStr = "armor"
end
soundName = "./audio/card/common/" .. subTypeStr
else
soundName = "./packages/" .. card.package.extensionName .. "/audio/card/"
.. (player.gender == General.Male and "male/" or "female/") .. card.name
end
room:broadcastPlaySound(soundName)
end
---@param room Room
---@param cardUseEvent CardUseStruct
local sendCardEmotionAndLog = function(room, cardUseEvent)
local from = cardUseEvent.from
local _card = cardUseEvent.card
-- when this function is called, card is already in PlaceTable and no filter skill is applied.
-- So filter this card manually here to get 'real' use.card
local card = _card
if not _card:isVirtual() then
local temp = { card = _card }
Fk:filterCard(_card.id, room:getPlayerById(from), temp)
card = temp.card
end
playCardEmotionAndSound(room, room:getPlayerById(from), card)
room:doAnimate("Indicate", {
from = from,
to = cardUseEvent.tos or {},
})
local useCardIds = card:isVirtual() and card.subcards or { card.id }
if cardUseEvent.tos and #cardUseEvent.tos > 0 then
local to = {}
for _, t in ipairs(cardUseEvent.tos) do
table.insert(to, t[1])
end
if card:isVirtual() or (card ~= _card) then
if #useCardIds == 0 then
room:sendLog{
type = "#UseV0CardToTargets",
from = from,
to = to,
arg = card:toLogString(),
}
else
room:sendLog{
type = "#UseVCardToTargets",
from = from,
to = to,
card = useCardIds,
arg = card:toLogString(),
}
end
else
room:sendLog{
type = "#UseCardToTargets",
from = from,
to = to,
card = useCardIds
}
end
for _, t in ipairs(cardUseEvent.tos) do
if t[2] then
local temp = {table.unpack(t)}
table.remove(temp, 1)
room:sendLog{
type = "#CardUseCollaborator",
from = t[1],
to = temp,
arg = card.name,
}
end
end
elseif cardUseEvent.toCard then
if card:isVirtual() or (card ~= _card) then
if #useCardIds == 0 then
room:sendLog{
type = "#UseV0CardToCard",
from = from,
arg = cardUseEvent.toCard.name,
arg2 = card:toLogString(),
}
else
room:sendLog{
type = "#UseVCardToCard",
from = from,
card = useCardIds,
arg = cardUseEvent.toCard.name,
arg2 = card:toLogString(),
}
end
else
room:sendLog{
type = "#UseCardToCard",
from = from,
card = useCardIds,
arg = cardUseEvent.toCard.name,
}
end
else
if card:isVirtual() or (card ~= _card) then
if #useCardIds == 0 then
room:sendLog{
type = "#UseV0Card",
from = from,
arg = card:toLogString(),
}
else
room:sendLog{
type = "#UseVCard",
from = from,
card = useCardIds,
arg = card:toLogString(),
}
end
else
room:sendLog{
type = "#UseCard",
from = from,
card = useCardIds,
}
end
end
end
---@param self GameEvent
GameEvent.functions[GameEvent.UseCard] = function(self)
local cardUseEvent = table.unpack(self.data)
local self = self.room
local from = cardUseEvent.from
self:moveCards({
ids = self:getSubcardsByRule(cardUseEvent.card),
from = from,
toArea = Card.Processing,
moveReason = fk.ReasonUse,
})
if cardUseEvent.card.skill then
cardUseEvent.card.skill:onUse(self, cardUseEvent)
end
sendCardEmotionAndLog(self, cardUseEvent)
if self.logic:trigger(fk.PreCardUse, self:getPlayerById(cardUseEvent.from), cardUseEvent) then
self.logic:breakEvent()
end
if not cardUseEvent.extraUse then
self:getPlayerById(cardUseEvent.from):addCardUseHistory(cardUseEvent.card.trueName, 1)
end
if cardUseEvent.responseToEvent then
cardUseEvent.responseToEvent.cardsResponded = cardUseEvent.responseToEvent.cardsResponded or {}
table.insert(cardUseEvent.responseToEvent.cardsResponded, cardUseEvent.card)
end
for _, event in ipairs({ fk.AfterCardUseDeclared, fk.AfterCardTargetDeclared, fk.BeforeCardUseEffect, fk.CardUsing }) do
if not cardUseEvent.toCard and #TargetGroup:getRealTargets(cardUseEvent.tos) == 0 then
break
end
self.logic:trigger(event, self:getPlayerById(cardUseEvent.from), cardUseEvent)
if event == fk.CardUsing then
self:doCardUseEffect(cardUseEvent)
end
end
self.logic:trigger(fk.CardUseFinished, self:getPlayerById(cardUseEvent.from), cardUseEvent)
end
GameEvent.cleaners[GameEvent.UseCard] = function(self)
local cardUseEvent = table.unpack(self.data)
local self = self.room
local leftRealCardIds = self:getSubcardsByRule(cardUseEvent.card, { Card.Processing })
if #leftRealCardIds > 0 then
self:moveCards({
ids = leftRealCardIds,
toArea = Card.DiscardPile,
moveReason = fk.ReasonPutIntoDiscardPile,
})
end
end
GameEvent.functions[GameEvent.RespondCard] = function(self)
local cardResponseEvent = table.unpack(self.data)
local self = self.room
local from = cardResponseEvent.customFrom or cardResponseEvent.from
local card = cardResponseEvent.card
local cardIds = self:getSubcardsByRule(card)
if card:isVirtual() then
if #cardIds == 0 then
self:sendLog{
type = "#ResponsePlayV0Card",
from = from,
arg = card:toLogString(),
}
else
self:sendLog{
type = "#ResponsePlayVCard",
from = from,
card = cardIds,
arg = card:toLogString(),
}
end
else
self:sendLog{
type = "#ResponsePlayCard",
from = from,
card = cardIds,
}
end
self:moveCards({
ids = cardIds,
from = from,
toArea = Card.Processing,
moveReason = fk.ReasonResonpse,
})
playCardEmotionAndSound(self, self:getPlayerById(from), card)
for _, event in ipairs({ fk.PreCardRespond, fk.CardResponding, fk.CardRespondFinished }) do
self.logic:trigger(event, self:getPlayerById(cardResponseEvent.from), cardResponseEvent)
end
local realCardIds = self:getSubcardsByRule(cardResponseEvent.card, { Card.Processing })
if #realCardIds > 0 and not cardResponseEvent.skipDrop then
self:moveCards({
ids = realCardIds,
toArea = Card.DiscardPile,
moveReason = fk.ReasonPutIntoDiscardPile,
})
end
end

81
lua/server/gameevent.lua Normal file
View File

@ -0,0 +1,81 @@
---@class GameEvent: Object
---@field room Room
---@field event integer
---@field data any
local GameEvent = class("GameEvent")
GameEvent.functions = {}
GameEvent.cleaners = {}
local function wrapCoFunc(f, ...)
if not f then return nil end
local args = {...}
return function() return f(table.unpack(args)) end
end
local function dummyFunc() end
function GameEvent:initialize(event, ...)
self.room = RoomInstance
self.event = event
self.data = { ... }
self.main_func = wrapCoFunc(GameEvent.functions[event], self) or dummyFunc
self.clear_func = wrapCoFunc(GameEvent.cleaners[event], self) or dummyFunc
end
function GameEvent:exec()
local room = self.room
local logic = room.logic
local ret = false -- false or nil means this event is running normally
local extra_ret
logic.game_event_stack:push(self)
local co = coroutine.create(self.main_func)
while true do
local err, yield_result, extra_yield_result = coroutine.resume(co)
if err == false then
-- handle error, then break
if not string.find(yield_result, "__manuallyBreak") then
fk.qCritical(yield_result)
print(debug.traceback(co))
end
self.clear_func()
ret = true
break
end
if yield_result == "__handleRequest" then
-- yield to requestLoop
coroutine.yield(yield_result, extra_yield_result)
elseif type(yield_result) == "table" and yield_result.class
and yield_result:isInstanceOf(GameEvent) then
-- yield to corresponding GameEvent, first pop self from stack
self.clear_func()
logic.game_event_stack:pop(self)
-- then, call yield
coroutine.yield(yield_result)
elseif yield_result == "__breakEvent" then
-- try to break this event
local cancelEvent = GameEvent:new(GameEvent.BreakEvent, self)
local notcanceled = cancelEvent:exec()
if not notcanceled then
self.clear_func()
ret = true
extra_ret = extra_yield_result
break
end
else
-- normally exit, simply break the loop
extra_ret = yield_result
break
end
end
logic.game_event_stack:pop(self)
return ret, extra_ret
end
return GameEvent

View File

@ -4,6 +4,7 @@
---@field refresh_skill_table table<Event, TriggerSkill[]>
---@field skills string[]
---@field event_stack Stack
---@field game_event_stack Stack
---@field role_table string[][]
local GameLogic = class("GameLogic")
@ -13,6 +14,7 @@ function GameLogic:initialize(room)
self.refresh_skill_table = {}
self.skills = {} -- skillName[]
self.event_stack = Stack:new()
self.game_event_stack = Stack:new()
self.role_table = {
{ "lord" },
@ -275,4 +277,8 @@ function GameLogic:trigger(event, target, data)
return broken
end
function GameLogic:breakEvent(ret)
coroutine.yield("__breakEvent", false)
end
return GameLogic

View File

@ -18,6 +18,8 @@
local Room = class("Room")
-- load classes used by the game
GameEvent = require "server.gameevent"
dofile "lua/server/events/init.lua"
GameLogic = require "server.gamelogic"
ServerPlayer = require "server.serverplayer"
@ -64,7 +66,7 @@ function Room:initialize(_room)
end)
local ret, err_msg = true, true
while not self.game_finished do
ret, err_msg = coroutine.resume(main_co, err_msg)
ret, _, err_msg = coroutine.resume(main_co, err_msg)
-- handle error
if ret == false then
@ -512,7 +514,7 @@ function Room:delay(ms)
if rest <= 0 then
break
end
coroutine.yield(rest)
coroutine.yield("__handleRequest", rest)
end
end
@ -1075,147 +1077,16 @@ end
-- use card logic, and wrappers
------------------------------------------------------------------------
local playCardEmotionAndSound = function(room, player, card)
if card.type ~= Card.TypeEquip then
room:setEmotion(player, "./packages/" ..
card.package.extensionName .. "/image/anim/" .. card.name)
end
local soundName
if card.type == Card.TypeEquip then
local subTypeStr
if card.sub_type == Card.SubtypeDefensiveRide or card.sub_type == Card.SubtypeOffensiveRide then
subTypeStr = "horse"
elseif card.sub_type == Card.SubtypeWeapon then
subTypeStr = "weapon"
else
subTypeStr = "armor"
end
soundName = "./audio/card/common/" .. subTypeStr
else
soundName = "./packages/" .. card.package.extensionName .. "/audio/card/"
.. (player.gender == General.Male and "male/" or "female/") .. card.name
end
room:broadcastPlaySound(soundName)
local function execGameEvent(type, ...)
local event = GameEvent:new(type, ...)
local _, ret = event:exec()
return ret
end
---@param room Room
---@param cardUseEvent CardUseStruct
local sendCardEmotionAndLog = function(room, cardUseEvent)
local from = cardUseEvent.from
local _card = cardUseEvent.card
-- when this function is called, card is already in PlaceTable and no filter skill is applied.
-- So filter this card manually here to get 'real' use.card
local card = _card
if not _card:isVirtual() then
local temp = { card = _card }
Fk:filterCard(_card.id, room:getPlayerById(from), temp)
card = temp.card
end
playCardEmotionAndSound(room, room:getPlayerById(from), card)
room:doAnimate("Indicate", {
from = from,
to = cardUseEvent.tos or {},
})
local useCardIds = card:isVirtual() and card.subcards or { card.id }
if cardUseEvent.tos and #cardUseEvent.tos > 0 then
local to = {}
for _, t in ipairs(cardUseEvent.tos) do
table.insert(to, t[1])
end
if card:isVirtual() or (card ~= _card) then
if #useCardIds == 0 then
room:sendLog{
type = "#UseV0CardToTargets",
from = from,
to = to,
arg = card:toLogString(),
}
else
room:sendLog{
type = "#UseVCardToTargets",
from = from,
to = to,
card = useCardIds,
arg = card:toLogString(),
}
end
else
room:sendLog{
type = "#UseCardToTargets",
from = from,
to = to,
card = useCardIds
}
end
for _, t in ipairs(cardUseEvent.tos) do
if t[2] then
local temp = {table.unpack(t)}
table.remove(temp, 1)
room:sendLog{
type = "#CardUseCollaborator",
from = t[1],
to = temp,
arg = card.name,
}
end
end
elseif cardUseEvent.toCard then
if card:isVirtual() or (card ~= _card) then
if #useCardIds == 0 then
room:sendLog{
type = "#UseV0CardToCard",
from = from,
arg = cardUseEvent.toCard.name,
arg2 = card:toLogString(),
}
else
room:sendLog{
type = "#UseVCardToCard",
from = from,
card = useCardIds,
arg = cardUseEvent.toCard.name,
arg2 = card:toLogString(),
}
end
else
room:sendLog{
type = "#UseCardToCard",
from = from,
card = useCardIds,
arg = cardUseEvent.toCard.name,
}
end
else
if card:isVirtual() or (card ~= _card) then
if #useCardIds == 0 then
room:sendLog{
type = "#UseV0Card",
from = from,
arg = card:toLogString(),
}
else
room:sendLog{
type = "#UseVCard",
from = from,
card = useCardIds,
arg = card:toLogString(),
}
end
else
room:sendLog{
type = "#UseCard",
from = from,
card = useCardIds,
}
end
end
---@return boolean
function Room:useCard(cardUseEvent)
return execGameEvent(GameEvent.UseCard, cardUseEvent)
end
---@param room Room
@ -1327,60 +1198,6 @@ local onAim = function(room, cardUseEvent, aimEventCollaborators)
return true
end
---@param cardUseEvent CardUseStruct
---@return boolean
function Room:useCard(cardUseEvent)
local from = cardUseEvent.from
self:moveCards({
ids = self:getSubcardsByRule(cardUseEvent.card),
from = from,
toArea = Card.Processing,
moveReason = fk.ReasonUse,
})
if cardUseEvent.card.skill then
cardUseEvent.card.skill:onUse(self, cardUseEvent)
end
sendCardEmotionAndLog(self, cardUseEvent)
if self.logic:trigger(fk.PreCardUse, self:getPlayerById(cardUseEvent.from), cardUseEvent) then
goto clean
end
if not cardUseEvent.extraUse then
self:getPlayerById(cardUseEvent.from):addCardUseHistory(cardUseEvent.card.trueName, 1)
end
if cardUseEvent.responseToEvent then
cardUseEvent.responseToEvent.cardsResponded = cardUseEvent.responseToEvent.cardsResponded or {}
table.insert(cardUseEvent.responseToEvent.cardsResponded, cardUseEvent.card)
end
for _, event in ipairs({ fk.AfterCardUseDeclared, fk.AfterCardTargetDeclared, fk.BeforeCardUseEffect, fk.CardUsing }) do
if not cardUseEvent.toCard and #TargetGroup:getRealTargets(cardUseEvent.tos) == 0 then
break
end
self.logic:trigger(event, self:getPlayerById(cardUseEvent.from), cardUseEvent)
if event == fk.CardUsing then
self:doCardUseEffect(cardUseEvent)
end
end
self.logic:trigger(fk.CardUseFinished, self:getPlayerById(cardUseEvent.from), cardUseEvent)
::clean::
local leftRealCardIds = self:getSubcardsByRule(cardUseEvent.card, { Card.Processing })
if #leftRealCardIds > 0 then
self:moveCards({
ids = leftRealCardIds,
toArea = Card.DiscardPile,
moveReason = fk.ReasonPutIntoDiscardPile,
})
end
end
---@param cardUseEvent CardUseStruct
function Room:doCardUseEffect(cardUseEvent)
---@type table<string, AimStruct>
@ -1607,7 +1424,9 @@ function Room:doCardEffect(cardEffectEvent)
if event == fk.CardEffecting then
if cardEffectEvent.card.skill then
cardEffectEvent.card.skill:onEffect(self, cardEffectEvent)
execGameEvent(GameEvent.SkillEffect, function ()
cardEffectEvent.card.skill:onEffect(self, cardEffectEvent)
end)
end
end
end
@ -1615,53 +1434,7 @@ end
---@param cardResponseEvent CardResponseEvent
function Room:responseCard(cardResponseEvent)
local from = cardResponseEvent.customFrom or cardResponseEvent.from
local card = cardResponseEvent.card
local cardIds = self:getSubcardsByRule(card)
if card:isVirtual() then
if #cardIds == 0 then
self:sendLog{
type = "#ResponsePlayV0Card",
from = from,
arg = card:toLogString(),
}
else
self:sendLog{
type = "#ResponsePlayVCard",
from = from,
card = cardIds,
arg = card:toLogString(),
}
end
else
self:sendLog{
type = "#ResponsePlayCard",
from = from,
card = cardIds,
}
end
self:moveCards({
ids = cardIds,
from = from,
toArea = Card.Processing,
moveReason = fk.ReasonResonpse,
})
playCardEmotionAndSound(self, self:getPlayerById(from), card)
for _, event in ipairs({ fk.PreCardRespond, fk.CardResponding, fk.CardRespondFinished }) do
self.logic:trigger(event, self:getPlayerById(cardResponseEvent.from), cardResponseEvent)
end
local realCardIds = self:getSubcardsByRule(cardResponseEvent.card, { Card.Processing })
if #realCardIds > 0 and not cardResponseEvent.skipDrop then
self:moveCards({
ids = realCardIds,
toArea = Card.DiscardPile,
moveReason = fk.ReasonPutIntoDiscardPile,
})
end
return execGameEvent(GameEvent.RespondCard, cardResponseEvent)
end
------------------------------------------------------------------------
-- move cards, and wrappers
@ -1670,119 +1443,7 @@ end
---@vararg CardsMoveInfo
---@return boolean
function Room:moveCards(...)
---@type CardsMoveStruct[]
local cardsMoveStructs = {}
local infoCheck = function(info)
assert(table.contains({ Card.PlayerHand, Card.PlayerEquip, Card.PlayerJudge, Card.PlayerSpecial, Card.Processing, Card.DrawPile, Card.DiscardPile, Card.Void }, info.toArea))
assert(info.toArea ~= Card.PlayerSpecial or type(info.specialName) == "string")
assert(type(info.moveReason) == "number")
end
for _, cardsMoveInfo in ipairs({...}) do
if #cardsMoveInfo.ids > 0 then
infoCheck(cardsMoveInfo)
---@type MoveInfo[]
local infos = {}
for _, id in ipairs(cardsMoveInfo.ids) do
table.insert(infos, {
cardId = id,
fromArea = self:getCardArea(id),
fromSpecialName = cardsMoveInfo.from and self:getPlayerById(cardsMoveInfo.from):getPileNameOfId(id),
})
end
---@type CardsMoveStruct
local cardsMoveStruct = {
moveInfo = infos,
from = cardsMoveInfo.from,
to = cardsMoveInfo.to,
toArea = cardsMoveInfo.toArea,
moveReason = cardsMoveInfo.moveReason,
proposer = cardsMoveInfo.proposer,
skillName = cardsMoveInfo.skillName,
moveVisible = cardsMoveInfo.moveVisible,
specialName = cardsMoveInfo.specialName,
specialVisible = cardsMoveInfo.specialVisible,
}
table.insert(cardsMoveStructs, cardsMoveStruct)
end
end
if #cardsMoveStructs < 1 then
return false
end
if self.logic:trigger(fk.BeforeCardsMove, nil, cardsMoveStructs) then
return false
end
self:notifyMoveCards(nil, cardsMoveStructs)
for _, data in ipairs(cardsMoveStructs) do
if #data.moveInfo > 0 then
infoCheck(data)
---@param info MoveInfo
for _, info in ipairs(data.moveInfo) do
local realFromArea = self:getCardArea(info.cardId)
local playerAreas = { Player.Hand, Player.Equip, Player.Judge, Player.Special }
if table.contains(playerAreas, realFromArea) and data.from then
self:getPlayerById(data.from):removeCards(realFromArea, { info.cardId }, info.fromSpecialName)
elseif realFromArea ~= Card.Unknown then
local fromAreaIds = {}
if realFromArea == Card.Processing then
fromAreaIds = self.processing_area
elseif realFromArea == Card.DrawPile then
fromAreaIds = self.draw_pile
elseif realFromArea == Card.DiscardPile then
fromAreaIds = self.discard_pile
elseif realFromArea == Card.Void then
fromAreaIds = self.void
end
table.removeOne(fromAreaIds, info.cardId)
end
if table.contains(playerAreas, data.toArea) and data.to then
self:getPlayerById(data.to):addCards(data.toArea, { info.cardId }, data.specialName)
else
local toAreaIds = {}
if data.toArea == Card.Processing then
toAreaIds = self.processing_area
elseif data.toArea == Card.DrawPile then
toAreaIds = self.draw_pile
elseif data.toArea == Card.DiscardPile then
toAreaIds = self.discard_pile
elseif data.toArea == Card.Void then
toAreaIds = self.void
end
table.insert(toAreaIds, toAreaIds == Card.DrawPile and 1 or #toAreaIds + 1, info.cardId)
end
self:setCardArea(info.cardId, data.toArea, data.to)
Fk:filterCard(info.cardId, self:getPlayerById(data.to))
local currentCard = Fk:getCardById(info.cardId)
if
data.toArea == Player.Equip and
currentCard.type == Card.TypeEquip and
data.to ~= nil and
self:getPlayerById(data.to):isAlive() and
currentCard.equip_skill
then
currentCard:onInstall(self, self:getPlayerById(data.to))
elseif realFromArea == Player.Equip and currentCard.type == Card.TypeEquip and data.from ~= nil and currentCard.equip_skill then
currentCard:onUninstall(self, self:getPlayerById(data.from))
end
end
end
end
self.logic:trigger(fk.AfterCardsMove, nil, cardsMoveStructs)
return true
return execGameEvent(GameEvent.MoveCards, ...)
end
---@param player integer
@ -1872,91 +1533,7 @@ end
---@param damageStruct DamageStruct|null
---@return boolean
function Room:changeHp(player, num, reason, skillName, damageStruct)
if num == 0 then
return false
end
assert(reason == nil or table.contains({ "loseHp", "damage", "recover" }, reason))
---@type HpChangedData
local data = {
num = num,
reason = reason,
skillName = skillName,
}
if self.logic:trigger(fk.BeforeHpChanged, player, data) then
return false
end
assert(not (data.reason == "recover" and data.num < 0))
player.hp = math.min(player.hp + data.num, player.maxHp)
self:broadcastProperty(player, "hp")
if reason == "damage" then
local damage_nature_table = {
[fk.NormalDamage] = "normal_damage",
[fk.FireDamage] = "fire_damage",
[fk.ThunderDamage] = "thunder_damage",
}
if damageStruct.from then
self:sendLog{
type = "#Damage",
to = {damageStruct.from.id},
from = player.id,
arg = 0 - num,
arg2 = damage_nature_table[damageStruct.damageType],
}
else
self:sendLog{
type = "#DamageWithNoFrom",
from = player.id,
arg = 0 - num,
arg2 = damage_nature_table[damageStruct.damageType],
}
end
self:sendLogEvent("Damage", {
to = player.id,
damageType = damage_nature_table[damageStruct.damageType],
damageNum = damageStruct.damage,
})
elseif reason == "loseHp" then
self:sendLog{
type = "#LoseHP",
from = player.id,
arg = 0 - num,
}
self:sendLogEvent("LoseHP", {})
elseif reason == "recover" then
self:sendLog{
type = "#HealHP",
from = player.id,
arg = num,
}
end
self:sendLog{
type = "#ShowHPAndMaxHP",
from = player.id,
arg = player.hp,
arg2 = player.maxHp,
}
self.logic:trigger(fk.HpChanged, player, data)
if player.hp < 1 then
if num < 0 then
---@type DyingStruct
local dyingStruct = {
who = player.id,
damage = damageStruct,
}
self:enterDying(dyingStruct)
end
elseif player.dying then
player.dying = false
end
return true
return execGameEvent(GameEvent.ChangeHp, player, num, reason, skillName, damageStruct)
end
---@param player ServerPlayer
@ -1964,181 +1541,36 @@ end
---@param skillName string
---@return boolean
function Room:loseHp(player, num, skillName)
if num == nil then
num = 1
elseif num < 1 then
return false
end
---@type HpLostData
local data = {
num = num,
skillName = skillName,
}
if self.logic:trigger(fk.PreHpLost, player, data) or data.num < 1 then
return false
end
if not self:changeHp(player, -num, "loseHp", skillName) then
return false
end
self.logic:trigger(fk.HpLost, player, data)
return true
return execGameEvent(GameEvent.LoseHp, player, num, skillName)
end
---@param player ServerPlayer
---@param num integer
---@return boolean
function Room:changeMaxHp(player, num)
if num == 0 then
return false
end
player.maxHp = math.max(player.maxHp + num, 0)
self:broadcastProperty(player, "maxHp")
local diff = player.hp - player.maxHp
if diff > 0 then
if not self:changeHp(player, -diff) then
player.hp = player.hp - diff
end
end
if player.maxHp == 0 then
self:killPlayer({ who = player.id })
end
self.logic:trigger(fk.MaxHpChanged, player, { num = num })
return true
return execGameEvent(GameEvent.ChangeMaxHp, player, num)
end
---@param damageStruct DamageStruct
---@return boolean
function Room:damage(damageStruct)
if damageStruct.damage < 1 then
return false
end
damageStruct.damageType = damageStruct.damageType or fk.NormalDamage
if damageStruct.from and not damageStruct.from:isAlive() then
damageStruct.from = nil
end
assert(damageStruct.to:isInstanceOf(ServerPlayer))
local stages = {
{fk.PreDamage, damageStruct.from},
{fk.DamageCaused, damageStruct.from},
{fk.DamageInflicted, damageStruct.to},
}
for _, struct in ipairs(stages) do
local event, player = table.unpack(struct)
if self.logic:trigger(event, player, damageStruct) or damageStruct.damage < 1 then
return false
end
assert(damageStruct.to:isInstanceOf(ServerPlayer))
end
if not damageStruct.to:isAlive() then
return false
end
if not self:changeHp(damageStruct.to, -damageStruct.damage, "damage", damageStruct.skillName, damageStruct) then
return false
end
stages = {
{fk.Damage, damageStruct.from},
{fk.Damaged, damageStruct.to},
{fk.DamageFinished, damageStruct.from},
}
for _, struct in ipairs(stages) do
local event, player = table.unpack(struct)
self.logic:trigger(event, player, damageStruct)
end
return true
return execGameEvent(GameEvent.Damage, damageStruct)
end
---@param recoverStruct RecoverStruct
---@return boolean
function Room:recover(recoverStruct)
if recoverStruct.num < 1 then
return false
end
local who = recoverStruct.who
if self.logic:trigger(fk.PreHpRecover, who, recoverStruct) or recoverStruct.num < 1 then
return false
end
if not self:changeHp(who, recoverStruct.num, "recover", recoverStruct.skillName) then
return false
end
self.logic:trigger(fk.HpRecover, who, recoverStruct)
return true
return execGameEvent(GameEvent.Recover, recoverStruct)
end
---@param dyingStruct DyingStruct
function Room:enterDying(dyingStruct)
local dyingPlayer = self:getPlayerById(dyingStruct.who)
dyingPlayer.dying = true
self:broadcastProperty(dyingPlayer, "dying")
self:sendLog{
type = "#EnterDying",
from = dyingPlayer.id,
}
self.logic:trigger(fk.EnterDying, dyingPlayer, dyingStruct)
if dyingPlayer.hp < 1 then
self.logic:trigger(fk.Dying, dyingPlayer, dyingStruct)
self.logic:trigger(fk.AskForPeaches, dyingPlayer, dyingStruct)
self.logic:trigger(fk.AskForPeachesDone, dyingPlayer, dyingStruct)
end
if not dyingPlayer.dead then
dyingPlayer.dying = false
self:broadcastProperty(dyingPlayer, "dying")
end
self.logic:trigger(fk.AfterDying, dyingPlayer, dyingStruct)
return execGameEvent(GameEvent.Dying, dyingStruct)
end
---@param deathStruct DeathStruct
function Room:killPlayer(deathStruct)
local victim = self:getPlayerById(deathStruct.who)
victim.dead = true
table.removeOne(self.alive_players, victim)
local logic = self.logic
logic:trigger(fk.BeforeGameOverJudge, victim, deathStruct)
local killer = deathStruct.damage and deathStruct.damage.from or nil
if killer then
self:sendLog{
type = "#KillPlayer",
to = {killer.id},
from = victim.id,
arg = victim.role,
}
else
self:sendLog{
type = "#KillPlayerWithNoKiller",
from = victim.id,
arg = victim.role,
}
end
self:sendLogEvent("Death", {to = victim.id})
self:broadcastProperty(victim, "role")
self:broadcastProperty(victim, "dead")
logic:trigger(fk.GameOverJudge, victim, deathStruct)
logic:trigger(fk.Death, victim, deathStruct)
logic:trigger(fk.BuryVictim, victim, deathStruct)
return execGameEvent(GameEvent.Death, deathStruct)
end
-- lose/acquire skill actions
@ -2219,46 +1651,8 @@ end
-- judge
---@param data JudgeStruct
---@return Card
function Room:judge(data)
local who = data.who
self.logic:trigger(fk.StartJudge, who, data)
data.card = Fk:getCardById(self:getNCards(1)[1])
if data.reason ~= "" then
self:sendLog{
type = "#StartJudgeReason",
from = who.id,
arg = data.reason,
}
end
self:sendLog{
type = "#InitialJudge",
from = who.id,
card = {data.card.id},
}
self:moveCardTo(data.card, Card.Processing, nil, fk.ReasonPrey)
self.logic:trigger(fk.AskForRetrial, who, data)
self.logic:trigger(fk.FinishRetrial, who, data)
Fk:filterCard(data.card.id, who, data)
self:sendLog{
type = "#JudgeResult",
from = who.id,
card = {data.card.id},
}
if data.pattern then
self:delay(400);
self:setCardEmotion(data.card.id, data.card:matchPattern(data.pattern) and "judgegood" or "judgebad")
self:delay(900);
end
self.logic:trigger(fk.FinishJudge, who, data)
if self:getCardArea(data.card.id) == Card.Processing then
self:moveCardTo(data.card, Card.DiscardPile, nil, fk.ReasonPutIntoDiscardPile)
end
return execGameEvent(GameEvent.Judge, data)
end
---@param card Card
@ -2398,7 +1792,7 @@ function Room:useSkill(player, skill, effect_cb)
end
player:addSkillUseHistory(skill.name)
if effect_cb then
return effect_cb()
return execGameEvent(GameEvent.SkillEffect, effect_cb)
end
end
@ -2413,7 +1807,7 @@ function Room:gameOver(winner)
self:doBroadcastNotify("GameOver", winner)
self.room:gameOver()
coroutine.yield()
coroutine.yield("__handleRequest")
end
---@param card Card

View File

@ -85,7 +85,7 @@ local function _waitForReply(player, timeout)
local ret_msg = true
while ret_msg do
-- when ret_msg is false, that means there is no request in the queue
ret_msg = coroutine.yield(1)
ret_msg = coroutine.yield("__handleRequest", 1)
end
checkNoHuman(player.room)
@ -102,7 +102,7 @@ local function _waitForReply(player, timeout)
if timeout and rest <= 0 then
return ""
end
coroutine.yield(rest)
coroutine.yield("__handleRequest", rest)
end
end