diff --git a/lang/zh_CN.ts b/lang/zh_CN.ts
index adb7ddce..1f401669 100644
--- a/lang/zh_CN.ts
+++ b/lang/zh_CN.ts
@@ -167,10 +167,6 @@
已为您与服务器同步拓展包,请尝试再次连入
-
-
- 是否确认退出?
-
@@ -179,6 +175,10 @@
新月杀
+
+
+ 是否确认退出?
+
diff --git a/lua/client/i18n/zh_CN.lua b/lua/client/i18n/zh_CN.lua
index 6ce4d89d..22959182 100644
--- a/lua/client/i18n/zh_CN.lua
+++ b/lua/client/i18n/zh_CN.lua
@@ -303,6 +303,7 @@ Fk:loadTranslationTable{
-- phase
["#PhaseSkipped"] = "%from 跳过了 %arg",
["#GainAnExtraTurn"] = "%from 开始进行一个额外的回合",
+ ["#GainAnExtraPhase"] = "%from 开始进行一个额外的 %arg",
-- useCard
["#UseCard"] = "%from 使用了牌 %card",
diff --git a/lua/core/general.lua b/lua/core/general.lua
index 1bb43d0a..70c81da1 100644
--- a/lua/core/general.lua
+++ b/lua/core/general.lua
@@ -59,6 +59,10 @@ function General:initialize(package, name, kingdom, hp, maxHp, gender)
package:addGeneral(self)
end
+function General:__tostring()
+ return string.format("", self.name)
+end
+
--- 为武将增加技能,需要注意增加其他武将技能时的处理方式。
---@param skill Skill @ (单个)武将技能
function General:addSkill(skill)
diff --git a/lua/core/player.lua b/lua/core/player.lua
index 5ed8dd1b..1422e88c 100644
--- a/lua/core/player.lua
+++ b/lua/core/player.lua
@@ -444,6 +444,18 @@ function Player:inMyAttackRange(other, fixLimit)
return self:distanceTo(other) <= (baseAttackRange + fixLimit)
end
+function Player:getNextAlive()
+ if Fk:currentRoom().alive_players == 0 then
+ return self
+ end
+
+ local ret = self.next
+ while ret.dead do
+ ret = ret.next
+ end
+ return ret
+end
+
--- 增加玩家使用特定牌的历史次数。
---@param cardName string @ 牌名
---@param num integer @ 次数
diff --git a/lua/core/util.lua b/lua/core/util.lua
index dc26ebcb..cdbd7ca9 100644
--- a/lua/core/util.lua
+++ b/lua/core/util.lua
@@ -1,6 +1,32 @@
-- SPDX-License-Identifier: GPL-3.0-or-later
local Util = {}
+Util.DummyFunc = function() end
+Util.DummyTable = setmetatable({}, {
+ __newindex = function() error("Cannot assign to dummy table") end
+})
+
+local metamethods = {
+ "__add", "__sub", "__mul", "__div", "__mod", "__pow", "__unm", "__idiv",
+ "__band", "__bor", "__bxor", "__bnot", "__shl", "__shr",
+ "__concat", "__len", "__eq", "__lt", "__le", "__call",
+ -- "__index", "__newindex",
+}
+-- 别对类用 暂且会弄坏isSubclassOf 懒得研究先
+Util.lockTable = function(t)
+ local mt = getmetatable(t) or Util.DummyTable
+ local new_mt = {
+ __index = t,
+ __newindex = function() error("Cannot assign to locked table") end,
+ __metatable = false,
+ }
+ for _, e in ipairs(metamethods) do
+ new_mt[e] = mt[e]
+ end
+ return setmetatable({}, new_mt)
+end
+
+function printf(fmt, ...) print(string.format(fmt, ...)) end
-- the iterator of QList object
local qlist_iterator = function(list, n)
@@ -150,13 +176,13 @@ function table.simpleClone(self)
return ret
end
--- similar to table.clone but convert all class/instances to string
+-- similar to table.clone but not clone class/instances
function table.cloneWithoutClass(self)
local ret = {}
for k, v in pairs(self) do
if type(v) == "table" then
if v.class or v.super then
- ret[k] = tostring(v)
+ ret[k] = v
else
ret[k] = table.cloneWithoutClass(v)
end
diff --git a/lua/lib/middleclass.lua b/lua/lib/middleclass.lua
index b479f1ef..626a13d7 100644
--- a/lua/lib/middleclass.lua
+++ b/lua/lib/middleclass.lua
@@ -77,6 +77,9 @@ local function _call(self, ...) return self:new(...) end
local function _createClass(name, super)
local dict = {}
dict.__index = dict
+ --[[ debug
+ dict.__gc = function(t) printf("%s destructed", tostring(t)) end
+ --]]
local aClass = { name = name, super = super, static = {},
__instanceDict = dict, __declaredMethods = {},
diff --git a/lua/server/event.lua b/lua/server/event.lua
index 2c31c4a2..a88deace 100644
--- a/lua/server/event.lua
+++ b/lua/server/event.lua
@@ -111,4 +111,7 @@ fk.CardShown = 77
-- 79 = BeforeTurnOver
-- 80 = BeforeChainStateChange
-fk.NumOfEvents = 81
+fk.SkillEffect = 81
+fk.AfterSkillEffect = 82
+
+fk.NumOfEvents = 83
diff --git a/lua/server/events/init.lua b/lua/server/events/init.lua
index 8706c6eb..139727e6 100644
--- a/lua/server/events/init.lua
+++ b/lua/server/events/init.lua
@@ -2,6 +2,9 @@
-- Definitions of game events
+-- 某类事件对应的结束事件,其id刚好就是那个事件的相反数
+-- GameEvent.EventFinish = -1
+
GameEvent.ChangeHp = 1
GameEvent.Damage = 2
GameEvent.LoseHp = 3
diff --git a/lua/server/events/skill.lua b/lua/server/events/skill.lua
index dd1a8d81..ff391338 100644
--- a/lua/server/events/skill.lua
+++ b/lua/server/events/skill.lua
@@ -1,6 +1,16 @@
-- SPDX-License-Identifier: GPL-3.0-or-later
GameEvent.functions[GameEvent.SkillEffect] = function(self)
- local effect_cb = table.unpack(self.data)
- return effect_cb()
+ local effect_cb, player, skill = table.unpack(self.data)
+ local room = self.room
+ local logic = room.logic
+
+ local cost_data_bak = skill.cost_data
+ logic:trigger(fk.SkillEffect, player, skill)
+ skill.cost_data = cost_data_bak
+
+ local ret = effect_cb()
+
+ logic:trigger(fk.AfterSkillEffect, player, skill)
+ return ret
end
diff --git a/lua/server/gameevent.lua b/lua/server/gameevent.lua
index 4e135c9f..2ea21a0d 100644
--- a/lua/server/gameevent.lua
+++ b/lua/server/gameevent.lua
@@ -1,40 +1,70 @@
-- SPDX-License-Identifier: GPL-3.0-or-later
---@class GameEvent: Object
----@field public room Room
----@field public event integer
----@field public data any
----@field public parent GameEvent
----@field public main_func fun(self: GameEvent)
----@field public clear_func fun(self: GameEvent)
----@field public extra_clear_funcs any[]
----@field public interrupted boolean
+---@field public id integer @ 事件的id,随着时间推移自动增加并分配给新事件
+---@field public end_id integer @ 事件的对应结束id,如果整个事件中未插入事件,那么end_id就是自己的id
+---@field public room Room @ room实例
+---@field public event integer @ 该事件对应的EventType
+---@field public data any @ 事件的附加数据,视类型而定
+---@field public parent GameEvent @ 事件的父事件(栈中的上一层事件)
+---@field public main_func fun(self: GameEvent) @ 事件的主函数
+---@field public clear_func fun(self: GameEvent) @ 事件结束时执行的函数
+---@field public extra_clear_funcs fun(self:GameEvent)[] @ 事件结束时执行的自定义函数列表
+---@field public exit_func fun(self: GameEvent) @ 事件结束后执行的函数
+---@field public extra_exit_funcs fun(self:GameEvent)[] @ 事件结束后执行的自定义函数
+---@field public interrupted boolean @ 事件是否是因为被强行中断而结束的
local GameEvent = class("GameEvent")
+---@type fun(self: GameEvent)[]
GameEvent.functions = {}
+
+---@type fun(self: GameEvent)[]
GameEvent.cleaners = {}
+
+---@type fun(self: GameEvent)[]
+GameEvent.exit_funcs = {}
+
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
+local dummyFunc = Util.DummyFunc
function GameEvent:initialize(event, ...)
+ self.id = -1
+ self.end_id = -1
self.room = RoomInstance
self.event = event
self.data = { ... }
self.main_func = wrapCoFunc(GameEvent.functions[event], self) or dummyFunc
self.clear_func = GameEvent.cleaners[event] or dummyFunc
- self.extra_clear_funcs = {}
+ self.extra_clear_funcs = Util.DummyTable
+ self.exit_func = GameEvent.exit_funcs[event] or dummyFunc
+ self.extra_exit_funcs = Util.DummyTable
self.interrupted = false
end
function GameEvent:__tostring()
- return GameEvent:translate(self.event)
+ return string.format("<%s #%d>", GameEvent:translate(self.event), self.id)
end
-function GameEvent:findParent(eventType)
+function GameEvent:addCleaner(f)
+ if self.extra_clear_funcs == Util.DummyTable then
+ self.extra_clear_funcs = {}
+ end
+ table.insert(self.extra_clear_funcs, f)
+end
+
+function GameEvent:addExitFunc(f)
+ if self.extra_exit_funcs == Util.DummyTable then
+ self.extra_exit_funcs = {}
+ end
+ table.insert(self.extra_exit_funcs, f)
+end
+
+function GameEvent:findParent(eventType, includeSelf)
+ if includeSelf and self.event == eventType then return self end
local e = self.parent
repeat
if e.event == eventType then return e end
@@ -43,12 +73,83 @@ function GameEvent:findParent(eventType)
return nil
end
+-- 找n个id介于from和to之间的事件。
+local function bin_search(events, from, to, n, func)
+ local left = 1
+ local right = #events
+ local mid
+ local ret = {}
+
+ if from < events[1].id then
+ mid = 1
+ elseif from > events[right].id then
+ return ret
+ else
+ while true do
+ if left > right then return ret end
+ mid = (left + right) // 2
+ local id = events[mid].id
+ local id_left = mid == 1 and -math.huge or events[mid - 1].id
+
+ if from < id then
+ if from >= id_left then
+ break
+ end
+ right = mid - 1
+ else
+ left = mid + 1
+ end
+ end
+ end
+
+ for i = mid, #events do
+ local v = events[i]
+ if v.id < to and func(v) then
+ table.insert(ret, v)
+ end
+ if #ret >= n then break end
+ end
+
+ return ret
+end
+
+-- 从某个区间中,找出类型符合且符合func函数检测的至多n个事件。
+---@param eventType integer @ 要查找的事件类型
+---@param n integer @ 最多找多少个
+---@param func fun(e: GameEvent): boolean @ 过滤用的函数
+---@param endEvent GameEvent|nil @ 区间终止点,默认为本事件结束
+---@return GameEvent[] @ 找到的符合条件的所有事件,最多n个但不保证有n个
+function GameEvent:searchEvents(eventType, n, func, endEvent)
+ local logic = self.room.logic
+ local events = logic.event_recorder[eventType]
+ local from = self.id
+ local to = endEvent and endEvent.id or self.end_id
+ if to == -1 then to = #logic.all_game_events end
+ n = n or 1
+ func = func or function() return true end
+
+ local ret
+ if #events < 6 then
+ ret = {}
+ for _, v in ipairs(events) do
+ if v.id > from and v.id < to and func(v) then
+ table.insert(ret, v)
+ end
+ if #ret >= n then break end
+ end
+ else
+ ret = bin_search(events, from, to, n, func)
+ end
+
+ return ret
+end
+
function GameEvent:clear()
local clear_co = coroutine.create(function()
+ self:clear_func()
for _, f in ipairs(self.extra_clear_funcs) do
if type(f) == "function" then f(self) end
end
- self:clear_func()
end)
while true do
@@ -72,6 +173,28 @@ function GameEvent:clear()
break
end
end
+
+ local logic = RoomInstance.logic
+ local end_id = logic.current_event_id + 1
+ if self.id ~= end_id - 1 then
+ logic.all_game_events[end_id] = -self.event
+ logic.current_event_id = end_id
+ self.end_id = end_id
+ else
+ self.end_id = self.id
+ end
+
+ logic.game_event_stack:pop()
+
+ local err, msg
+ err, msg = xpcall(self.exit_func, debug.traceback, self)
+ if err == false then fk.qCritical(msg) end
+ for _, f in ipairs(self.extra_exit_funcs) do
+ if type(f) == "function" then
+ err, msg = xpcall(f, debug.traceback, self)
+ if err == false then fk.qCritical(msg) end
+ end
+ end
end
local function breakEvent(self, extra_yield_result)
@@ -95,6 +218,12 @@ function GameEvent:exec()
self.parent = logic:getCurrentEvent()
logic.game_event_stack:push(self)
+ logic.current_event_id = logic.current_event_id + 1
+ self.id = logic.current_event_id
+ logic.all_game_events[self.id] = self
+ logic.event_recorder[self.event] = logic.event_recorder[self.event] or {}
+ table.insert(logic.event_recorder[self.event], self)
+
local co = coroutine.create(self.main_func)
while true do
local err, yield_result, extra_yield_result = coroutine.resume(co)
@@ -123,7 +252,7 @@ function GameEvent:exec()
-- yield to corresponding GameEvent, first pop self from stack
self.interrupted = true
self:clear()
- logic.game_event_stack:pop(self)
+ -- logic.game_event_stack:pop(self)
coroutine.close(co)
-- then, call yield
@@ -151,7 +280,6 @@ function GameEvent:exec()
end
end
- logic.game_event_stack:pop(self)
return ret, extra_ret
end
diff --git a/lua/server/gamelogic.lua b/lua/server/gamelogic.lua
index 3d0e4893..61c42eaf 100644
--- a/lua/server/gamelogic.lua
+++ b/lua/server/gamelogic.lua
@@ -3,12 +3,14 @@
---@class GameLogic: Object
---@field public room Room
---@field public skill_table table
----@field public skill_priority_table
+---@field public skill_priority_table table
---@field public refresh_skill_table table
---@field public skills string[]
----@field public event_stack Stack
---@field public game_event_stack Stack
---@field public role_table string[][]
+---@field public all_game_events GameEvent[]
+---@field public event_recorder table
+---@field public current_event_id integer
local GameLogic = class("GameLogic")
function GameLogic:initialize(room)
@@ -17,8 +19,10 @@ function GameLogic:initialize(room)
self.skill_priority_table = {}
self.refresh_skill_table = {}
self.skills = {} -- skillName[]
- self.event_stack = Stack:new()
self.game_event_stack = Stack:new()
+ self.all_game_events = {}
+ self.event_recorder = {}
+ self.current_event_id = 0
self.role_table = {
{ "lord" },
@@ -381,8 +385,6 @@ function GameLogic:trigger(event, target, data, refresh_only)
local _target = room.current -- for iteration
local player = _target
- self.event_stack:push({event, target, data})
-
if #skills_to_refresh > 0 then repeat do
-- refresh skills. This should not be broken
for _, skill in ipairs(skills_to_refresh) do
@@ -450,7 +452,6 @@ function GameLogic:trigger(event, target, data, refresh_only)
::trigger_loop_continue::
end
- self.event_stack:pop()
return broken
end
@@ -459,6 +460,29 @@ function GameLogic:getCurrentEvent()
return self.game_event_stack.t[self.game_event_stack.p]
end
+-- 在指定历史范围中找至多n个符合条件的事件
+---@param eventType integer @ 要查找的事件类型
+---@param n integer @ 最多找多少个
+---@param func fun(e: GameEvent): boolean @ 过滤用的函数
+---@param scope integer @ 查询历史范围,只能是当前阶段/回合/轮次
+---@return GameEvent[] @ 找到的符合条件的所有事件,最多n个但不保证有n个
+function GameLogic:getEventsOfScope(eventType, n, func, scope)
+ scope = scope or Player.HistoryTurn
+ local event = self:getCurrentEvent()
+ local start_event ---@type GameEvent
+ if scope == Player.HistoryGame then
+ start_event = self.all_game_events[1]
+ elseif scope == Player.HistoryRound then
+ start_event = event:findParent(GameEvent.Round)
+ elseif scope == Player.HistoryTurn then
+ start_event = event:findParent(GameEvent.Turn)
+ elseif scope == Player.HistoryPhase then
+ start_event = event:findParent(GameEvent.Phase)
+ end
+
+ return start_event:searchEvents(eventType, n, func)
+end
+
function GameLogic:dumpEventStack(detailed)
local top = self:getCurrentEvent()
local i = self.game_event_stack.p
@@ -493,6 +517,28 @@ function GameLogic:dumpEventStack(detailed)
print("\n===== End of event stack dump =====")
end
+function GameLogic:dumpAllEvents(from, to)
+ from = from or 1
+ to = to or #self.all_game_events
+ assert(from <= to)
+
+ local indent = 0
+ local tab = " "
+ for i = from, to, 1 do
+ local v = self.all_game_events[i]
+ if type(v) == "number" then
+ indent = math.max(indent - 1, 0)
+ -- v = "End"
+ -- print(tab:rep(indent) .. string.format("#%d: %s", i, v))
+ else
+ print(tab:rep(indent) .. string.format("%s", tostring(v)))
+ if v.id ~= v.end_id then
+ indent = indent + 1
+ end
+ end
+ end
+end
+
function GameLogic:breakEvent(ret)
coroutine.yield("__breakEvent", ret)
end
diff --git a/lua/server/room.lua b/lua/server/room.lua
index ed6eba0c..71bab027 100644
--- a/lua/server/room.lua
+++ b/lua/server/room.lua
@@ -62,8 +62,7 @@ dofile "lua/server/ai/init.lua"
---@param _room fk.Room
function Room:initialize(_room)
self.room = _room
-
- self.room.startGame = function(_self)
+ _room.startGame = function(_self)
Room.initialize(self, _room) -- clear old data
self.settings = json.decode(_room:settings())
Fk.disabled_packs = self.settings.disabledPack
@@ -1387,7 +1386,7 @@ function Room:handleUseCardReply(player, data)
Self = player
local c = skill:viewAs(selected_cards)
if c then
- self:useSkill(player, skill)
+ self:useSkill(player, skill, Util.DummyFunc)
local use = {} ---@type CardUseStruct
use.from = player.id
@@ -2177,7 +2176,7 @@ function Room:handleCardEffect(event, cardEffectEvent)
if cardEffectEvent.card.skill then
execGameEvent(GameEvent.SkillEffect, function ()
cardEffectEvent.card.skill:onEffect(self, cardEffectEvent)
- end)
+ end, self:getPlayerById(cardEffectEvent.from), cardEffectEvent.card.skill)
end
end
end
@@ -2691,7 +2690,7 @@ function Room:useSkill(player, skill, effect_cb)
player:addSkillUseHistory(skill.name)
if effect_cb then
- return execGameEvent(GameEvent.SkillEffect, effect_cb)
+ return execGameEvent(GameEvent.SkillEffect, effect_cb, player, skill)
end
end
diff --git a/lua/server/serverplayer.lua b/lua/server/serverplayer.lua
index b78f3be3..35667d60 100644
--- a/lua/server/serverplayer.lua
+++ b/lua/server/serverplayer.lua
@@ -322,18 +322,6 @@ function ServerPlayer:isAlive()
return self.dead == false
end
-function ServerPlayer:getNextAlive()
- if #self.room.alive_players == 0 then
- return self
- end
-
- local ret = self.next
- while ret.dead do
- ret = ret.next
- end
- return ret
-end
-
function ServerPlayer:turnOver()
if self.room.logic:trigger(fk.BeforeTurnOver, self) then
return
@@ -367,6 +355,13 @@ function ServerPlayer:showCards(cards)
room.logic:trigger(fk.CardShown, self, { cardIds = cards })
end
+local phase_name_table = {
+ [Player.Judge] = "phase_judge",
+ [Player.Draw] = "phase_draw",
+ [Player.Play] = "phase_play",
+ [Player.Discard] = "phase_discard",
+}
+
---@param from_phase Phase
---@param to_phase Phase
function ServerPlayer:changePhase(from_phase, to_phase)
@@ -397,25 +392,35 @@ function ServerPlayer:changePhase(from_phase, to_phase)
return false
end
-function ServerPlayer:gainAnExtraPhase(phase)
+function ServerPlayer:gainAnExtraPhase(phase, delay)
local room = self.room
+ delay = (delay == nil) and true or delay
+ if delay then
+ local logic = room.logic
+ local turn = logic:getCurrentEvent():findParent(GameEvent.Phase, true)
+ if turn then
+ turn:addExitFunc(function() self:gainAnExtraPhase(phase, false) end)
+ return
+ end
+ end
+
local current = self.phase
self.phase = phase
room:notifyProperty(self, self, "phase")
+ room:sendLog{
+ type = "#GainAnExtraPhase",
+ from = self.id,
+ arg = phase_name_table[phase],
+ }
+
+
GameEvent(GameEvent.Phase, self):exec()
self.phase = current
room:notifyProperty(self, self, "phase")
end
-local phase_name_table = {
- [Player.Judge] = "phase_judge",
- [Player.Draw] = "phase_draw",
- [Player.Play] = "phase_play",
- [Player.Discard] = "phase_discard",
-}
-
---@param phase_table Phase[]
function ServerPlayer:play(phase_table)
phase_table = phase_table or {}
@@ -505,8 +510,18 @@ function ServerPlayer:skip(phase)
end
end
-function ServerPlayer:gainAnExtraTurn()
+function ServerPlayer:gainAnExtraTurn(delay)
local room = self.room
+ delay = (delay == nil) and true or delay
+ if delay then
+ local logic = room.logic
+ local turn = logic:getCurrentEvent():findParent(GameEvent.Turn, true)
+ if turn then
+ turn:addExitFunc(function() self:gainAnExtraTurn(false) end)
+ return
+ end
+ end
+
room:sendLog{
type = "#GainAnExtraTurn",
from = self.id