diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 9d5fab94..bc141151 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -41,6 +41,7 @@ jobs: find -name "*.cpp" -exec sed -i '1i #include "pch.h"' "{}" \; find -name "*.h" -exec sed -i '1i #include "pch.h"' "{}" \; sed -i '1d' pch.h + sed -i '1d' main.cpp sed -i '/pch.h/d' CMakeLists.txt - name: Configure CMake Project diff --git a/CHANGELOG.md b/CHANGELOG.md index f2d91660..21c9f97e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # ChangeLog +## v0.4.20 + +- 重构了UI逻辑 +- 重构了客户端隐藏信息的处理 +- 旁观不会看到手牌和身份了 +- 牌局内可查看一览 +- 新增了明置牌概念 以及相关状态技 +- 新增了筛选房间功能 +- 新增拖动手牌排序 +- 新增技能times + +___ + ## v0.4.19 - 修改加入服务器界面,新增服务器列表(暂不会自动更新) diff --git a/CMakeLists.txt b/CMakeLists.txt index aa9e6bd1..c8ffa7c9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.16) -project(FreeKill VERSION 0.4.19) +project(FreeKill VERSION 0.4.20) add_definitions(-DFK_VERSION=\"${CMAKE_PROJECT_VERSION}\") find_package(Qt6 REQUIRED COMPONENTS @@ -40,22 +40,6 @@ include_directories(include) include_directories(include/libgit2) include_directories(src) -file(GLOB SWIG_FILES "${PROJECT_SOURCE_DIR}/src/swig/*.i") -if (DEFINED FK_SERVER_ONLY) - set(SWIG_SOURCE ${PROJECT_SOURCE_DIR}/src/swig/freekill-nogui.i) -else () - set(SWIG_SOURCE ${PROJECT_SOURCE_DIR}/src/swig/freekill.i) -endif () - -add_custom_command( - OUTPUT ${PROJECT_SOURCE_DIR}/src/swig/freekill-wrap.cxx - DEPENDS ${SWIG_FILES} - COMMENT "Generating freekill-wrap.cxx" - COMMAND swig -c++ -lua -Wall -o - ${PROJECT_SOURCE_DIR}/src/swig/freekill-wrap.cxx - ${SWIG_SOURCE} -) - qt_add_executable(FreeKill) if (NOT DEFINED FK_SERVER_ONLY) diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 18108dbf..072e16fd 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -3,8 +3,8 @@ + android:versionCode="420" + android:versionName="0.4.20"> diff --git a/deploycore.sh b/deploycore.sh new file mode 100755 index 00000000..7bbf25a9 --- /dev/null +++ b/deploycore.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# 游戏依托freekill-core这个特殊仓库进行日常更新与开发 +# 在更新版本号之前,需要先把它们与自带的lua/同步一下 + +PWD=$(pwd) + +if ! [ -e packages/freekill-core ]; then + echo '需要有freekill-core才可执行' + cd $PWD + exit 1 +fi + +rm -r lua/ + +delcode() { + cd $1 + find -name '*.lua' -delete + find -empty -delete + cd .. +} +cd packages +delcode standard +delcode standard_cards +delcode maneuvering + +cp -r freekill-core/lua .. +cp -r freekill-core/standard . +cp -r freekill-core/standard_cards . +cp -r freekill-core/maneuvering . + +cd $PWD diff --git a/include b/include new file mode 160000 index 00000000..4fd2070d --- /dev/null +++ b/include @@ -0,0 +1 @@ +Subproject commit 4fd2070d099d1f967d1070d72beb0fae2cb6e4be diff --git a/lua/client/client.lua b/lua/client/client.lua index 590130f3..17d2340c 100644 --- a/lua/client/client.lua +++ b/lua/client/client.lua @@ -6,7 +6,6 @@ ---@field public alive_players ClientPlayer[] @ 所有存活玩家的数组 ---@field public observers ClientPlayer[] @ 观察者的数组 ---@field public current ClientPlayer @ 当前回合玩家 ----@field public discard_pile integer[] @ 弃牌堆 ---@field public observing boolean ---@field public record any ---@field public last_update_ui integer @ 上次刷新状态技UI的时间 @@ -35,7 +34,7 @@ local no_decode_commands = { function Client:initialize() AbstractRoom.initialize(self) self.client = fk.ClientInstance - self.notifyUI = function(self, command, data) + self.notifyUI = function(_, command, data) fk.Backend:notifyUI(command, data) end self.client.callback = function(_self, command, jsonData, isRequest) @@ -83,12 +82,11 @@ function Client:initialize() for _, cid in ipairs(Self:getCardIds("h")) do self:notifyUI("UpdateCard", cid) end + -- 刷技能状态 + self:notifyUI("UpdateSkill", nil) end end - self.discard_pile = {} - self._processing = {} - self.disabled_packs = {} self.disabled_generals = {} -- self.last_update_ui = os.getms() @@ -106,79 +104,20 @@ function Client:getPlayerById(id) return nil end ----@param cardId integer | Card ----@return CardArea -function Client:getCardArea(cardId) - local cardIds = Card:getIdList(cardId) - local resultPos = {} - for _, cid in ipairs(cardIds) do - if not table.contains(resultPos, Card.PlayerHand) and table.contains(Self.player_cards[Player.Hand], cid) then - table.insert(resultPos, Card.PlayerHand) - end - if not table.contains(resultPos, Card.PlayerEquip) and table.contains(Self.player_cards[Player.Equip], cid) then - table.insert(resultPos, Card.PlayerEquip) - end - for _, t in pairs(Self.special_cards) do - if table.contains(t, cid) then - table.insertIfNeed(resultPos, Card.PlayerSpecial) - end - end - end - if #resultPos == 1 then - return resultPos[1] - end - return Card.Unknown -end - +---@param moves CardsMoveStruct[] function Client:moveCards(moves) - for _, move in ipairs(moves) do - if move.from and move.fromArea then - local from = self:getPlayerById(move.from) - if move.fromArea == Card.PlayerHand and not Self:isBuddy(self:getPlayerById(move.from)) then - for _ = 1, #move.ids do - table.remove(from.player_cards[Player.Hand]) - end - else - if table.contains({ Player.Hand, Player.Equip, Player.Judge, Player.Special }, move.fromArea) then - from:removeCards(move.fromArea, move.ids, move.fromSpecialName) - end + for _, data in ipairs(moves) do + if #data.moveInfo > 0 then + for _, info in ipairs(data.moveInfo) do + self:applyMoveInfo(data, info) + Fk:filterCard(info.cardId, self:getPlayerById(data.to)) end - elseif move.fromArea == Card.DiscardPile then - table.removeOne(self.discard_pile, move.ids[1]) - end - - if move.to and move.toArea then - local ids = move.ids - if (move.toArea == Card.PlayerHand and not Self:isBuddy(self:getPlayerById(move.to))) or - (move.toArea == Card.PlayerSpecial and not move.moveVisible) then - ids = {-1} - end - - self:getPlayerById(move.to):addCards(move.toArea, ids, move.specialName) - elseif move.toArea == Card.DiscardPile then - table.insert(self.discard_pile, move.ids[1]) - end - - -- FIXME: 需要系统化的重构 - if move.fromArea == Card.Processing then - for _, v in ipairs(move.ids) do - self._processing[v] = nil - end - end - if move.toArea == Card.Processing then - for _, v in ipairs(move.ids) do - self._processing[v] = true - end - end - - if (move.ids[1] ~= -1) then - Fk:filterCard(move.ids[1], ClientInstance:getPlayerById(move.to)) end end end ---@param msg LogMessage -local function parseMsg(msg, nocolor) +local function parseMsg(msg, nocolor, visible_data) local self = ClientInstance local data = msg local function getPlayerStr(pid, color) @@ -218,7 +157,9 @@ local function parseMsg(msg, nocolor) local allUnknown = true local unknownCount = 0 for _, id in ipairs(card) do - if id ~= -1 then + local known = id ~= -1 + if visible_data then known = visible_data[tostring(id)] end + if known then allUnknown = false else unknownCount = unknownCount + 1 @@ -230,11 +171,15 @@ local function parseMsg(msg, nocolor) else local card_str = {} for _, id in ipairs(card) do - table.insert(card_str, Fk:getCardById(id, true):toLogString()) + local known = id ~= -1 + if visible_data then known = visible_data[tostring(id)] end + if known then + table.insert(card_str, Fk:getCardById(id, true):toLogString()) + end end if unknownCount > 0 then - table.insert(card_str, Fk:translate("unknown_card") - .. unknownCount == 1 and "x" .. unknownCount or "") + local suffix = unknownCount > 1 and ("x" .. unknownCount) or "" + table.insert(card_str, Fk:translate("unknown_card") .. suffix) end card = table.concat(card_str, ", ") end @@ -261,8 +206,8 @@ local function parseMsg(msg, nocolor) end ---@param msg LogMessage -function Client:appendLog(msg) - local text = parseMsg(msg) +function Client:appendLog(msg, visible_data) + local text = parseMsg(msg, nil, visible_data) self:notifyUI("GameLog", text) if msg.toast then self:notifyUI("ShowToast", text) @@ -298,7 +243,10 @@ end fk.client_callback["EnterRoom"] = function(_data) Self = ClientPlayer:new(fk.Self) + -- 垃圾bug 怎么把这玩意忘了 + local ob = ClientInstance.observing ClientInstance = Client:new() -- clear old client data + ClientInstance.observing = ob ClientInstance.players = {Self} ClientInstance.alive_players = {Self} ClientInstance.discard_pile = {} @@ -405,6 +353,14 @@ fk.client_callback["PropertyUpdate"] = function(data) ClientInstance:notifyUI("PropertyUpdate", data) end +fk.client_callback["PlayCard"] = function(data) + local h = Fk.request_handlers["PlayCard"]:new(Self) + h.change = {} + h:setup() + h.scene:notifyUI() + ClientInstance:notifyUI("PlayCard", data) +end + fk.client_callback["AskForCardChosen"] = function(data) -- jsonData: [ int target_id, string flag, int reason ] local id, flag, reason, prompt = data[1], data[2], data[3], data[4] @@ -557,7 +513,8 @@ local function mergeMoves(moves) proposer = move.proposer, } end - table.insert(temp[info].ids, move.moveVisible and move.ids[1] or -1) + -- table.insert(temp[info].ids, move.moveVisible and move.ids[1] or -1) + table.insert(temp[info].ids, move.ids[1]) end for _, v in pairs(temp) do table.insert(ret, v) @@ -565,109 +522,111 @@ local function mergeMoves(moves) return ret end -local function sendMoveCardLog(move) +local function sendMoveCardLog(move, visible_data) local client = ClientInstance ---@class Client if #move.ids == 0 then return end - local hidden = table.contains(move.ids, -1) + local hidden = not not table.find(move.ids, function(id) + return visible_data[tostring(id)] == false + end) local msgtype if move.toArea == Card.PlayerHand then if move.fromArea == Card.PlayerSpecial then - client:appendLog{ + client:appendLog({ type = "$GetCardsFromPile", from = move.to, arg = move.fromSpecialName, arg2 = #move.ids, card = move.ids, - } + }, visible_data) elseif move.fromArea == Card.DrawPile then - client:appendLog{ + client:appendLog({ type = "$DrawCards", from = move.to, card = move.ids, arg = #move.ids, - } + }, visible_data) elseif move.fromArea == Card.Processing then - client:appendLog{ + client:appendLog({ type = "$GotCardBack", from = move.to, card = move.ids, arg = #move.ids, - } + }, visible_data) elseif move.fromArea == Card.DiscardPile then - client:appendLog{ + client:appendLog({ type = "$RecycleCard", from = move.to, card = move.ids, arg = #move.ids, - } + }, visible_data) elseif move.from then - client:appendLog{ + client:appendLog({ type = "$MoveCards", from = move.from, to = { move.to }, arg = #move.ids, card = move.ids, - } + }, visible_data) else - client:appendLog{ + client:appendLog({ type = "$PreyCardsFromPile", from = move.to, card = move.ids, arg = #move.ids, - } + }, visible_data) end elseif move.toArea == Card.PlayerEquip then - client:appendLog{ + client:appendLog({ type = "$InstallEquip", from = move.to, card = move.ids, - } + }, visible_data) elseif move.toArea == Card.PlayerJudge then if move.from ~= move.to and move.fromArea == Card.PlayerJudge then - client:appendLog{ + client:appendLog({ type = "$LightningMove", from = move.from, to = { move.to }, card = move.ids, - } + }, visible_data) elseif move.from then - client:appendLog{ + client:appendLog({ type = "$PasteCard", from = move.from, to = { move.to }, card = move.ids, - } + }, visible_data) end elseif move.toArea == Card.PlayerSpecial then - client:appendLog{ + client:appendLog({ type = "$AddToPile", arg = move.specialName, arg2 = #move.ids, from = move.to, card = move.ids, - } + }, visible_data) elseif move.fromArea == Card.PlayerEquip then - client:appendLog{ + client:appendLog({ type = "$UninstallEquip", from = move.from, card = move.ids, - } + }, visible_data) elseif move.toArea == Card.Processing then if move.fromArea == Card.DrawPile and (move.moveReason == fk.ReasonPut or move.moveReason == fk.ReasonJustMove) then if hidden then - client:appendLog{ + client:appendLog({ type = "$ViewCardFromDrawPile", from = move.proposer, arg = #move.ids, - } + }, visible_data) else - client:appendLog{ + client:appendLog({ type = "$TurnOverCardFromDrawPile", from = move.proposer, card = move.ids, arg = #move.ids, - } + }, visible_data) client:setCardNote(move.ids, { type = "$$TurnOverCard", from = move.proposer, @@ -676,12 +635,12 @@ local function sendMoveCardLog(move) end elseif move.from and move.toArea == Card.DrawPile then msgtype = hidden and "$PutCard" or "$PutKnownCard" - client:appendLog{ + client:appendLog({ type = msgtype, from = move.from, card = move.ids, arg = #move.ids, - } + }, visible_data) client:setCardNote(move.ids, { type = "$$PutCard", from = move.from, @@ -689,27 +648,27 @@ local function sendMoveCardLog(move) elseif move.toArea == Card.DiscardPile then if move.moveReason == fk.ReasonDiscard then if move.proposer and move.proposer ~= move.from then - client:appendLog{ + client:appendLog({ type = "$DiscardOther", from = move.from, to = {move.proposer}, card = move.ids, arg = #move.ids, - } + }, visible_data) else - client:appendLog{ + client:appendLog({ type = "$DiscardCards", from = move.from, card = move.ids, arg = #move.ids, - } + }, visible_data) end elseif move.moveReason == fk.ReasonPutIntoDiscardPile then - client:appendLog{ + client:appendLog({ type = "$PutToDiscard", card = move.ids, arg = #move.ids, - } + }, visible_data) end -- elseif move.toArea == Card.Void then -- nop @@ -724,14 +683,23 @@ local function sendMoveCardLog(move) end end +---@param raw_moves CardsMoveStruct[] fk.client_callback["MoveCards"] = function(raw_moves) -- jsonData: CardsMoveStruct[] + ClientInstance:moveCards(raw_moves) + local visible_data = {} + for _, move in ipairs(raw_moves) do + for _, info in ipairs(move.moveInfo) do + local cid = info.cardId + visible_data[tostring(cid)] = Self:cardVisible(cid, move) + end + end local separated = separateMoves(raw_moves) - ClientInstance:moveCards(separated) local merged = mergeMoves(separated) - ClientInstance:notifyUI("MoveCards", merged) + visible_data.merged = merged + ClientInstance:notifyUI("MoveCards", visible_data) for _, move in ipairs(merged) do - sendMoveCardLog(move) + sendMoveCardLog(move, visible_data) end end @@ -861,6 +829,17 @@ fk.client_callback["AddSkill"] = function(data) updateLimitSkill(id, skill, target:usedSkillTimes(skill_name, Player.HistoryGame)) end +fk.client_callback["AskForSkillInvoke"] = function(data) + -- jsonData: [ string name, string prompt ] + + local h = Fk.request_handlers["AskForSkillInvoke"]:new(Self) + h.prompt = data[2] + h.change = {} + h:setup() + h.scene:notifyUI() + ClientInstance:notifyUI("AskForSkillInvoke", data) +end + fk.client_callback["AskForUseActiveSkill"] = function(data) -- jsonData: [ string skill_name, string prompt, bool cancelable. json extra_data ] local skill = Fk.skills[data[1]] @@ -868,16 +847,44 @@ fk.client_callback["AskForUseActiveSkill"] = function(data) skill._extra_data = extra_data Fk.currentResponseReason = extra_data.skillName + local h = Fk.request_handlers["AskForUseActiveSkill"]:new(Self) + h.skill_name = data[1] + h.prompt = data[2] + h.cancelable = data[3] + h.extra_data = data[4] + h.change = {} + h:setup() + h.scene:notifyUI() ClientInstance:notifyUI("AskForUseActiveSkill", data) end fk.client_callback["AskForUseCard"] = function(data) + -- jsonData: card, pattern, prompt, cancelable, {} Fk.currentResponsePattern = data[2] + local h = Fk.request_handlers["AskForUseCard"]:new(Self) + -- h.skill_name = data[1] (skill_name是给选中的视为技用的) + h.pattern = data[2] + h.prompt = data[3] + h.cancelable = data[4] + h.extra_data = data[5] + h.change = {} + h:setup() + h.scene:notifyUI() ClientInstance:notifyUI("AskForUseCard", data) end fk.client_callback["AskForResponseCard"] = function(data) + -- jsonData: card, pattern, prompt, cancelable, {} Fk.currentResponsePattern = data[2] + local h = Fk.request_handlers["AskForResponseCard"]:new(Self) + -- h.skill_name = data[1] (skill_name是给选中的视为技用的) + h.pattern = data[2] + h.prompt = data[3] + h.cancelable = data[4] + h.extra_data = data[5] + h.change = {} + h:setup() + h.scene:notifyUI() ClientInstance:notifyUI("AskForResponseCard", data) end @@ -894,7 +901,7 @@ fk.client_callback["SetPlayerMark"] = function(data) local spec = Fk.qml_marks[mtype] if spec then local text = spec.how_to_show(mark, value, p) - if text == "#hidden" then return end + if text == "#hidden" then data[3] = 0 end end end ClientInstance:notifyUI("SetPlayerMark", data) @@ -1006,10 +1013,11 @@ fk.client_callback["Heartbeat"] = function() end fk.client_callback["ChangeSelf"] = function(data) - local p = ClientInstance:getPlayerById(data.id) - p.player_cards[Player.Hand] = data.handcards - p.special_cards = data.special_cards - ClientInstance:notifyUI("ChangeSelf", data.id) + local pid = tonumber(data) + local c = ClientInstance + c.client:changeSelf(pid) -- for qml + Self = c:getPlayerById(pid) + ClientInstance:notifyUI("ChangeSelf", pid) end fk.client_callback["UpdateQuestSkillUI"] = function(data) @@ -1111,161 +1119,60 @@ end fk.client_callback["PrintCard"] = function(data) local n, s, num = table.unpack(data) - local cd = Fk:cloneCard(n, s, num) - Fk:_addPrintedCard(cd) + ClientInstance:printCard(n, s, num) end fk.client_callback["AddBuddy"] = function(data) local c = ClientInstance - local id, hand = table.unpack(data) + local fromid, id = table.unpack(data) + local from = c:getPlayerById(fromid) local to = c:getPlayerById(id) - Self:addBuddy(to) - to.player_cards[Player.Hand] = hand + from:addBuddy(to) end fk.client_callback["RmBuddy"] = function(data) local c = ClientInstance - local id = data + local fromid, id = table.unpack(data) + local from = c:getPlayerById(fromid) local to = c:getPlayerById(id) - Self:removeBuddy(to) - to.player_cards[Player.Hand] = table.map(to.player_cards, function() return -1 end) -end - -local function loadPlayerSummary(pdata) - local f = fk.client_callback["PropertyUpdate"] - local id = pdata.d[1] - local properties = { - "general", "deputyGeneral", "maxHp", "hp", "shield", "gender", "kingdom", - "dead", "role", "rest", "seat", "phase", "faceup", "chained", - "sealedSlots", - } - - for _, k in ipairs(properties) do - if pdata.p[k] ~= nil then - f{ id, k, pdata.p[k] } - end - end - - local card_moves = {} - local cards = pdata.c - if #cards[Player.Hand] ~= 0 then - local info = {} - for _, i in ipairs(cards[Player.Hand]) do - table.insert(info, { cardId = i, fromArea = Card.DrawPile }) - end - local move = { moveInfo = info, to = id, toArea = Card.PlayerHand } - table.insert(card_moves, move) - end - if #cards[Player.Equip] ~= 0 then - local info = {} - for _, i in ipairs(cards[Player.Equip]) do - table.insert(info, { cardId = i, fromArea = Card.DrawPile }) - end - local move = { moveInfo = info, to = id, toArea = Card.PlayerEquip } - table.insert(card_moves, move) - end - if #cards[Player.Judge] ~= 0 then - local info = {} - for _, i in ipairs(cards[Player.Judge]) do - table.insert(info, { cardId = i, fromArea = Card.DrawPile }) - end - local move = { moveInfo = info, to = id, toArea = Card.PlayerJudge } - table.insert(card_moves, move) - end - - for k, v in pairs(pdata.sc) do - local info = {} - for _, i in ipairs(v) do - table.insert(info, { cardId = i, fromArea = Card.DrawPile }) - end - local move = { - moveInfo = info, - to = id, - toArea = Card.PlayerSpecial, - specialName = k, - moveVisible = true, - } - table.insert(card_moves, move) - end - - if #card_moves > 0 then - -- TODO: visibility - fk.client_callback["MoveCards"](card_moves) - end - - f = fk.client_callback["SetPlayerMark"] - for k, v in pairs(pdata.m) do - f{ id, k, v } - end - - f = fk.client_callback["AddSkill"] - for _, v in pairs(pdata.s) do - f{ id, v } - end - - f = fk.client_callback["AddCardUseHistory"] - for k, v in pairs(pdata.ch) do - if v[1] > 0 then - f{ k, v[1] } - end - end - - f = fk.client_callback["SetSkillUseHistory"] - for k, v in pairs(pdata.sh) do - if v[4] > 0 then - f{ id, k, v[1], 1 } - f{ id, k, v[2], 2 } - f{ id, k, v[3], 3 } - f{ id, k, v[4], 4 } - end - end + from:removeBuddy(to) end local function loadRoomSummary(data) - local players = data.p + local players = data.players fk.client_callback["StartGame"]("") for _, pid in ipairs(data.circle) do if pid ~= data.you then - fk.client_callback["AddPlayer"](players[tostring(pid)].d) + fk.client_callback["AddPlayer"](players[tostring(pid)].setup_data) end end fk.client_callback["ArrangeSeats"](data.circle) - for _, d in ipairs(data.pc) do - local cd = Fk:cloneCard(table.unpack(d)) - Fk:_addPrintedCard(cd) - end + ClientInstance:loadJsonObject(data) -- 此处已同步全部数据 剩下就是更新UI - for cid, marks in pairs(data.cm) do - for k, v in pairs(marks) do - Fk:getCardById(tonumber(cid)):setMark(k, v) - ClientInstance:notifyUI("UpdateCard", cid) + for k, v in pairs(ClientInstance.banners) do + if k[1] == "@" then + ClientInstance:notifyUI("SetBanner", { k, v }) end end - for k, v in pairs(data.b) do - fk.client_callback["SetBanner"]{ k, v } - end + for _, p in ipairs(ClientInstance.players) do p:sendDataToUI() end - for _, pid in ipairs(data.circle) do - local pdata = data.p[tostring(pid)] - loadPlayerSummary(pdata) - end - - ClientInstance:notifyUI("UpdateDrawPile", data.dp) - ClientInstance:notifyUI("UpdateRoundNum", data.rnd) + ClientInstance:notifyUI("UpdateDrawPile", #ClientInstance.draw_pile) + ClientInstance:notifyUI("UpdateRoundNum", data.round_count) end fk.client_callback["Reconnect"] = function(data) - local players = data.p - local setup_data = players[tostring(data.you)].d + local players = data.players + + local setup_data = players[tostring(data.you)].setup_data setup(setup_data[1], setup_data[2], setup_data[3]) fk.client_callback["AddTotalGameTime"]{ setup_data[1], setup_data[5] } - local enter_room_data = data.d + local enter_room_data = { data.timeout, data.settings } table.insert(enter_room_data, 1, #data.circle) fk.client_callback["EnterLobby"]("") fk.client_callback["EnterRoom"](enter_room_data) @@ -1274,19 +1181,28 @@ fk.client_callback["Reconnect"] = function(data) end fk.client_callback["Observe"] = function(data) - local players = data.p + local players = data.players - local setup_data = players[tostring(data.you)].d + local setup_data = players[tostring(data.you)].setup_data setup(setup_data[1], setup_data[2], setup_data[3]) - local enter_room_data = data.d + local enter_room_data = { data.timeout, data.settings } table.insert(enter_room_data, 1, #data.circle) fk.client_callback["EnterRoom"](enter_room_data) - fk.client_callback["StartGame"]("") loadRoomSummary(data) end +fk.client_callback["PrepareDrawPile"] = function(data) + local seed = tonumber(data) + ClientInstance:prepareDrawPile(seed) +end + +fk.client_callback["ShuffleDrawPile"] = function(data) + local seed = tonumber(data) + ClientInstance:shuffleDrawPile(seed) +end + -- Create ClientInstance (used by Lua) ClientInstance = Client:new() dofile "lua/client/client_util.lua" diff --git a/lua/client/client_util.lua b/lua/client/client_util.lua index c6f75933..a898b173 100644 --- a/lua/client/client_util.lua +++ b/lua/client/client_util.lua @@ -37,31 +37,13 @@ function GetGeneralDetail(name) deputyMaxHp = general.deputyMaxHpAdjustedValue, gender = general.gender, skill = {}, - related_skill = {}, companions = general.companions } - for _, s in ipairs(general.skills) do + for _, s in ipairs(general.all_skills) do table.insert(ret.skill, { - name = s.name, - description = Fk:getDescription(s.name) - }) - end - for _, s in ipairs(general.other_skills) do - table.insert(ret.skill, { - name = s, - description = Fk:getDescription(s) - }) - end - for _, s in ipairs(general.related_skills) do - table.insert(ret.related_skill, { - name = s.name, - description = Fk:getDescription(s.name) - }) - end - for _, s in ipairs(general.related_other_skills) do - table.insert(ret.related_skill, { - name = s, - description = Fk:getDescription(s) + name = s[1], + description = Fk:getDescription(s[1]), + is_related_skill = s[2], }) end for _, g in pairs(Fk.generals) do @@ -114,7 +96,8 @@ function GetCardData(id, virtualCardForm) color = card:getColorString(), mark = mark, type = card.type, - subtype = cardSubtypeStrings[card.sub_type] + subtype = cardSubtypeStrings[card.sub_type], + known = Self:cardVisible(id) } if card.skillName ~= "" then local orig = Fk:getCardById(id, true) @@ -352,6 +335,49 @@ function CanUseCardToTarget(card, to_select, selected, extra_data_str) return ret end +---@param card string | integer +---@param to_select integer @ id of the target +---@param selected integer[] @ ids of selected targets +---@param selectable bool +---@param extra_data_str string @ extra data +function GetUseCardTargetTip(card, to_select, selected, selectable, extra_data_str) + local extra_data = extra_data_str == "" and nil or json.decode(extra_data_str) + local c ---@type Card + local selected_cards + if type(card) == "number" then + c = Fk:getCardById(card) + selected_cards = {card} + else + local t = json.decode(card) + return ActiveTargetTip(t.skill, to_select, selected, t.subcards, selectable, extra_data) + end + + local ret + local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or Util.DummyTable + for _, skill in ipairs(status_skills) do + ret = ret or {} + if #ret > 4 then + return ret + end + + local tip = skill:getTargetTip(Self, to_select, selected, selected_cards, c, selectable, extra_data) + if type(tip) == "string" then + table.insert(ret, { content = ret, type = "normal" }) + elseif type(tip) == "table" then + table.insertTable(ret, tip) + end + end + + ret = ret or {} + local tip = c.skill:targetTip(to_select, selected, selected_cards, c, selectable, extra_data) + if type(tip) == "string" then + table.insert(ret, { content = ret, type = "normal" }) + elseif type(tip) == "table" then + table.insertTable(ret, tip) + end + return ret +end + ---@param card string | integer ---@param to_select integer @ id of a card not selected ---@param selected_targets integer[] @ ids of selected players @@ -430,6 +456,18 @@ function GetSkillData(skill_name) } end +function GetSkillStatus(skill_name) + local skill = Fk.skills[skill_name] + local locked = not skill:isEffectable(Self) + if not locked and type(Self:getMark(MarkEnum.InvalidSkills)) == "table" and table.contains(Self:getMark(MarkEnum.InvalidSkills), skill_name) then + locked = true + end + return { + locked = locked, ---@type boolean + times = skill:getTimes() + } +end + function ActiveCanUse(skill_name, extra_data_str) local extra_data = extra_data_str == "" and nil or json.decode(extra_data_str) local skill = Fk.skills[skill_name] @@ -505,6 +543,46 @@ function ActiveTargetFilter(skill_name, to_select, selected, selected_cards, ext return ret end +function ActiveTargetTip(skill_name, to_select, selected, selected_cards, selectable, extra_data) + local skill = Fk.skills[skill_name] + local ret + if skill then + if skill:isInstanceOf(ActiveSkill) then + ret = skill:targetTip(to_select, selected, selected_cards, nil, selectable) + if type(ret) == "string" then + ret = { { content = ret, type = "normal" } } + end + elseif skill:isInstanceOf(ViewAsSkill) then + local card = skill:viewAs(selected_cards) + if card then + local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or Util.DummyTable + for _, skill in ipairs(status_skills) do + ret = ret or {} + if #ret > 4 then + return ret + end + + local tip = skill:getTargetTip(Self, to_select, selected, selected_cards, card, selectable, extra_data) + if type(tip) == "string" then + table.insert(ret, { content = ret, type = "normal" }) + elseif type(tip) == "table" then + table.insertTable(ret, tip) + end + end + + ret = ret or {} + local tip = card.skill:targetTip(to_select, selected, selected_cards, card, selectable, extra_data) + if type(tip) == "string" then + table.insert(ret, { content = ret, type = "normal" }) + elseif type(tip) == "table" then + table.insertTable(ret, tip) + end + end + end + end + return ret +end + function ActiveFeasible(skill_name, selected, selected_cards) local skill = Fk.skills[skill_name] local ret = false @@ -654,12 +732,6 @@ function SetInteractionDataOfSkill(skill_name, data) end end -function ChangeSelf(pid) - local c = ClientInstance - c.client:changeSelf(pid) -- for qml - Self = c:getPlayerById(pid) -end - function GetPlayerHandcards(pid) local c = ClientInstance local p = c:getPlayerById(pid) @@ -767,6 +839,10 @@ function GetCardProhibitReason(cid, method, pattern) end end +function CanSortHandcards(pid) + return ClientInstance:getPlayerById(pid):getMark(MarkEnum.SortProhibited) == 0 +end + function PoxiPrompt(poxi_type, data, extra_data) local poxi = Fk.poxi_methods[poxi_type] if not poxi or not poxi.prompt then return "" end @@ -810,4 +886,91 @@ function ReloadPackage(path) Fk:reloadPackage(path) end +function GetPendingSkill() + local h = ClientInstance.current_request_handler + local reqActive = Fk.request_handlers["AskForUseActiveSkill"] + return h and h:isInstanceOf(reqActive) and + (h.selected_card == nil and h.skill_name) or "" +end + +function RevertSelection() + local h = ClientInstance.current_request_handler ---@type ReqActiveSkill + local reqActive = Fk.request_handlers["AskForUseActiveSkill"] + if not (h and h:isInstanceOf(reqActive) and h.pendings) then return end + h.change = {} + -- 1. 取消选中所有已选 2. 尝试选中所有之前未选的牌 + local unselectData = { selected = false } + local selectData = { selected = true } + local to_select = {} + for cid, cardItem in pairs(h.scene:getAllItems("CardItem")) do + if table.contains(h.pendings, cid) then + h:selectCard(cid, unselectData) + else + table.insert(to_select, cardItem) + end + end + for _, cardItem in ipairs(to_select) do + if cardItem.enabled then + h:selectCard(cardItem.id, selectData) + end + end + h.scene:notifyUI() +end + +function UpdateRequestUI(elemType, id, action, data) + local h = ClientInstance.current_request_handler + h.change = {} + local finish = h:update(elemType, id, action, data) + if not finish then + h.scene:notifyUI() + else + h:_finish() + end +end + +function FinishRequestUI() + local h = ClientInstance.current_request_handler + if h then + h:_finish() + end +end + +-- TODO 传参带上cardMoveData... +function CardVisibility(cardId, move) + local player = Self + local card = Fk:getCardById(cardId) + if not card then return false end + return player:cardVisible(cardId, move) +end + +function RoleVisibility(targetId) + local player = Self + local target = ClientInstance:getPlayerById(targetId) + if not target then return false end + return player:roleVisible(target) +end + +function IsMyBuddy(me, other) + local from = ClientInstance:getPlayerById(me) + local to = ClientInstance:getPlayerById(other) + return from and to and from:isBuddy(to) +end + +-- special_name 为nil时是手牌 +function HasVisibleCard(me, other, special_name) + local from = ClientInstance:getPlayerById(me) + local to = ClientInstance:getPlayerById(other) + if not (from and to) then return false end + local ids + if not special_name then ids = to:getCardIds("h") + else ids = to:getPile(special_name) end + + for _, id in ipairs(ids) do + if from:cardVisible(id) then + return true + end + end + return false +end + dofile "lua/client/i18n/init.lua" diff --git a/lua/client/clientplayer.lua b/lua/client/clientplayer.lua index 3209bf87..0ce5e083 100644 --- a/lua/client/clientplayer.lua +++ b/lua/client/clientplayer.lua @@ -2,16 +2,84 @@ ---@class ClientPlayer: Player ---@field public player fk.Player ----@field public known_cards integer[] ----@field public global_known_cards integer[] local ClientPlayer = Player:subclass("ClientPlayer") function ClientPlayer:initialize(cp) Player.initialize(self) self.id = cp:getId() self.player = cp - self.known_cards = {} -- you know he/she have this card, but not shown - self.global_known_cards = {} -- card that visible to all players +end + +local function fillMoveData(card_moves, visible_data, self, area, specialName) + local cards = self.player_cards + local ids = cards[area] + if specialName then ids = ids[specialName] end + if #ids ~= 0 then + for _, id in ipairs(ids) do + visible_data[tostring(id)] = Self:cardVisible(id) + end + local move = { + ids = ids, + to = self.id, + fromArea = Card.DrawPile, + toArea = area, + specialName = specialName, + } + table.insert(card_moves, move) + end +end + +-- 仅用于断线重连或者旁观时:将数据同步到qml界面中 +function ClientPlayer:sendDataToUI() + local c = ClientInstance + local id = self.id + for _, k in ipairs(self.property_keys) do + c:notifyUI("PropertyUpdate", { id, k, self[k] }) + end + + local card_moves = {} + local visible_data = {} + for _, area in ipairs { Player.Hand, Player.Equip, Player.Judge } do + fillMoveData(card_moves, visible_data, self, area) + end + for name in pairs(self.special_cards) do + fillMoveData(card_moves, visible_data, self, Card.PlayerSpecial, name) + end + if #card_moves > 0 then + visible_data.merged = card_moves + c:notifyUI("MoveCards", visible_data) + end + + for mark, value in pairs(self.mark) do + if mark[1] == "@" then + if mark:startsWith("@[") and mark:find(']') then + local close = mark:find(']') + local mtype = mark:sub(3, close - 1) + local spec = Fk.qml_marks[mtype] + if spec then + local text = spec.how_to_show(mark, value, p) + if text == "#hidden" then value = 0 end + end + end + c:notifyUI("SetPlayerMark", { id, mark, value }) + end + end + + for _, skill in ipairs(self.player_skills) do + if skill.visible then + c:notifyUI("AddSkill", { id, skill.name }) + end + end + + local f = fk.client_callback["SetSkillUseHistory"] + for k, v in pairs(self.skillUsedHistory) do + if v[4] > 0 then + f{ id, k, v[1], 1 } + f{ id, k, v[2], 2 } + f{ id, k, v[3], 3 } + f{ id, k, v[4], 4 } + end + end end return ClientPlayer diff --git a/lua/client/i18n/en_US.lua b/lua/client/i18n/en_US.lua index 070b2aee..766a39ad 100644 --- a/lua/client/i18n/en_US.lua +++ b/lua/client/i18n/en_US.lua @@ -34,6 +34,8 @@ Fk:loadTranslationTable({ -- ["Refresh Room List"] = "刷新房间列表", ["Disable Extension"] = "Please ignore this checkbox", + -- ["Filter"] = "筛选", + -- ["Room ID"] = "房间ID", -- ["Create Room"] = "创建房间", -- ["Room Name"] = "房间名字", ["$RoomName"] = "%1's room", @@ -42,9 +44,14 @@ Fk:loadTranslationTable({ -- ["No enough generals"] = "可用武将不足!", ["Operation timeout"] = "Operation timeout (sec)", ["Luck Card Times"] = "Luck card count", - ["Has Password"] = "(PW) ", + -- ["Has Password"] = "有密码", + -- ["No Password"] = "无密码", -- ["Room Password"] = "房间密码", -- ["Please input room's password"] = "请输入房间的密码", + -- ["Room Fullness"] = "房间满员", + -- ["Full"] = "已满", + -- ["Not Full"] = "未满", + -- ["Room Capacity"] = "人数上限", ["Add Robot"] = "Add robot", ["Start Game"] = "Start game", -- ["Ready"] = "准备", @@ -89,6 +96,7 @@ Fk:loadTranslationTable({ ["$OnlineInfo"] = "Lobby: %1, Online: %2", + -- ["Overview"] = "一览", ["Generals Overview"] = "Characters", ["Cards Overview"] = "Cards", ["Special card skills:"] = "Special use method:", @@ -97,7 +105,7 @@ Fk:loadTranslationTable({ -- ["Female Audio"] = "女性音效", -- ["Equip Effect Audio"] = "效果音效", -- ["Equip Use Audio"] = "使用音效", - ["Scenarios Overview"] = "Game modes", + ["Modes Overview"] = "Game modes", -- ["Replay"] = "录像", -- ["Replay Manager"] = "来欣赏潇洒的录像吧!", -- ["Replay from File"] = "从文件打开", @@ -142,6 +150,8 @@ Fk:loadTranslationTable({ -- ["Quit"] = "退出", ["BanGeneral"] = "Ban", ["ResumeGeneral"] = "Unban", + -- ["Enable"] = "启用", + -- ["Prohibit"] = "禁", ["BanPackage"] = "Ban packages", ["$BanPkgHelp"] = "Banning packages", ["$BanCharaHelp"] = "Banning characters", @@ -153,6 +163,11 @@ Fk:loadTranslationTable({ ["Designer"] = "Designer: ", ["Voice Actor"] = "Voice Actor: ", ["Illustrator"] = "Illustrator: ", + -- ["Hidden General"] = "隐藏武将", + ["Audio Code Copy Success"] = "Audio code has been copied to your clipboard", + ["Audio Text Copy Success"] = "Audio text has been copied to your clipboard", + -- ["Copy Audio Code"] = "复制语音代码", + -- ["Copy Audio Text"] = "复制语音文本", ["$WelcomeToLobby"] = "Welcome to FreeKill lobby!", ["GameMode"] = "Game mode: ", @@ -253,8 +268,12 @@ Fk:loadTranslationTable({ -- ["Trust"] = "托管", ["Sort Cards"] = "Sort", + ["Sort by Type"] = "by Type", + ["Sort by Number"] = "by Num", + ["Sort by Suit"] = "by Suit", -- ["Chat"] = "聊天", ["Log"] = "Game Log", + -- ["Return to Bottom"] = "回到底部", -- ["Trusting ..."] = "托管中 ...", -- ["Observing ..."] = "旁观中 ...", @@ -404,6 +423,7 @@ Fk:loadTranslationTable({ -- skill ["#InvokeSkill"] = '%from used skill "%arg"', + ["#InvokeSkillTo"] = '%from used skill "%arg" to %to', -- judge ["#StartJudgeReason"] = "%from started a judgement (%arg)", diff --git a/lua/client/i18n/zh_CN.lua b/lua/client/i18n/zh_CN.lua index f6ff38a7..7e55c32c 100644 --- a/lua/client/i18n/zh_CN.lua +++ b/lua/client/i18n/zh_CN.lua @@ -29,6 +29,7 @@ Fk:loadTranslationTable{ ["Hide unselectable cards"] = "下移不可选卡牌", ["Hide observer chatter"] = "屏蔽旁观者聊天", ["Rotate table card"] = "处理区的牌随机旋转", + ["Hide presents"] = "屏蔽送花砸蛋", ["Ban General Settings"] = "禁将", ["Set as Avatar"] = "设为头像", ["Search"] = "搜索", @@ -37,7 +38,9 @@ Fk:loadTranslationTable{ ["Refresh Room List"] = "刷新房间列表 (%1个房间)", ["Disable Extension"] = "禁用Lua拓展 (重启后生效)", + ["Filter"] = "筛选", ["Create Room"] = "创建房间", + ["Room ID"] = "房间ID", ["Room Name"] = "房间名字", ["$RoomName"] = "%1的房间", ["Player num"] = "玩家数目", @@ -45,9 +48,14 @@ Fk:loadTranslationTable{ ["No enough generals"] = "可用武将不足!", ["Operation timeout"] = "操作时长(秒)", ["Luck Card Times"] = "手气卡次数", - ["Has Password"] = "(有密码)", + ["Has Password"] = "有密码", + ["No Password"] = "无密码", ["Room Password"] = "房间密码", ["Please input room's password"] = "请输入房间的密码", + ["Room Fullness"] = "房间满员", + ["Full"] = "已满", + ["Not Full"] = "未满", + ["Room Capacity"] = "人数上限", ["Add Robot"] = "添加机器人", ["Start Game"] = "开始游戏", ["Ready"] = "准备", @@ -97,6 +105,7 @@ Fk:loadTranslationTable{ ["$OnlineInfo"] = "大厅人数:%1,总在线人数:%2", + ["Overview"] = "一览", ["Generals Overview"] = "武将一览", ["Cards Overview"] = "卡牌一览", ["Special card skills:"] = "卡牌的特殊用法:", @@ -105,7 +114,7 @@ Fk:loadTranslationTable{ ["Female Audio"] = "女性音效", ["Equip Effect Audio"] = "效果音效", ["Equip Use Audio"] = "使用音效", - ["Scenarios Overview"] = "玩法一览", + ["Modes Overview"] = "玩法一览", ["Replay"] = "录像", ["Replay Manager"] = "来欣赏潇洒的录像吧!", ["Replay from File"] = "从文件打开", @@ -121,6 +130,8 @@ Fk:loadTranslationTable{ 项目链接: https://github.com/Notify-ctrl/FreeKill +使用手册: https://fkbook-all-in-one.readthedocs.io + --- 作者: Notify Ho-spair @@ -196,6 +207,8 @@ FreeKill使用的是libgit2的C API,与此同时使用Git完成拓展包的下 -- ["Quit"] = "退出", ["BanGeneral"] = "禁将", ["ResumeGeneral"] = "解禁", + ["Enable"] = "启用", + ["Prohibit"] = "禁", ["BanPackage"] = "禁拓展包", ["$BanPkgHelp"] = "正在禁用拓展包", ["$BanCharaHelp"] = "正在禁用武将", @@ -207,6 +220,11 @@ FreeKill使用的是libgit2的C API,与此同时使用Git完成拓展包的下 ["Designer"] = "设计:", ["Voice Actor"] = "配音:", ["Illustrator"] = "画师:", + ["Hidden General"] = "隐藏武将", + ["Audio Code Copy Success"] = "语音代码已复制到剪贴板", + ["Audio Text Copy Success"] = "语音文本已复制到剪贴板", + ["Copy Audio Code"] = "复制语音代码", + ["Copy Audio Text"] = "复制语音文本", ["$WelcomeToLobby"] = "欢迎进入新月杀游戏大厅!", ["GameMode"] = "游戏模式:", @@ -229,7 +247,7 @@ FreeKill使用的是libgit2的C API,与此同时使用Git完成拓展包的下 ["#PlayCard"] = "出牌阶段,请使用一张牌", ["#AskForGeneral"] = "请选择 1 名武将", ["#AskForSkillInvoke"] = "你想发动〖%1〗吗?", - ["#AskForLuckCard"] = "你想使用手气卡吗?还可以使用 %1 次,剩余手气卡∞张", + ["#AskForLuckCard"] = "你想使用手气卡吗?还可以使用 %arg 次,剩余手气卡∞张", ["AskForLuckCard"] = "手气卡", ["#AskForChoice"] = "%1:请选择", ["#AskForChoices"] = "%1:请选择", @@ -307,8 +325,12 @@ FreeKill使用的是libgit2的C API,与此同时使用Git完成拓展包的下 ["Trust"] = "托管", ["Sort Cards"] = "牌序", + ["Sort by Type"] = "按类型", + ["Sort by Number"] = "按点数", + ["Sort by Suit"] = "按花色", ["Chat"] = "聊天", ["Log"] = "战报", + ["Return to Bottom"] = "回到底部", ["Trusting ..."] = "托管中 ...", ["Observing ..."] = "旁观中 ...", ["Resting, don't leave!"] = "稍后你可返回战局,不要离开", @@ -360,16 +382,20 @@ Fk:loadTranslationTable{ ["hp_lost"] = "失去体力", ["lose_hp"] = "失去体力", + ["phase_roundstart"] = "回合开始", ["phase_start"] = "准备阶段", ["phase_judge"] = "判定阶段", ["phase_draw"] = "摸牌阶段", ["phase_play"] = "出牌阶段", ["phase_discard"] = "弃牌阶段", ["phase_finish"] = "结束阶段", + ["phase_notactive"] = "回合外", + ["phase_phasenone"] = "临时阶段", ["chained"] = "横置", ["un-chained"] = "重置", ["reset-general"] = "复原", + ["reset"] = "复原武将牌", ["yang"] = "阳", ["yin"] = "阴", @@ -396,6 +422,7 @@ Fk:loadTranslationTable{ ["Distance"] = "距离", ["Judge"] = "判定", ["Retrial"] = "改判", + ["Pindian"] = "拼点", ["_sealed"] = "废除", ["weapon_sealed"] = "武器栏废除", @@ -408,6 +435,8 @@ Fk:loadTranslationTable{ ["DefensiveRideSlot"] = "防御坐骑栏", ["TreasureSlot"] = "宝物栏", ["JudgeSlot"] = "判定区", + + ["skill"] = "技能", } -- related to sendLog @@ -474,9 +503,12 @@ Fk:loadTranslationTable{ ["#ResponsePlayV0Card"] = "%from 打出了 %arg", ["#FilterCard"] = "由于 %arg 的效果,与 %from 相关的 %arg2 被视为了 %arg3", + ["#AddTargetsBySkill"] = "用于 %arg 的效果,%from 使用的 %arg2 增加了目标 %to", + ["#RemoveTargetsBySkill"] = "用于 %arg 的效果,%from 使用的 %arg2 取消了目标 %to", -- skill ["#InvokeSkill"] = "%from 发动了〖%arg〗", + ["#InvokeSkillTo"] = "%from 对 %to 发动了〖%arg〗", -- judge ["#StartJudgeReason"] = "%from 开始了 %arg 的判定", @@ -488,6 +520,7 @@ Fk:loadTranslationTable{ ["#TurnOver"] = "%from 将武将牌翻面,现在是 %arg", ["face_up"] = "正面朝上", ["face_down"] = "背面朝上", + ["turnOver"] = "翻面", -- damage, heal and lose HP ["#Damage"] = "%to 对 %from 造成了 %arg 点 %arg2 伤害", diff --git a/lua/core/abstract_room.lua b/lua/core/abstract_room.lua deleted file mode 100644 index b6685d19..00000000 --- a/lua/core/abstract_room.lua +++ /dev/null @@ -1,52 +0,0 @@ --- 作Room和Client的基类,这二者有不少共通之处 ----@class AbstractRoom : Object ----@fiele public players Player[] @ 房内参战角色们 ----@field public alive_players Player[] @ 所有存活玩家的数组 ----@field public observers Player[] @ 看戏的 ----@field public current Player @ 当前行动者 ----@field public status_skills table @ 这个房间中含有的状态技列表 ----@field public filtered_cards table @ 见于Engine,其实在这 ----@field public printed_cards table @ 同上 ----@field public skill_costs table @ 用来存skill.cost_data ----@field public card_marks table @ 用来存实体卡的card.mark ----@field public banners table @ 全局mark -local AbstractRoom = class("AbstractRoom") - -function AbstractRoom:initialize() - self.players = {} - self.alive_players = {} - self.observers = {} - self.current = nil - - self.status_skills = {} - for class, skills in pairs(Fk.global_status_skill) do - self.status_skills[class] = {table.unpack(skills)} - end - - self.filtered_cards = {} - self.printed_cards = {} - self.skill_costs = {} - self.card_marks = {} - self.banners = {} -end - --- 仅供注释,其余空函数一样 ----@param id integer ----@return Player? -function AbstractRoom:getPlayerById(id) end - ---- 获取一张牌所处的区域。 ----@param cardId integer | Card @ 要获得区域的那张牌,可以是Card或者一个id ----@return CardArea @ 这张牌的区域 -function AbstractRoom:getCardArea(cardId) return Card.Unknown end - -function AbstractRoom:setBanner(name, value) - if value == 0 then value = nil end - self.banners[name] = value -end - -function AbstractRoom:getBanner(name) - return self.banners[name] -end - -return AbstractRoom diff --git a/lua/core/card.lua b/lua/core/card.lua index 10faad65..9d6a026b 100644 --- a/lua/core/card.lua +++ b/lua/core/card.lua @@ -20,7 +20,7 @@ ---@field public skillName string @ 虚拟牌的技能名 for virtual cards ---@field private _skillName string ---@field public skillNames string[] @ 虚拟牌的技能名们(一张虚拟牌可能有多个技能名,如芳魂、龙胆、朱雀羽扇) ----@field public skill Skill @ 技能(用于实现卡牌效果) +---@field public skill ActiveSkill @ 技能(用于实现卡牌效果) ---@field public special_skills? string[] @ 衍生技能,如重铸 ---@field public is_damage_card boolean @ 是否为会造成伤害的牌 ---@field public multiple_targets boolean @ 是否为指定多个目标的牌 @@ -514,4 +514,12 @@ function Card.static:getIdList(c) return ret end +--- 获得卡牌的标记并初始化为表 +---@param mark string @ 标记 +---@return table +function Card:getTableMark(mark) + local ret = self:getMark(mark) + return type(ret) == "table" and ret or {} +end + return Card diff --git a/lua/core/engine.lua b/lua/core/engine.lua index 17044b41..19c1aa71 100644 --- a/lua/core/engine.lua +++ b/lua/core/engine.lua @@ -29,10 +29,13 @@ ---@field public printed_cards table @ 被某些房间现场打印的卡牌,id都是负数且从-2开始 ---@field private kingdoms string[] @ 总势力 ---@field private kingdom_map table @ 势力映射表 +---@field private damage_nature table @ 伤害映射表 ---@field private _custom_events any[] @ 自定义事件列表 ---@field public poxi_methods table @ “魄袭”框操作方法表 ---@field public qml_marks table @ 自定义Qml标记的表 ---@field public mini_games table @ 自定义多人交互表 +---@field public request_handlers table @ 请求处理程序 +---@field public target_tips table @ 选择目标提示对应表 local Engine = class("Engine") --- Engine的构造函数。 @@ -69,13 +72,18 @@ function Engine:initialize() self.game_mode_disabled = {} self.kingdoms = {} self.kingdom_map = {} + self.damage_nature = { [fk.NormalDamage] = { "normal_damage", false } } self._custom_events = {} self.poxi_methods = {} self.qml_marks = {} self.mini_games = {} + self.request_handlers = {} + self.target_tips = {} self:loadPackages() + self:setLords() self:loadDisabled() + self:loadRequestHandlers() self:addSkills(AuxSkills) end @@ -201,15 +209,14 @@ end --- 标包和标准卡牌包比较特殊,它们永远会在第一个加载。 ---@return nil function Engine:loadPackages() - local new_core = false if FileIO.pwd():endsWith("packages/freekill-core") then - new_core = true + UsingNewCore = true FileIO.cd("../..") end local directories = FileIO.ls("packages") -- load standard & standard_cards first - if new_core then + if UsingNewCore then self:loadPackage(require("packages.freekill-core.standard")) self:loadPackage(require("packages.freekill-core.standard_cards")) self:loadPackage(require("packages.freekill-core.maneuvering")) @@ -248,7 +255,7 @@ function Engine:loadPackages() end end - if new_core then + if UsingNewCore then FileIO.cd("packages/freekill-core") end end @@ -269,6 +276,15 @@ function Engine:loadDisabled() end end +--- 载入响应事件 +function Engine:loadRequestHandlers() + self.request_handlers["AskForSkillInvoke"] = require 'core.request_type.invoke' + self.request_handlers["AskForUseActiveSkill"] = require 'core.request_type.active_skill' + self.request_handlers["AskForResponseCard"] = require 'core.request_type.response_card' + self.request_handlers["AskForUseCard"] = require 'core.request_type.use_card' + self.request_handlers["PlayCard"] = require 'core.request_type.play_card' +end + --- 向翻译表中加载新的翻译表。 ---@param t table @ 要加载的翻译表,这是一个 原文 --> 译文 的键值对表 ---@param lang? string @ 目标语言,默认为zh_CN @@ -349,9 +365,9 @@ function Engine:addGeneral(general) table.insert(self.same_generals[tName], general.name) end - if table.find(general.skills, function(s) return s.lordSkill end) then - table.insert(self.lords, general.name) - end + -- if table.find(general.skills, function(s) return s.lordSkill end) then + -- table.insert(self.lords, general.name) + -- end end --- 加载一系列武将。 @@ -363,6 +379,19 @@ function Engine:addGenerals(generals) end end +--- 为所有武将加载主公技和主公判定 +function Engine:setLords() + for _, general in pairs(self.generals) do + local other_skills = table.map(general.other_skills, Util.Name2SkillMapper) + local skills = table.connect(general.skills, other_skills) + for _, skill in ipairs(skills) do + if skill.lordSkill then + table.insert(self.lords, general.name) + end + end + end +end + --- 为一个势力添加势力映射 --- --- 这意味着原势力登场时必须改变为添加的几个势力之一(须存在) @@ -387,6 +416,50 @@ function Engine:getKingdomMap(kingdom) return ret end +--- 注册一个伤害 +---@param nature string | number @ 伤害ID +---@param name string @ 属性伤害名 +---@param can_chain bool @ 是否可传导 +function Engine:addDamageNature(nature, name, can_chain) + assert(table.contains({ "string", "number" }, type(nature)), "Must use string or number as nature!") + assert(type(name) == "string", "Must use string as this damage nature's name!") + if can_chain == nil then can_chain = true end + self.damage_nature[nature] = { name, can_chain } +end + +--- 返回伤害列表 +---@return table @ 具体信息(伤害ID => {伤害名,是否可传导}) +function Engine:getDamageNatures() + local ret = {} + for k, v in pairs(self.damage_nature) do + ret[k] = v + end + return ret +end + +--- 由伤害ID获得伤害属性 +---@param nature string | number @ 伤害ID +---@return table @ 具体信息({伤害名,是否可传导}),若不存在则为空 +function Engine:getDamageNature(nature) + return self.damage_nature[nature] +end + +--- 由伤害ID获得伤害名 +---@param nature string | number @ 伤害ID +---@return string @ 伤害名 +function Engine:getDamageNatureName(nature) + local ret = self:getDamageNature(nature) + return ret and ret[1] or "" +end + +--- 判断一种伤害是否可传导 +---@param nature string | number @ 伤害ID +---@return bool +function Engine:canChain(nature) + local ret = self:getDamageNature(nature) + return ret and ret[2] +end + --- 判断一个武将是否在本房间可用。 ---@param g string @ 武将名 function Engine:canUseGeneral(g) @@ -504,6 +577,15 @@ function Engine:addMiniGame(spec) self.mini_games[spec.name] = spec end +---@param spec TargetTipSpec +function Engine:addTargetTip(spec) + assert(type(spec.name) == "string") + if self.target_tips[spec.name] then + fk.qCritical("Warning: duplicated target tip type " .. spec.name) + end + self.target_tips[spec.name] = spec +end + --- 从已经开启的拓展包中,随机选出若干名武将。 --- --- 对于同名武将不会重复选取。 @@ -595,72 +677,7 @@ end ---@param player Player @ 和这张牌扯上关系的那名玩家 ---@param data any @ 随意,目前只用到JudgeStruct,为了影响判定牌 function Engine:filterCard(id, player, data) - if player == nil then - self.filtered_cards[id] = nil - return - end - - local card = self:getCardById(id, true) - local filters = self:currentRoom().status_skills[FilterSkill] or Util.DummyTable - - if #filters == 0 then - self.filtered_cards[id] = nil - return - end - - local modify = false - if data and type(data) == "table" and data.card - and type(data.card) == "table" and data.card:isInstanceOf(Card) then - modify = true - end - - for _, f in ipairs(filters) do - if f:cardFilter(card, player, type(data) == "table" and data.isJudgeEvent) then - local _card = f:viewAs(card, player) - _card.id = id - _card.skillName = f.name - if modify and RoomInstance then - if not f.mute then - player:broadcastSkillInvoke(f.name) - RoomInstance:doAnimate("InvokeSkill", { - name = f.name, - player = player.id, - skill_type = f.anim_type, - }) - end - RoomInstance:sendLog{ - type = "#FilterCard", - arg = f.name, - from = player.id, - arg2 = card:toLogString(), - arg3 = _card:toLogString(), - } - end - card = _card - end - if card == nil then - card = self:getCardById(id) - end - self.filtered_cards[id] = card - end - - if modify then - self.filtered_cards[id] = nil - data.card = card - return - end -end - ---- 添加一张现场打印的牌到游戏中。 ---- ---- 这张牌必须是clone出来的虚拟牌,不能有子卡;因为他接下来就要变成实体卡了 ----@param card Card -function Engine:_addPrintedCard(card) - assert(card:isVirtual() and #card.subcards == 0) - table.insert(self.printed_cards, card) - local id = -#self.printed_cards - 1 - card.id = id - self.printed_cards[id] = card + return Fk:currentRoom():filterCard(id, player, data) end --- 获知当前的Engine是跑在服务端还是客户端,并返回相应的实例。 diff --git a/lua/core/general.lua b/lua/core/general.lua index 8bd5d046..6d2bb1ad 100644 --- a/lua/core/general.lua +++ b/lua/core/general.lua @@ -22,6 +22,7 @@ ---@field public other_skills string[] @ 武将身上属于其他武将的技能,通过字符串调用 ---@field public related_skills Skill[] @ 武将相关的不属于其他武将的技能,例如邓艾的急袭 ---@field public related_other_skills string [] @ 武将相关的属于其他武将的技能,例如孙策的英姿 +---@field public all_skills table @ 武将的所有技能,包括相关技能和属于其他武将的技能 ---@field public companions string [] @ 有珠联璧合关系的武将 ---@field public hidden boolean @ 不在选将框里出现,可以点将,可以在武将一览里查询到 ---@field public total_hidden boolean @ 完全隐藏 @@ -60,6 +61,7 @@ function General:initialize(package, name, kingdom, hp, maxHp, gender) self.other_skills = {} -- skill belongs other general, e.g. "mashu" of pangde self.related_skills = {} -- skills related to this general, but not first added to it, e.g. "jixi" of dengai self.related_other_skills = {} -- skills related to this general and belong to other generals, e.g. "yingzi" of sunce + self.all_skills = {} self.companions = {} @@ -75,8 +77,10 @@ end function General:addSkill(skill) if (type(skill) == "string") then table.insert(self.other_skills, skill) + table.insert(self.all_skills, {skill, false}) elseif (skill.class and skill.class:isSubclassOf(Skill)) then table.insert(self.skills, skill) + table.insert(self.all_skills, {skill.name, false}) skill.package = self.package end end @@ -86,8 +90,10 @@ end function General:addRelatedSkill(skill) if (type(skill) == "string") then table.insert(self.related_other_skills, skill) + table.insert(self.all_skills, {skill, true}) -- only for UI elseif (skill.class and skill.class:isSubclassOf(Skill)) then table.insert(self.related_skills, skill) + table.insert(self.all_skills, {skill.name, true}) -- only for UI Fk:addSkill(skill) skill.package = self.package end diff --git a/lua/core/player.lua b/lua/core/player.lua index 5a39332f..40d1cf4d 100644 --- a/lua/core/player.lua +++ b/lua/core/player.lua @@ -13,6 +13,7 @@ ---@field public shield integer @ 护甲数 ---@field public kingdom string @ 势力 ---@field public role string @ 身份 +---@field public role_shown boolean ---@field public general string @ 武将 ---@field public deputyGeneral string @ 副将 ---@field public gender integer @ 性别 @@ -71,6 +72,11 @@ Player.JudgeSlot = 'JudgeSlot' --- 构造函数。总之这不是随便调用的函数 function Player:initialize() self.id = 0 + self.property_keys = { + "general", "deputyGeneral", "maxHp", "hp", "shield", "gender", "kingdom", + "dead", "role", "role_shown", "rest", "seat", "phase", "faceup", "chained", + "sealedSlots", + } self.hp = 0 self.maxHp = 0 self.kingdom = "qun" @@ -98,8 +104,8 @@ function Player:initialize() [Player.Equip] = {}, [Player.Judge] = {}, } - self.virtual_equips = {} self.special_cards = {} + self.virtual_equips = {} self.equipSlots = { Player.WeaponSlot, @@ -221,6 +227,15 @@ function Player:getMark(mark) return mark end +--- 获取角色对应Mark并初始化为table +---@param mark string @ 标记 +---@return table +function Player:getTableMark(mark) + local mark = self.mark[mark] + if type(mark) == "table" then return table.simpleClone(mark) end + return {} +end + --- 获取角色有哪些Mark。 function Player:getMarkNames() local ret = {} @@ -623,7 +638,7 @@ function Player:getNextAlive(ignoreRemoved, num, ignoreRest) num = num or 1 for _ = 1, num do ret = ret.next - while (ret.dead and not ignoreRest) or (doNotIgnore and ret:isRemoved()) do + while (ret.dead and (ret.rest == 0 or not ignoreRest)) or (doNotIgnore and ret:isRemoved()) do ret = ret.next end end @@ -1179,6 +1194,71 @@ function Player:isBuddy(other) return self.id == id or table.contains(self.buddy_list, id) end +--- Player是否可看到某card +--- @param cardId integer +---@param move? CardsMoveStruct +---@return boolean +function Player:cardVisible(cardId, move) + if move then + if table.find(move.moveInfo, function(info) return info.cardId == cardId end) then + if move.moveVisible then return true end + -- specialVisible还要控制这个pile对他人是否应该可见,但是不在这里生效 + if move.specialVisible then return true end + + if (type(move.visiblePlayers) == "number" and move.visiblePlayers == self.id) or + (type(move.visiblePlayers) == "table" and table.find(move.visiblePlayers, self.id)) then + return true + end + end + end + + local room = Fk:currentRoom() + local area = room:getCardArea(cardId) + local card = Fk:getCardById(cardId) + + local public_areas = {Card.DiscardPile, Card.Processing, Card.Void, Card.PlayerEquip, Card.PlayerJudge} + local player_areas = {Card.PlayerHand, Card.PlayerSpecial} + + if room.observing == true then return table.contains(public_areas, area) end + + local status_skills = Fk:currentRoom().status_skills[VisibilitySkill] or Util.DummyTable + for _, skill in ipairs(status_skills) do + local f = skill:cardVisible(self, card) + if f ~= nil then + return f + end + end + + if area == Card.DrawPile then return false + elseif table.contains(public_areas, area) then return true + elseif move and area == Card.PlayerSpecial and not move.specialName:startsWith("$") then + return true + elseif table.contains(player_areas, area) then + local to = room:getCardOwner(cardId) + return to == self or self:isBuddy(to) + else + return false + end +end + +--- Player是否可看到某target的身份 +--- @param target Player +---@return boolean +function Player:roleVisible(target) + local room = Fk:currentRoom() + local status_skills = room.status_skills[VisibilitySkill] or Util.DummyTable + for _, skill in ipairs(status_skills) do + local f = skill:roleVisible(self, target) + if f ~= nil then + return f + end + end + + if room.observing == false and target == self then return true end + + return target.role_shown +end + --- 比较两名角色的性别是否相同。 ---@param other Player @ 另一名角色 ---@param diff? bool @ 比较二者不同 @@ -1204,4 +1284,50 @@ function Player:isFemale() return self.gender == General.Female or self.gender == General.Bigender end +function Player:toJsonObject() + local ptable = {} + for _, k in ipairs(self.property_keys) do + ptable[k] = self[k] + end + + return { + properties = ptable, + card_history = self.cardUsedHistory, + skill_history = self.skillUsedHistory, + mark = self.mark, + skills = table.map(self.player_skills, Util.NameMapper), + player_cards = self.player_cards, + special_cards = self.special_cards, + buddy_list = self.buddy_list, + } +end + +function Player:loadJsonObject(o) + for k, v in pairs(o.properties) do self[k] = v end + self.cardUsedHistory = o.card_history + self.skillUsedHistory = o.skill_history + self.mark = o.mark + for _, sname in ipairs(o.skills) do self:addSkill(sname) end + self.player_cards = o.player_cards + self.special_cards = o.special_cards + self.buddy_list = o.buddy_list + + local pid = self.id + local room = Fk:currentRoom() + for _, id in ipairs(o.player_cards[Player.Hand]) do + room:setCardArea(id, Card.PlayerHand, pid) + end + for _, id in ipairs(o.player_cards[Player.Equip]) do + room:setCardArea(id, Card.PlayerEquip, pid) + end + for _, id in ipairs(o.player_cards[Player.Judge]) do + room:setCardArea(id, Card.PlayerJudge, pid) + end + for _, ids in ipairs(o.special_cards) do + for _, id in ipairs(ids) do + room:setCardArea(id, Card.PlayerSpecial, pid) + end + end +end + return Player diff --git a/lua/core/request_handler.lua b/lua/core/request_handler.lua new file mode 100644 index 00000000..d065d53a --- /dev/null +++ b/lua/core/request_handler.lua @@ -0,0 +1,80 @@ +--[[ + RequestHandler是当一名Player收到Request信息时,创建的数据结构。 + 根据Player是由真人控制还是由Bot控制,该数据可能创建于客户端或者服务端。 + + 内容: + * 一个Scene对象,保存着在答复该Request时,界面上所有的可操作要素 + * Player与currentRoom + * setup(): 初始化函数 + + 当RequestHandler创建于客户端时,其还负责与实际显示出的UI进行通信。为了与实际 + 界面进行通信,需要额外的方法与数据: + + * notifyUI(): 向qml发送所有与UI更新有关的信息 + * update(): Qml向Lua发送UI事件后,这里做出相关的处理, + 一般最后通过notifyUI反馈更新信息 + * self.change: 一次update中,产生的UI变化;设置这个的目的是当notifyUI时 + 减少信息量,只将状态发生改变的元素发回客户端 + (*) 在QML中需定义applyChange函数以接收来自Lua的更改 + + 当RequestHandler创建于服务端时,因为并没有实际的界面,所以上述三个方法无用, + 此时与RequestHandler进行交互的就是AI逻辑代码;这些就留到以后讨论了。 +--]] +--@field public data any 相关数据,需要子类自行定义一个类或者模拟类 + +-- 关于self.change: +-- * _new: 新创建的Item,一开始的时候UI上没显示它们 +-- * _delete: 删除新创建的Item +-- * _prompt: 提示信息。实践证明prompt值得单开一个key +-- * _misc: 其他乱七八糟的需要告诉UI的信息 +-- * Item类名:这类Item中某个Item发生的信息变动,change的主体部分 + +---@class RequestHandler: Object +---@field public room AbstractRoom +---@field public scene Scene +---@field public player Player 需要应答的玩家 +---@field public prompt string 提示信息 +---@field public change { [string]: Item[] } 将会传递给UI的更新数据 +local RequestHandler = class("RequestHandler") + +function RequestHandler:initialize(player) + self.room = Fk:currentRoom() + self.player = player + -- finish只在Client执行 用于保证UI执行了某些必须执行的善后 + if ClientInstance and ClientInstance.current_request_handler then + ClientInstance.current_request_handler:_finish() + end + self.room.current_request_handler = self +end + +-- 进入Request之后需要做的第一步操作,对应之前UI代码中state变换 +function RequestHandler:setup() end + +function RequestHandler:_finish() + if not self.finished then + self.finished = true + self.change = {} + self:finish() + self.scene:notifyUI() + end +end + +-- 因为发送答复或者超时等原因导致UI进入notactive状态时调用。 +-- 只会由UI调用且只执行一次;意义主要在于清除那些传给了UI的半路新建的对象 +function RequestHandler:finish() end + +-- 产生UI事件后由UI触发 +-- 需要实现各种合法性检验,决定需要变更状态的UI,并最终将变更反馈给真实的界面 +---@param elemType string +---@param id string | integer +---@param action string +---@param data any +function RequestHandler:update(elemType, id, action, data) end + +function RequestHandler:setPrompt(str) + if not self.change then return end + self.prompt = str + self.change["_prompt"] = str +end + +return RequestHandler diff --git a/lua/core/request_type/active_skill.lua b/lua/core/request_type/active_skill.lua new file mode 100644 index 00000000..e781d5b8 --- /dev/null +++ b/lua/core/request_type/active_skill.lua @@ -0,0 +1,367 @@ +local RoomScene = require 'ui_emu.roomscene' +local Interaction = require 'ui_emu.interaction' +local CardItem = (require 'ui_emu.common').CardItem + +--[[ + 负责处理AskForUseActiveSkill的Handler。 + 涉及的UI组件:手牌区内的牌(TODO:expand牌)、在场角色、确定取消按钮 + (TODO:interaction小组件) + 可能发生的事件: + * 点击手牌:刷新所有未选中牌的enable + * 点击角色:刷新所有未选中的角色 + * (TODO) 修改interaction:重置信息 + * 按下按钮:发送答复 + + 为了后续的复用性需将ViewAsSkill也考虑进去 +--]] + +---@class ReqActiveSkill: RequestHandler +---@field public skill_name string 当前响应的技能名 +---@field public prompt string 提示信息 +---@field public cancelable boolean 可否取消 +---@field public extra_data UseExtraData 传入的额外信息 +---@field public pendings integer[] 卡牌id数组 +---@field public selected_targets integer[] 选择的目标 +---@field public expanded_piles { [string]: integer[] } 用于展开/收起 +local ReqActiveSkill = RequestHandler:subclass("ReqActiveSkill") + +function ReqActiveSkill:initialize(player) + RequestHandler.initialize(self, player) + self.scene = RoomScene:new(self) + + self.expanded_piles = {} +end + +function ReqActiveSkill:setup(ignoreInteraction) + local scene = self.scene + + -- FIXME: 偷懒了,让修改interaction时的全局刷新功能复用setup 总之这里写的很垃圾 + if not ignoreInteraction then + scene:removeItem("Interaction", "1") + self:setupInteraction() + end + + self:setPrompt(self.prompt) + + self.pendings = {} + self:retractAllPiles() + self:expandPiles() + scene:unselectAllCards() + + self.selected_targets = {} + scene:unselectAllTargets() + + self:updateUnselectedCards() + self:updateUnselectedTargets() + + self:updateButtons() +end + +function ReqActiveSkill:finish() + self:retractAllPiles() +end + +function ReqActiveSkill:setSkillPrompt(skill, cid) + local prompt = skill.prompt + if type(skill.prompt) == "function" then + prompt = skill:prompt(cid or self.pendings, self.selected_targets) + end + if type(prompt) == "string" then + self:setPrompt(prompt) + else + self:setPrompt(self.original_prompt or "") + end +end + +function ReqActiveSkill:setupInteraction() + local skill = Fk.skills[self.skill_name] + if skill and skill.interaction then + skill.interaction.data = nil -- FIXME + local interaction = skill:interaction() + -- 假设只有1个interaction (其实目前就是这样) + local i = Interaction:new(self.scene, "1", interaction) + i.skill_name = self.skill_name + self.scene:addItem(i) + end +end + +function ReqActiveSkill:expandPile(pile, extra_ids, extra_footnote) + if self.expanded_piles[pile] ~= nil then return end + local ids, footnote + local player = self.player + + if pile == "_equip" then + ids = player:getCardIds("e") + footnote = "$Equip" + elseif pile == "_extra" then + ids = extra_ids + footnote = extra_footnote + -- self.extra_cards = exira_ids + else + -- FIXME: 可能存在的浅拷贝 + ids = table.simpleClone(player:getPile(pile)) + footnote = pile + end + self.expanded_piles[pile] = ids + + local scene = self.scene + for _, id in ipairs(ids) do + scene:addItem(CardItem:new(scene, id), { + reason = "expand", + footnote = footnote, + }) + end +end + +function ReqActiveSkill:retractPile(pile) + if self.expanded_piles[pile] == nil then return end + local ids = self.expanded_piles[pile] + self.expanded_piles[pile] = nil + + local scene = self.scene + for _, id in ipairs(ids) do + scene:removeItem("CardItem", id, { reason = "retract" }) + end +end + +function ReqActiveSkill:retractAllPiles() + for k, v in pairs(self.expanded_piles) do + self:retractPile(k) + end +end + +function ReqActiveSkill:expandPiles() + local skill = Fk.skills[self.skill_name] + local player = self.player + if not skill then return end + -- 特殊:equips至少有一张能亮着的情况下才展开 且无视是否存在skill.expand_pile + for _, id in ipairs(player:getCardIds("e")) do + if self:cardValidity(id) then + self:expandPile("_equip") + break + end + end + + if not skill.expand_pile then return end + local pile = skill.expand_pile + if type(pile) == "function" then + pile = pile(skill) + end + + local ids = pile + if type(pile) == "string" then + ids = player:getPile(pile) + else -- if type(pile) == "table" then + pile = "_extra" + end + + self:expandPile(pile, ids, self.skill_name) +end + +function ReqActiveSkill:feasible() + local player = self.player + local skill = Fk.skills[self.skill_name] + if not skill then return false end + local ret + if skill:isInstanceOf(ActiveSkill) then + ret = skill:feasible(self.selected_targets, self.pendings, player) + elseif skill:isInstanceOf(ViewAsSkill) then + local card = skill:viewAs(self.pendings) + if card then + local card_skill = card.skill ---@type ActiveSkill + ret = card_skill:feasible(self.selected_targets, { card.id }, player, card) + end + end + return ret +end + +function ReqActiveSkill:isCancelable() + return self.cancelable +end + +function ReqActiveSkill:cardValidity(cid) + local skill = Fk.skills[self.skill_name] + if not skill then return false end + return skill:cardFilter(cid, self.pendings) +end + +function ReqActiveSkill:extraDataValidity(pid) + local data = self.extra_data or {} + -- 逻辑块地狱 + if data.must_targets then + -- must_targets: 必须先选择must_targets内的**所有**目标 + if not (#data.must_targets <= #self.selected_targets or + table.contains(data.must_targets, pid)) then return false end + end + if data.include_targets then + -- include_targets: 必须先选择include_targets内的**其中一个**目标 + if not (table.hasIntersection(data.include_targets, self.selected_targets) or + table.contains(data.include_targets, pid)) then return false end + end + if data.exclusive_targets then + -- exclusive_targets: **只能选择**exclusive_targets内的目标 + if not table.contains(data.exclusive_targets, pid) then return false end + end + return true +end + +function ReqActiveSkill:targetValidity(pid) + if not self:extraDataValidity(pid) then return false end + + local skill = Fk.skills[self.skill_name] --- @type ActiveSkill | ViewAsSkill + if not skill then return false end + local card -- 姑且接一下(雾) + if skill:isInstanceOf(ViewAsSkill) then + card = skill:viewAs(self.pendings) + if not card or self.player:isProhibited(self.room:getPlayerById(pid), card) then return false end + skill = card.skill + end + return skill:targetFilter(pid, self.selected_targets, self.pendings, card, self.extra_data) +end + +function ReqActiveSkill:updateButtons() + local scene = self.scene + scene:update("Button", "OK", { enabled = not not self:feasible() }) + scene:update("Button", "Cancel", { enabled = not not self:isCancelable() }) +end + +function ReqActiveSkill:updateUnselectedCards() + local scene = self.scene + + for cid, item in pairs(scene:getAllItems("CardItem")) do + if not item.selected then + scene:update("CardItem", cid, { enabled = not not self:cardValidity(cid) }) + end + end +end + +function ReqActiveSkill:updateUnselectedTargets() + local scene = self.scene + + for pid, item in pairs(scene:getAllItems("Photo")) do + if not item.selected then + scene:updateTargetEnability(pid, self:targetValidity(pid)) + end + end +end + +function ReqActiveSkill:initiateTargets() + local room = self.room + local scene = self.scene + local skill = Fk.skills[self.skill_name] + if skill:isInstanceOf(ViewAsSkill) then + local card = skill:viewAs(self.pendings) + if card then skill = card.skill else skill = nil end + end + + self.selected_targets = {} + scene:unselectAllTargets() + if skill then + self:updateUnselectedTargets() + else + scene:disableAllTargets() + end + self:updateButtons() +end + +function ReqActiveSkill:updateInteraction(data) + local skill = Fk.skills[self.skill_name] + if skill and skill.interaction then + skill.interaction.data = data + self.scene:update("Interaction", "1", { data = data }) + ReqActiveSkill.setup(self, true) -- interaction变动后需复原 + end +end + +function ReqActiveSkill:doOKButton() + local skill = Fk.skills[self.skill_name] + local cardstr = json.encode{ + skill = self.skill_name, + subcards = self.pendings + } + local reply = { + card = cardstr, + targets = self.selected_targets, + --special_skill = roomScene.getCurrentCardUseMethod(), + interaction_data = skill and skill.interaction and skill.interaction.data, + } + if self.selected_card then + reply.special_skill = self.skill_name + end + ClientInstance:notifyUI("ReplyToServer", json.encode(reply)) +end + +function ReqActiveSkill:doCancelButton() + ClientInstance:notifyUI("ReplyToServer", "__cancel") +end + +-- 对点击卡牌的处理。data中包含selected属性,可能是选中或者取消选中,分开考虑。 +function ReqActiveSkill:selectCard(cardid, data) + local scene = self.scene + local selected = data.selected + scene:update("CardItem", cardid, data) + + -- 若选中,则加入已选列表;若取消选中,则其他牌可能无法满足可选条件,需额外判断 + -- 例如周善 选择包括“安”在内的任意张手牌交出 + if selected then + table.insert(self.pendings, cardid) + else + local old_pendings = table.simpleClone(self.pendings) + self.pendings = {} + for _, cid in ipairs(old_pendings) do + local ret = cid ~= cardid and self:cardValidity(cid) + if ret then table.insert(self.pendings, cid) end + -- 因为这里而变成未选中的牌稍后将更新一次enable 但是存在着冗余的cardFilter调用 + scene:update("CardItem", cid, { selected = not not ret }) + end + end + + -- 最后刷新未选牌的enable属性 + self:updateUnselectedCards() +end + +-- 对点击角色的处理。data中包含selected属性,可能是选中或者取消选中。 +function ReqActiveSkill:selectTarget(playerid, data) + local scene = self.scene + local selected = data.selected + local skill = Fk.skills[self.skill_name] + scene:update("Photo", playerid, data) + -- 发生以下Viewas判断时已经是因为选角色触发的了,说明肯定有card了,这么写不会出事吧? + if skill:isInstanceOf(ViewAsSkill) then + skill = skill:viewAs(self.pendings).skill + end + + -- 类似选卡 + if selected then + table.insert(self.selected_targets, playerid) + else + local old_targets = table.simpleClone(self.selected_targets) + self.selected_targets = {} + scene:unselectAllTargets() + for _, pid in ipairs(old_targets) do + local ret = pid ~= playerid and self:targetValidity(pid) + if ret then table.insert(self.selected_targets, pid) end + scene:update("Photo", pid, { selected = not not ret }) + end + end + + self:updateUnselectedTargets() + self:updateButtons() +end + +function ReqActiveSkill:update(elemType, id, action, data) + if elemType == "Button" then + if id == "OK" then self:doOKButton() + elseif id == "Cancel" then self:doCancelButton() end + return true + elseif elemType == "CardItem" then + self:selectCard(id, data) + self:initiateTargets() + elseif elemType == "Photo" then + self:selectTarget(id, data) + elseif elemType == "Interaction" then + self:updateInteraction(data) + end +end + +return ReqActiveSkill diff --git a/lua/core/request_type/invoke.lua b/lua/core/request_type/invoke.lua new file mode 100644 index 00000000..4a9fb847 --- /dev/null +++ b/lua/core/request_type/invoke.lua @@ -0,0 +1,36 @@ +local OKScene = require 'ui_emu.okscene' + +-- 极其简单的skillinvoke + +---@class ReqInvoke: RequestHandler +local ReqInvoke = RequestHandler:subclass("ReqInvoke") + +function ReqInvoke:initialize(player) + RequestHandler.initialize(self, player) + self.scene = OKScene:new(self) +end + +function ReqInvoke:setup() + local scene = self.scene + + scene:update("Button", "OK", { enabled = true }) + scene:update("Button", "Cancel", { enabled = true }) +end + +function ReqInvoke:doOKButton() + ClientInstance:notifyUI("ReplyToServer", "1") +end + +function ReqInvoke:doCancelButton() + ClientInstance:notifyUI("ReplyToServer", "__cancel") +end + +function ReqInvoke:update(elemType, id, action, data) + if elemType == "Button" then + if id == "OK" then self:doOKButton() + elseif id == "Cancel" then self:doCancelButton() end + return true + end +end + +return ReqInvoke diff --git a/lua/core/request_type/play_card.lua b/lua/core/request_type/play_card.lua new file mode 100644 index 00000000..844bb310 --- /dev/null +++ b/lua/core/request_type/play_card.lua @@ -0,0 +1,132 @@ +local ReqActiveSkill = require 'core.request_type.active_skill' +local ReqUseCard = require 'lua.core.request_type.use_card' +local SpecialSkills = require 'ui_emu.specialskills' +local Button = (require 'ui_emu.control').Button + +---@class ReqPlayCard: ReqUseCard +local ReqPlayCard = ReqUseCard:subclass("ReqPlayCard") + +function ReqPlayCard:initialize(player) + ReqUseCard.initialize(self, player) + + self.original_prompt = "#PlayCard" + local scene = self.scene + -- 出牌阶段还要多模拟一个结束按钮 + scene:addItem(Button:new(self.scene, "End")) + scene:addItem(SpecialSkills:new(self.scene, "1")) +end + +function ReqPlayCard:setup() + ReqUseCard.setup(self) + + self:setPrompt(self.original_prompt) + self.scene:update("Button", "End", { enabled = true }) +end + +function ReqPlayCard:cardValidity(cid) + if self.skill_name and not self.selected_card then return ReqActiveSkill.cardValidity(self, cid) end + local player = self.player + local card = cid + if type(cid) == "number" then card = Fk:getCardById(cid) end + local ret = player:canUse(card) + if ret then + local min_target = card.skill:getMinTargetNum() + if min_target > 0 then + for pid, _ in pairs(self.scene:getAllItems("Photo")) do + if card.skill:targetFilter(pid, {}, {}, card, self.extra_data) then + return true + end + end + return false + end + end + return ret +end + +function ReqPlayCard:skillButtonValidity(name) + local player = self.player + local skill = Fk.skills[name] + if skill:isInstanceOf(ViewAsSkill) then + return skill:enabledAtPlay(player, true) + elseif skill:isInstanceOf(ActiveSkill) then + return skill:canUse(player, nil) + end +end + +function ReqPlayCard:feasible() + local player = self.player + if self.skill_name then + return ReqActiveSkill.feasible(self) + end + local card = self.selected_card + local ret = false + if card then + local skill = card.skill ---@type ActiveSkill + ret = skill:feasible(self.selected_targets, { card.id }, player, card) + end + return ret +end + +function ReqPlayCard:selectSpecialUse(data) + -- 相当于使用一个以已选牌为pendings的主动技 + if not data or data == "_normal_use" then + self.skill_name = nil + self.pendings = nil + self:setSkillPrompt(self.selected_card.skill, self.selected_card:getEffectiveId()) + else + self.skill_name = data + self.pendings = Card:getIdList(self.selected_card) + self:setSkillPrompt(Fk.skills[data], self.pendings) + end + self:initiateTargets() +end + +function ReqPlayCard:doOKButton() + self.scene:update("SpecialSkills", "1", { skills = {} }) + self.scene:notifyUI() + return ReqUseCard.doOKButton(self) +end + +function ReqPlayCard:doCancelButton() + self.scene:update("SpecialSkills", "1", { skills = {} }) + self.scene:notifyUI() + return ReqUseCard.doCancelButton(self) +end + +function ReqPlayCard:doEndButton() + self.scene:update("SpecialSkills", "1", { skills = {} }) + self.scene:notifyUI() + ClientInstance:notifyUI("ReplyToServer", "") +end + +function ReqPlayCard:selectCard(cid, data) + ReqUseCard.selectCard(self, cid, data) + if self.skill_name and not self.selected_card then return end + + if self.selected_card then + self:setSkillPrompt(self.selected_card.skill, self.selected_card:getEffectiveId()) + local sp_skills = {} + if self.selected_card.special_skills then + sp_skills = table.simpleClone(self.selected_card.special_skills) + if self:cardValidity(self.selected_card) then + table.insert(sp_skills, 1, "_normal_use") + end + end + self.scene:update("SpecialSkills", "1", { skills = sp_skills }) + else + self:setPrompt(self.original_prompt) + self.scene:update("SpecialSkills", "1", { skills = {} }) + end +end + +function ReqPlayCard:update(elemType, id, action, data) + if elemType == "Button" and id == "End" then + self:doEndButton() + return true + elseif elemType == "SpecialSkills" then + self:selectSpecialUse(data) + end + return ReqUseCard.update(self, elemType, id, action, data) +end + +return ReqPlayCard diff --git a/lua/core/request_type/response_card.lua b/lua/core/request_type/response_card.lua new file mode 100644 index 00000000..a26f0b5b --- /dev/null +++ b/lua/core/request_type/response_card.lua @@ -0,0 +1,164 @@ +local RoomScene = require 'ui_emu.roomscene' +local ReqActiveSkill = require 'core.request_type.active_skill' + +--[[ + 负责处理AskForResponseCard的Handler。 + 涉及的UI组件:较基类增加技能按钮、减少角色 + 可能发生的事件: + * 点击手牌:取消选中其他牌 + * 按下按钮:发送答复 + * 点击技能按钮:若有则取消其他已按下按钮的按下,重置信息 + 若有按下的技能按钮则走ActiveSkill合法性流程 +--]] + +---@class ReqResponseCard: ReqActiveSkill +---@field public selected_card? Card 使用一张牌时会用到 支持锁视技 +---@field public pattern string 请求格式 +---@field public original_prompt string 最开始的提示信息;这种涉及技能按钮的需要这样一下 +local ReqResponseCard = ReqActiveSkill:subclass("ReqResponseCard") + +function ReqResponseCard:setup() + if not self.original_prompt then + self.original_prompt = self.prompt or "" + end + + ReqActiveSkill.setup(self) + self.selected_card = nil + self:updateSkillButtons() +end + +-- FIXME: 关于&牌堆的可使用打出瞎jb写了点 来个懂哥优化一下 +function ReqResponseCard:expandPiles() + if self.skill_name then return ReqActiveSkill.expandPiles(self) end + local player = self.player + for pile in pairs(player.special_cards) do + if pile:endsWith('&') then + self:expandPile(pile) + end + end +end + +function ReqResponseCard:skillButtonValidity(name) + local player = self.player + local skill = Fk.skills[name] + return skill:isInstanceOf(ViewAsSkill) and skill:enabledAtResponse(player, true) +end + +function ReqResponseCard:cardValidity(cid) + if self.skill_name then return ReqActiveSkill.cardValidity(self, cid) end + local card = cid + if type(cid) == "number" then card = Fk:getCardById(cid) end + return self:cardFeasible(card) +end + +function ReqResponseCard:cardFeasible(card) + local exp = Exppattern:Parse(self.pattern) + local player = self.player + return not player:prohibitResponse(card) and exp:match(card) +end + +function ReqResponseCard:feasible() + local skill = Fk.skills[self.skill_name] + local card = self.selected_card + if skill then + card = skill:viewAs(self.pendings) + end + return card and self:cardFeasible(card) +end + +function ReqResponseCard:isCancelable() + if self.skill_name then return true end + return self.cancelable +end + +function ReqResponseCard:updateSkillButtons() + local scene = self.scene + for name, item in pairs(scene:getAllItems("SkillButton")) do + local skill = Fk.skills[name] + local ret = self:skillButtonValidity(name) + if ret and skill:isInstanceOf(ViewAsSkill) then + local exp = Exppattern:Parse(skill.pattern) + local cnames = {} + for _, m in ipairs(exp.matchers) do + if m.name then table.insertTable(cnames, m.name) end + if m.trueName then table.insertTable(cnames, m.trueName) end + end + for _, n in ipairs(cnames) do + local c = Fk:cloneCard(n) + c.skillName = name + ret = self:cardValidity(c) + if ret then break end + end + end + scene:update("SkillButton", name, { enabled = not not ret }) + end +end + +function ReqResponseCard:doOKButton() + if self.skill_name then return ReqActiveSkill.doOKButton(self) end + local reply = { + card = self.selected_card:getEffectiveId(), + targets = self.selected_targets, + } + ClientInstance:notifyUI("ReplyToServer", json.encode(reply)) +end + +function ReqResponseCard:doCancelButton() + if self.skill_name then + self:selectSkill(self.skill_name, { selected = false }) + self.scene:notifyUI() + return + end + return ReqActiveSkill:doCancelButton() +end + +function ReqResponseCard:selectSkill(skill, data) + local scene = self.scene + local selected = data.selected + scene:update("SkillButton", skill, data) + + if selected then + for name, item in pairs(scene:getAllItems("SkillButton")) do + scene:update("SkillButton", name, { enabled = item.selected }) + end + self.skill_name = skill + self.selected_card = nil + self:setSkillPrompt(skill) + + ReqActiveSkill.setup(self) + else + self.skill_name = nil + self.prompt = self.original_prompt + self:setup() + end +end + +function ReqResponseCard:selectCard(cid, data) + if self.skill_name and not self.selected_card then + return ReqActiveSkill.selectCard(self, cid, data) + end + local scene = self.scene + local selected = data.selected + scene:update("CardItem", cid, data) + + if selected then + self.skill_name = nil + self.selected_card = Fk:getCardById(cid) + scene:unselectOtherCards(cid) + else + self.selected_card = nil + end +end + +function ReqResponseCard:update(elemType, id, action, data) + if elemType == "CardItem" then + self:selectCard(id, data) + self:updateButtons() + elseif elemType == "SkillButton" then + self:selectSkill(id, data) + else -- if elemType == "Button" or elemType == "Interaction" then + return ReqActiveSkill.update(self, elemType, id, action, data) + end +end + +return ReqResponseCard diff --git a/lua/core/request_type/use_card.lua b/lua/core/request_type/use_card.lua new file mode 100644 index 00000000..e65a818c --- /dev/null +++ b/lua/core/request_type/use_card.lua @@ -0,0 +1,107 @@ +local ReqActiveSkill = require 'core.request_type.active_skill' +local ReqResponseCard = require 'core.request_type.response_card' + +---@class ReqUseCard: ReqResponseCard +local ReqUseCard = ReqResponseCard:subclass("ReqUseCard") + +function ReqUseCard:cardValidity(cid) + if self.skill_name then return ReqActiveSkill.cardValidity(self, cid) end + local card = cid + if type(cid) == "number" then card = Fk:getCardById(cid) end + return self:cardFeasible(card) +end + +function ReqUseCard:targetValidity(pid) + if self.skill_name then return ReqActiveSkill.targetValidity(self, pid) end + local player = self.player + local room = self.room + local p = room:getPlayerById(pid) + local card = self.selected_card + local ret = card and not player:isProhibited(p, card) and + card.skill:targetFilter(pid, self.selected_targets, { card.id }, card, self.extra_data) + return ret +end + +function ReqUseCard:cardFeasible(card) + local exp = Exppattern:Parse(self.pattern) + local player = self.player + return not player:prohibitUse(card) and exp:match(card) +end + +function ReqUseCard:feasible() + local skill = Fk.skills[self.skill_name] + local card = self.selected_card + local ret = false + if card and self:cardFeasible(card) then + ret = card.skill:feasible(self.selected_targets, + skill and self.pendings or { card.id }, self.player, card) + end + return ret +end + +function ReqUseCard:initiateTargets() + if self.skill_name then + return ReqActiveSkill.initiateTargets(self) + end + + -- 重置 + self.selected_targets = {} + self.scene:unselectAllTargets() + self:updateUnselectedTargets() + self:updateButtons() +end + +function ReqUseCard:selectTarget(playerid, data) + if self.skill_name then + return ReqActiveSkill.selectTarget(self, playerid, data) + end + + local player = self.player + local scene = self.scene + local selected = data.selected + local card = self.selected_card + scene:update("Photo", playerid, data) + + if card then + local skill = card.skill + if selected then + table.insert(self.selected_targets, playerid) + else + -- 存储剩余目标 + local previous_targets = table.filter(self.selected_targets, function(id) + return id ~= playerid + end) + self.selected_targets = {} + for _, pid in ipairs(previous_targets) do + local ret + ret = not player:isProhibited(pid, card) and skill and + skill:targetFilter(pid, self.selected_targets, + { card.id }, card, data.extra_data) + -- 从头开始写目标 + if ret then + table.insert(self.selected_targets, pid) + end + scene:update("Photo", pid, { selected = not not ret }) + end + end + end + self:updateUnselectedTargets() + self:updateButtons() +end + +function ReqUseCard:selectSkill(skill, data) + ReqResponseCard.selectSkill(self, skill, data) + self.selected_targets = {} + self.scene:unselectAllTargets() + self:updateUnselectedTargets() +end + +function ReqUseCard:update(elemType, id, action, data) + if elemType == "CardItem" or elemType == "Photo" then + return ReqActiveSkill.update(self, elemType, id, action, data) + else --if elemType == "Button" or elemType == "SkillButton" then or interaction + return ReqResponseCard.update(self, elemType, id, action, data) + end +end + +return ReqUseCard diff --git a/lua/core/room/abstract_room.lua b/lua/core/room/abstract_room.lua new file mode 100644 index 00000000..02ad01ad --- /dev/null +++ b/lua/core/room/abstract_room.lua @@ -0,0 +1,92 @@ +-- 作Room和Client的基类,这二者有不少共通之处 +-- +-- 子类纯属写给注释看,这个面向对象库没有实现多重继承 +---@class AbstractRoom : CardManager +---@field public players Player[] @ 房内参战角色们 +---@field public alive_players Player[] @ 所有存活玩家的数组 +---@field public observers Player[] @ 看戏的 +---@field public current Player @ 当前行动者 +---@field public status_skills table @ 这个房间中含有的状态技列表 +---@field public skill_costs table @ 用来存skill.cost_data +---@field public card_marks table @ 用来存实体卡的card.mark +---@field public banners table @ 全局mark +---@field public current_request_handler RequestHandler @ 当前正处理的请求数据 +---@field public timeout integer @ 出牌时长上限 +---@field public settings table @ 房间的额外设置,差不多是json对象 +local AbstractRoom = class("AbstractRoom") + +local CardManager = require 'core.room.card_manager' +AbstractRoom:include(CardManager) + +function AbstractRoom:initialize() + self.players = {} + self.alive_players = {} + self.observers = {} + self.current = nil + + self:initCardManager() + self.status_skills = {} + for class, skills in pairs(Fk.global_status_skill) do + self.status_skills[class] = {table.unpack(skills)} + end + + self.skill_costs = {} + self.banners = {} +end + +-- 仅供注释,其余空函数一样 +---@param id integer +---@return Player +---@diagnostic disable-next-line: missing-return +function AbstractRoom:getPlayerById(id) end + +--- 获得拥有某一张牌的玩家。 +---@param cardId integer | Card @ 要获得主人的那张牌,可以是Card实例或者id +---@return Player? @ 这张牌的主人,可能返回nil +function AbstractRoom:getCardOwner(cardId) + local ret = CardManager.getCardOwner(self, cardId) + return ret and self:getPlayerById(ret) +end + +function AbstractRoom:setBanner(name, value) + if value == 0 then value = nil end + self.banners[name] = value +end + +function AbstractRoom:getBanner(name) + return self.banners[name] +end + +function AbstractRoom:toJsonObject() + local card_manager = CardManager.toJsonObject(self) + + local players = {} + for _, p in ipairs(self.players) do + players[tostring(p.id)] = p:toJsonObject() + end + + return { + card_manager = card_manager, + circle = table.map(self.players, Util.IdMapper), + banners = self.banners, + timeout = self.timeout, + settings = self.settings, + + players = players, + } +end + +function AbstractRoom:loadJsonObject(o) + CardManager.loadJsonObject(self, o.card_manager) + + -- 需要上层(目前是Client)自己根据circle添加玩家 + self.banners = o.banners + self.timeout = o.timeout + self.settings = o.settings + for k, v in pairs(o.players) do + local pid = tonumber(k) + self:getPlayerById(pid):loadJsonObject(v) + end +end + +return AbstractRoom diff --git a/lua/core/room/card_manager.lua b/lua/core/room/card_manager.lua new file mode 100644 index 00000000..3b6e5e9e --- /dev/null +++ b/lua/core/room/card_manager.lua @@ -0,0 +1,355 @@ +--- 负责管理AbstractRoom中所有Card的位置,若在玩家的区域中,则管理所属玩家 +---@class CardManager : Object +---@field public draw_pile integer[] @ 摸牌堆,这是卡牌id的数组 +---@field public discard_pile integer[] @ 弃牌堆,也是卡牌id的数组 +---@field public processing_area integer[] @ 处理区,依然是卡牌id数组 +---@field public void integer[] @ 从游戏中除外区,一样的是卡牌id数组 +---@field public card_place table @ 每个卡牌的id对应的区域,一张表 +---@field public owner_map table @ 每个卡牌id对应的主人,表的值是那个玩家的id,可能是nil +---@field public filtered_cards table @ 见于Engine,其实在这 +---@field public printed_cards table @ 同上 +---@field public next_print_card_id integer +---@field public card_marks table @ 用来存实体卡的card.mark +local CardManager = {} -- mixin + +function CardManager:initCardManager() + self.draw_pile = {} + self.discard_pile = {} + self.processing_area = {} + self.void = {} + + self.card_place = {} + self.owner_map = {} + + self.filtered_cards = {} + self.printed_cards = {} + self.next_print_card_id = -2 + self.card_marks = {} +end + +--- 基本算是私有函数,别去用 +---@param cardId integer +---@param cardArea CardArea +---@param owner? integer +function CardManager:setCardArea(cardId, cardArea, owner) + self.card_place[cardId] = cardArea + self.owner_map[cardId] = owner +end + +--- 获取一张牌所处的区域。 +---@param cardId integer | Card @ 要获得区域的那张牌,可以是Card或者一个id +---@return CardArea @ 这张牌的区域 +function CardManager:getCardArea(cardId) + local cardIds = {} + for _, cid in ipairs(Card:getIdList(cardId)) do + local place = self.card_place[cid] or Card.Unknown + table.insertIfNeed(cardIds, place) + end + return #cardIds == 1 and cardIds[1] or Card.Unknown +end + +function CardManager:getCardOwner(cardId) + if type(cardId) ~= "number" then + assert(cardId and cardId:isInstanceOf(Card)) + cardId = cardId:getEffectiveId() + end + return self.owner_map[cardId] or nil +end + +local playerAreas = { Player.Hand, Player.Equip, Player.Judge, Player.Special } + +--- 根据area获取相关的数组,若为玩家的区域则需指定玩家 +--- +--- 若不存在这种区域,需要返回nil +---@param area CardArea +---@param player Player? +---@param dup boolean? 是否返回复制 默认true +---@param special_name string? +function CardManager:getCardsByArea(area, player, dup, special_name) + local ret + dup = dup == nil and true or false + + if area == Card.Processing then + ret = self.processing_area + elseif area == Card.DrawPile then + ret = self.draw_pile + elseif area == Card.DiscardPile then + ret = self.discard_pile + elseif area == Card.Void then + ret = self.void + elseif table.contains(playerAreas, area) then + assert(player ~= nil) + if area == Player.Special then + assert(special_name ~= nil) + ret = player.special_cards[special_name] + else + ret = player.player_cards[area] + end + end + + if dup and ret then ret = table.simpleClone(ret) end + return ret +end + +--- 根据moveInfo来移动牌,先将牌从旧数组移动到新数组,再更新两个map表 +---@param data CardsMoveStruct +---@param info MoveInfo +function CardManager:applyMoveInfo(data, info) + local realFromArea = self:getCardArea(info.cardId) + local room = Fk:currentRoom() + + local fromAreaIds = self:getCardsByArea(realFromArea, + data.from and room:getPlayerById(data.from), false, info.fromSpecialName) + + table.removeOne(fromAreaIds, info.cardId) + + local toAreaIds = self:getCardsByArea(data.toArea, + data.to and room:getPlayerById(data.to), false, data.specialName) + + if data.toArea == Card.DrawPile then + local putIndex = data.drawPilePosition or 1 + if putIndex == -1 then + putIndex = #self.draw_pile + 1 + elseif putIndex < 1 or putIndex > #self.draw_pile + 1 then + putIndex = 1 + end + + table.insert(toAreaIds, putIndex, info.cardId) + else + table.insert(toAreaIds, info.cardId) + end + self:setCardArea(info.cardId, data.toArea, data.to) +end + +--- 对那个id应用锁定视为技,将它变成要被锁定视为的牌。 +---@param id integer @ 要处理的id +---@param player Player @ 和这张牌扯上关系的那名玩家 +---@param data any @ 随意,目前只用到JudgeStruct,为了影响判定牌 +function CardManager:filterCard(id, player, data) + if player == nil then + self.filtered_cards[id] = nil + return + end + + local card = Fk:getCardById(id, true) + local filters = Fk:currentRoom().status_skills[FilterSkill] or Util.DummyTable + + if #filters == 0 then + self.filtered_cards[id] = nil + return + end + + local modify = false + if data and type(data) == "table" and data.card + and type(data.card) == "table" and data.card:isInstanceOf(Card) then + modify = true + end + + for _, f in ipairs(filters) do + if f:cardFilter(card, player, type(data) == "table" and data.isJudgeEvent) then + local _card = f:viewAs(card, player) + _card.id = id + _card.skillName = f.name + if modify and RoomInstance then + if not f.mute then + player:broadcastSkillInvoke(f.name) + RoomInstance:doAnimate("InvokeSkill", { + name = f.name, + player = player.id, + skill_type = f.anim_type, + }) + end + RoomInstance:sendLog{ + type = "#FilterCard", + arg = f.name, + from = player.id, + arg2 = card:toLogString(), + arg3 = _card:toLogString(), + } + end + card = _card + end + if card == nil then + card = Fk:getCardById(id) + end + self.filtered_cards[id] = card + end + + if modify then + self.filtered_cards[id] = nil + data.card = card + return + end +end + +function CardManager:printCard(name, suit, number) + local card = Fk:cloneCard(name, suit, number) + + local id = self.next_print_card_id + card.id = id + self.printed_cards[id] = card + self.next_print_card_id = self.next_print_card_id - 1 + + table.insert(self.void, card.id) + self:setCardArea(card.id, Card.Void, nil) + return card +end + +-- misc + +function CardManager:prepareDrawPile(seed) + local allCardIds = Fk:getAllCardIds() + + for i = #allCardIds, 1, -1 do + if Fk:getCardById(allCardIds[i]).is_derived then + local id = allCardIds[i] + table.removeOne(allCardIds, id) + table.insert(self.void, id) + self:setCardArea(id, Card.Void, nil) + end + end + + table.shuffle(allCardIds, seed) + self.draw_pile = allCardIds + for _, id in ipairs(self.draw_pile) do + self:setCardArea(id, Card.DrawPile, nil) + end +end + +function CardManager:shuffleDrawPile(seed) + if #self.draw_pile + #self.discard_pile == 0 then + return + end + + table.insertTable(self.draw_pile, self.discard_pile) + for _, id in ipairs(self.discard_pile) do + self:setCardArea(id, Card.DrawPile, nil) + end + self.discard_pile = {} + table.shuffle(self.draw_pile, seed) +end + +---@param card Card +---@param fromAreas? CardArea[] +---@return integer[] +function CardManager:getSubcardsByRule(card, fromAreas) + if card:isVirtual() and #card.subcards == 0 then + return {} + end + + local cardIds = {} + fromAreas = fromAreas or Util.DummyTable + for _, cardId in ipairs(card:isVirtual() and card.subcards or { card.id }) do + if #fromAreas == 0 or table.contains(fromAreas, self:getCardArea(cardId)) then + table.insert(cardIds, cardId) + end + end + + return cardIds +end + +---@param pattern string +---@param num? number +---@param fromPile? string @ 查找的来源区域,值为drawPile|discardPile|allPiles +---@return integer[] @ id列表 可能空 +function CardManager:getCardsFromPileByRule(pattern, num, fromPile) + num = num or 1 + local pileToSearch = self.draw_pile + if fromPile == "discardPile" then + pileToSearch = self.discard_pile + elseif fromPile == "allPiles" then + pileToSearch = table.simpleClone(self.draw_pile) + table.insertTable(pileToSearch, self.discard_pile) + end + + if #pileToSearch == 0 then + return {} + end + + local cardPack = {} + if num < 3 then + for i = 1, num do + local randomIndex = math.random(1, #pileToSearch) + local curIndex = randomIndex + repeat + local curCardId = pileToSearch[curIndex] + if Fk:getCardById(curCardId):matchPattern(pattern) and not table.contains(cardPack, curCardId) then + table.insert(cardPack, pileToSearch[curIndex]) + break + end + + curIndex = curIndex + 1 + if curIndex > #pileToSearch then + curIndex = 1 + end + until curIndex == randomIndex + + if #cardPack == 0 then + break + end + end + else + local matchedIds = {} + for _, id in ipairs(pileToSearch) do + if Fk:getCardById(id):matchPattern(pattern) then + table.insert(matchedIds, id) + end + end + + local loopTimes = math.min(num, #matchedIds) + for i = 1, loopTimes do + local randomCardId = matchedIds[math.random(1, #matchedIds)] + table.insert(cardPack, randomCardId) + table.removeOne(matchedIds, randomCardId) + end + end + + return cardPack +end + +function CardManager:toJsonObject() + local printed_cards = {} + for i = -2, -math.huge, -1 do + local c = self.printed_cards[i] + if not c then break end + table.insert(printed_cards, { c.name, c.suit, c.number }) + end + + local cmarks = {} + for k, v in pairs(self.card_marks) do + cmarks[tostring(k)] = v + end + + return { + draw_pile = self.draw_pile, + discard_pile = self.discard_pile, + processing_area = self.processing_area, + void = self.void, + -- card_place和owner_map没必要;载入时setCardArea + + printed_cards = printed_cards, + card_marks = cmarks, + } +end + +function CardManager:loadJsonObject(o) + self.draw_pile = o.draw_pile + self.discard_pile = o.discard_pile + self.processing_area = o.processing_area + self.void = o.void + + for _, id in ipairs(o.draw_pile) do self:setCardArea(id, Card.DrawPile, nil) end + for _, id in ipairs(o.discard_pile) do self:setCardArea(id, Card.DiscardPile, nil) end + for _, id in ipairs(o.processing_area) do self:setCardArea(id, Card.Processing, nil) end + for _, id in ipairs(o.void) do self:setCardArea(id, Card.Void, nil) end + + for _, data in ipairs(o.printed_cards) do self:printCard(table.unpack(data)) end + + for cid, marks in pairs(o.card_marks) do + for k, v in pairs(marks) do + Fk:getCardById(tonumber(cid)):setMark(k, v) + end + end +end + +return CardManager diff --git a/lua/core/room/skill_manager.lua b/lua/core/room/skill_manager.lua new file mode 100644 index 00000000..e69de29b diff --git a/lua/core/room/user_manager.lua b/lua/core/room/user_manager.lua new file mode 100644 index 00000000..e69de29b diff --git a/lua/core/skill.lua b/lua/core/skill.lua index 79967807..cc81fe25 100644 --- a/lua/core/skill.lua +++ b/lua/core/skill.lua @@ -16,6 +16,7 @@ ---@field public attached_equip string @ 属于什么装备的技能? ---@field public relate_to_place string @ 主将技/副将技 ---@field public switchSkillName string @ 转换技名字 +---@field public times integer @ 技能剩余次数,负数不显示,正数显示 local Skill = class("Skill") ---@alias Frequency integer @@ -112,7 +113,7 @@ end ---@param player Player @ 玩家 ---@return boolean function Skill:isEffectable(player) - if self.cardSkill then + if self.cardSkill or self.permanent_skill then return true end @@ -144,4 +145,15 @@ function Skill:isPlayerSkill(player) return not (self:isEquipmentSkill(player) or self.name:endsWith("&")) end +---@return integer +function Skill:getTimes() + local ret = self.times + if not ret then + return -1 + elseif type(ret) == "function" then + ret = ret(self) + end + return ret +end + return Skill diff --git a/lua/core/skill_type/active.lua b/lua/core/skill_type/active.lua index 0fdc04f9..eac20145 100644 --- a/lua/core/skill_type/active.lua +++ b/lua/core/skill_type/active.lua @@ -26,8 +26,8 @@ end -- 判断该技能是否可主动发动 ---@param player Player @ 使用者 ----@param card Card @ 牌 ----@param extra_data UseExtraData @ 额外数据 +---@param card? Card @ 牌,若该技能是卡牌的效果技能,需输入此值 +---@param extra_data? UseExtraData @ 额外数据 ---@return bool function ActiveSkill:canUse(player, card, extra_data) return self:isEffectable(player) @@ -36,7 +36,7 @@ end -- 判断一张牌是否可被此技能选中 ---@param to_select integer @ 待选牌 ---@param selected integer[] @ 已选牌 ----@param selected_targets integer[] @ 已选目标 +---@param selected_targets? integer[] @ 已选目标 ---@return bool function ActiveSkill:cardFilter(to_select, selected, selected_targets) return true @@ -46,8 +46,8 @@ end ---@param to_select integer @ 待选目标 ---@param selected integer[] @ 已选目标 ---@param selected_cards integer[] @ 已选牌 ----@param card Card @ 牌 ----@param extra_data UseExtraData @ 额外数据 +---@param card? Card @ 牌 +---@param extra_data? UseExtraData @ 额外数据 ---@return bool function ActiveSkill:targetFilter(to_select, selected, selected_cards, card, extra_data) return false @@ -83,8 +83,8 @@ function ActiveSkill:getMinTargetNum() end -- 获得技能的最大目标数 ----@param player Player @ 使用者 ----@param card Card @ 牌 +---@param player? Player @ 使用者 +---@param card? Card @ 牌 ---@return number @ 最大目标数 function ActiveSkill:getMaxTargetNum(player, card) local ret @@ -99,11 +99,13 @@ function ActiveSkill:getMaxTargetNum(player, card) ret = ret[#ret] end - local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or Util.DummyTable - for _, skill in ipairs(status_skills) do - local correct = skill:getExtraTargetNum(player, self, card) - if correct == nil then correct = 0 end - ret = ret + correct + if player and card then + local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or Util.DummyTable + for _, skill in ipairs(status_skills) do + local correct = skill:getExtraTargetNum(player, self, card) + if correct == nil then correct = 0 end + ret = ret + correct + end end return ret end @@ -217,7 +219,7 @@ end ---@param selected integer[] @ 已选目标 ---@param selected_cards integer[] @ 已选牌 ---@param player Player @ 使用者 ----@param card Card @ 牌 +---@param card? Card @ 牌 ---@return bool function ActiveSkill:feasible(selected, selected_cards, player, card) return #selected >= self:getMinTargetNum() and #selected <= self:getMaxTargetNum(player, card) @@ -254,4 +256,12 @@ function ActiveSkill:onEffect(room, cardEffectEvent) end ---@param cardEffectEvent CardEffectEvent | SkillEffectEvent function ActiveSkill:onNullified(room, cardEffectEvent) end +---@param to_select integer @ id of the target +---@param selected integer[] @ ids of selected targets +---@param selected_cards integer[] @ ids of selected cards +---@param card Card @ helper +---@param selectable boolean @can be selected +---@param extra_data? any @ extra_data +function ActiveSkill:targetTip(to_select, selected, selected_cards, card, selectable, extra_data) end + return ActiveSkill diff --git a/lua/core/skill_type/target_mod.lua b/lua/core/skill_type/target_mod.lua index 2c7a628b..142cd758 100644 --- a/lua/core/skill_type/target_mod.lua +++ b/lua/core/skill_type/target_mod.lua @@ -40,4 +40,13 @@ function TargetModSkill:getExtraTargetNum(player, card_skill, card) return 0 end +---@param player Player +---@param to_select integer @ id of the target +---@param selected integer[] @ ids of selected targets +---@param selected_cards integer[] @ ids of selected cards +---@param card Card @ helper +---@param selectable boolean @can be selected +---@param extra_data? any @ extra_data +function TargetModSkill:getTargetTip(player, to_select, selected, selected_cards, card, selectable, extra_data) end + return TargetModSkill diff --git a/lua/core/skill_type/trigger.lua b/lua/core/skill_type/trigger.lua index 83b890f9..6c94820b 100644 --- a/lua/core/skill_type/trigger.lua +++ b/lua/core/skill_type/trigger.lua @@ -72,9 +72,17 @@ function TriggerSkill:doCost(event, target, player, data) self.cost_data = cost_data_bak if ret then + local skill_data = {cost_data = cost_data_bak, tos = {}, cards = {}} + if type(cost_data_bak) == "table" then + if type(cost_data_bak.tos) == "table" and #cost_data_bak.tos > 0 and type(cost_data_bak.tos[1]) == "number" and + room:getPlayerById(cost_data_bak.tos[1]) ~= nil then + skill_data.tos = cost_data_bak.tos + end + if type(cost_data_bak.cards) == "table" then skill_data.cards = cost_data_bak.cards end + end return room:useSkill(player, self, function() return self:use(event, target, player, data) - end) + end, skill_data) end end diff --git a/lua/core/skill_type/visibility.lua b/lua/core/skill_type/visibility.lua new file mode 100644 index 00000000..912f3045 --- /dev/null +++ b/lua/core/skill_type/visibility.lua @@ -0,0 +1,22 @@ +-- SPDX-License-Identifier: GPL-3.0-or-later + +-- UI专用状态技 表示某张牌、某人身份等可能需要在界面上隐藏的元素是否能被某人看到 +-- 默认情况参见client_util.lua: CardVisibility 和player.role_shown + +---@class VisibilitySkill : StatusSkill +local VisibilitySkill = StatusSkill:subclass("VisibilitySkill") + +---@param player Player +---@param card Card +---@return bool +function VisibilitySkill:cardVisible(player, card) + return nil +end + +---@param player Player +---@return bool +function VisibilitySkill:roleVisible(player, target) + return nil +end + +return VisibilitySkill diff --git a/lua/core/util.lua b/lua/core/util.lua index 3d2f2234..77ffd362 100644 --- a/lua/core/util.lua +++ b/lua/core/util.lua @@ -7,6 +7,13 @@ Util.FalseFunc = function() return false end Util.DummyTable = setmetatable({}, { __newindex = function() error("Cannot assign to dummy table") end }) +Util.array2hash = function(t) + local ret = {} + for _, e in ipairs(t) do + ret[e] = true + end + return ret +end local metamethods = { "__add", "__sub", "__mul", "__div", "__mod", "__pow", "__unm", "__idiv", @@ -133,6 +140,23 @@ Util.Name2SkillMapper = function(e) return Fk.skills[e] end --- 返回译文 Util.TranslateMapper = function(str) return Fk:translate(str) end +-- 阶段int型和string型互换 +---@return string|integer +Util.PhaseStrMapper = function(phase) + local phase_table = { + [Player.RoundStart] = "phase_roundstart", + [Player.Start] = "phase_start", + [Player.Judge] = "phase_judge", + [Player.Draw] = "phase_draw", + [Player.Play] = "phase_play", + [Player.Discard] = "phase_discard", + [Player.Finish] = "phase_finish", + [Player.NotActive] = "phase_notactive", + [Player.PhaseNone] = "phase_phasenone", + } + return type(phase) == "string" and table.indexOf(phase_table, phase) or phase_table[phase] +end + -- for card preset --- 全局卡牌(包括自己)的canUse @@ -249,14 +273,16 @@ function table:contains(element) end end -function table:shuffle() +function table:shuffle(seed) + seed = seed or math.random(2 << 32 - 1) + local rnd = fk.QRandomGenerator(seed) if #self == 2 then - if math.random() < 0.5 then + if rnd:random() < 0.5 then self[1], self[2] = self[2], self[1] end else for i = #self, 2, -1 do - local j = math.random(i) + local j = rnd:random(i) self[i], self[j] = self[j], self[i] end end @@ -414,6 +440,17 @@ function table:assign(targetTbl) end end +function table:hasIntersection(table) + local hash = {} + for _, value in ipairs(self) do + hash[value] = true + end + for _, value in ipairs(table) do + if hash[value] then return true end + end + return false +end + function table.empty(t) return next(t) == nil end diff --git a/lua/fk_ex.lua b/lua/fk_ex.lua index 1c1e6b5a..f53a5bdc 100644 --- a/lua/fk_ex.lua +++ b/lua/fk_ex.lua @@ -18,6 +18,7 @@ MaxCardsSkill = require "core.skill_type.max_cards" TargetModSkill = require "core.skill_type.target_mod" FilterSkill = require "core.skill_type.filter" InvaliditySkill = require "lua.core.skill_type.invalidity" +VisibilitySkill = require "lua.core.skill_type.visibility" BasicCard = require "core.card_type.basic" local Trick = require "core.card_type.trick" @@ -71,6 +72,7 @@ local function readUsableSpecToSkill(skill, spec) } skill.distance_limit = spec.distance_limit or skill.distance_limit skill.expand_pile = spec.expand_pile + skill.times = spec.times or skill.times end local function readStatusSpecToSkill(skill, spec) @@ -85,6 +87,7 @@ end ---@field public max_turn_use_time? integer ---@field public max_round_use_time? integer ---@field public max_game_use_time? integer +---@field public times? integer | fun(self: UsableSkill): integer ---@class StatusSkillSpec: StatusSkill @@ -214,6 +217,7 @@ function fk.CreateActiveSkill(spec) if spec.on_effect then skill.onEffect = spec.on_effect end if spec.on_nullified then skill.onNullified = spec.on_nullified end if spec.prompt then skill.prompt = spec.prompt end + if spec.target_tip then skill.targetTip = spec.target_tip end if spec.interaction then skill.interaction = setmetatable({}, { @@ -237,7 +241,7 @@ end ---@field public enabled_at_response? fun(self: ViewAsSkill, player: Player, response: boolean): boolean? ---@field public before_use? fun(self: ViewAsSkill, player: ServerPlayer, use: CardUseStruct): string? ---@field public after_use? fun(self: ViewAsSkill, player: ServerPlayer, use: CardUseStruct): string? ----@field public prompt? string|fun(self: ActiveSkill, selected: integer[], selected_cards: integer[]): string +---@field public prompt? string|fun(self: ActiveSkill, selected_cards: integer[], selected: integer[]): string ---@param spec ViewAsSkillSpec ---@return ViewAsSkill @@ -392,6 +396,7 @@ end ---@field public bypass_distances? fun(self: TargetModSkill, player: Player, skill: ActiveSkill, card: Card, to: Player): boolean? ---@field public distance_limit_func? fun(self: TargetModSkill, player: Player, skill: ActiveSkill, card: Card, to: Player): number? ---@field public extra_target_func? fun(self: TargetModSkill, player: Player, skill: ActiveSkill, card: Card): number? +---@field public target_tip_func? fun(self: TargetModSkill, player: Player, to_select: integer, selected: integer[], selected_cards: integer[], card: Card, selectable: boolean, extra_data: any): string|TargetTipDataSpec? ---@param spec TargetModSpec ---@return TargetModSkill @@ -415,6 +420,9 @@ function fk.CreateTargetModSkill(spec) if spec.extra_target_func then skill.getExtraTargetNum = spec.extra_target_func end + if spec.target_tip_func then + skill.getTargetTip = spec.target_tip_func + end return skill end @@ -453,6 +461,22 @@ function fk.CreateInvaliditySkill(spec) return skill end +---@class VisibilitySpec: StatusSkillSpec +---@field public card_visible? fun(self: VisibilitySkill, player: Player, card: Card): boolean? +---@field public role_visible? fun(self: VisibilitySkill, player: Player, target: Player): boolean? + +---@param spec VisibilitySpec +function fk.CreateVisibilitySkill(spec) + assert(type(spec.name) == "string") + + local skill = VisibilitySkill:new(spec.name) + readStatusSpecToSkill(skill, spec) + if spec.card_visible then skill.cardVisible = spec.card_visible end + if spec.role_visible then skill.roleVisible = spec.role_visible end + + return skill +end + ---@class CardSpec: Card ---@field public skill? Skill ---@field public equip_skill? Skill @@ -663,3 +687,11 @@ end ---@field qml_path string | fun(player: Player, data: any): string ---@field update_func? fun(player: ServerPlayer, data: any) ---@field default_choice? fun(player: ServerPlayer, data: any): any + +---@class TargetTipDataSpec +---@field content string +---@field type "normal"|"warning" + +---@class TargetTipSpec +---@field name string +---@field target_tip fun(self: ActiveSkill, to_select: integer, selected: integer[], selected_cards: integer[], card: Card, selectable: boolean, extra_data: any): string|TargetTipDataSpec? diff --git a/lua/freekill.lua b/lua/freekill.lua index 1ab4c6e6..0ed8b43d 100644 --- a/lua/freekill.lua +++ b/lua/freekill.lua @@ -55,7 +55,8 @@ UsableSkill = require "core.skill_type.usable_skill" StatusSkill = require "core.skill_type.status_skill" Player = require "core.player" GameMode = require "core.game_mode" -AbstractRoom = require "core.abstract_room" +AbstractRoom = require "core.room.abstract_room" +RequestHandler = require "core.request_handler" UI = require "ui-util" -- 读取配置文件。 diff --git a/lua/lib/debugger.lua b/lua/lib/debugger.lua index b60100e0..8a9b0470 100644 --- a/lua/lib/debugger.lua +++ b/lua/lib/debugger.lua @@ -244,6 +244,9 @@ local function where(info, context_lines) source = {} local filename = info.source:match("@(.*)") if filename then + if UsingNewCore and (filename:startsWith("./lua/") or filename:startsWith("lua/")) then + filename = "./packages/freekill-core/" .. filename + end pcall(function() for line in io.lines(filename) do table.insert(source, line) end end) elseif info.source then for line in info.source:gmatch("(.-)\n") do table.insert(source, line) end diff --git a/lua/lsp/lib.lua b/lua/lsp/lib.lua index 63a64e5b..5540212c 100644 --- a/lua/lsp/lib.lua +++ b/lua/lsp/lib.lua @@ -35,6 +35,8 @@ function Object:isInstanceOf(class) end ---@return boolean function Object:isSubclassOf(class) end +function Object:include(e) end + ---@class json json = {} diff --git a/lua/server/ai/ai.lua b/lua/server/ai/ai.lua index 12c6d56a..844c3851 100644 --- a/lua/server/ai/ai.lua +++ b/lua/server/ai/ai.lua @@ -34,11 +34,12 @@ end function AI:makeReply() Self = self.player - local start = os.getms() + -- local start = os.getms() local ret = self.cb_table[self.command] and self.cb_table[self.command](self, self.jsonData) or "__cancel" - local to_delay = 500 - (os.getms() - start) / 1000 + if ret == "" then ret = "__cancel" end + -- local to_delay = 500 - (os.getms() - start) / 1000 -- print(to_delay) - self.room:delay(to_delay) + -- self.room:delay(to_delay) return ret end diff --git a/lua/server/ai/random_ai.lua b/lua/server/ai/random_ai.lua index 6a5ca098..2ef243a8 100644 --- a/lua/server/ai/random_ai.lua +++ b/lua/server/ai/random_ai.lua @@ -6,75 +6,118 @@ local RandomAI = AI:subclass("RandomAI") ---@param self RandomAI ---@param skill ActiveSkill ---@param card? Card -function RandomAI:useActiveSkill(skill, card) +---@param extra_data? table +function RandomAI:useActiveSkill(skill, card, extra_data) local room = self.room local player = self.player + extra_data = extra_data or Util.DummyTable if skill:isInstanceOf(ViewAsSkill) then return "" end - local filter_func = skill.cardFilter - if card then - filter_func = Util.FalseFunc + if self.command == "PlayCard" and (not skill:canUse(player, card) or (card and player:prohibitUse(card))) then + return "" end - if self.command == "PlayCard" and (not skill:canUse(player, card) or player:prohibitUse(card)) then - return "" + local interaction_data + if skill and skill.interaction then + skill.interaction.data = nil + interaction_data = skill:interaction() + if type(interaction_data) == "table" then + if interaction_data.type == "spin" then + interaction_data = math.random(interaction_data.from, interaction_data.to) + elseif interaction_data.type == "combo" then + interaction_data = interaction_data.default + else + -- use default data when handling custom interaction + interaction_data = interaction_data.default or interaction_data.default_choice or nil + end + end + if interaction_data == nil then return "" end + skill.interaction.data = interaction_data end local max_try_times = 100 local selected_targets = {} local selected_cards = {} - -- FIXME: ... - -- local min = skill:getMinTargetNum() - -- local max = skill:getMaxTargetNum(player, card) - -- local min_card = skill:getMinCardNum() - -- local max_card = skill:getMaxCardNum() - -- FIXME: ViewAsSkill can be buggy here - for _ = 0, max_try_times do - if skill:feasible(selected_targets, selected_cards, self.player, card) then break end - local avail_targets = table.filter(room:getAlivePlayers(), function(p) - local ret = skill:targetFilter(p.id, selected_targets, selected_cards, card or Fk:cloneCard'zixing') - if ret and card then - if player:isProhibited(p, card) then - ret = false - end - end - return ret - end) - avail_targets = table.map(avail_targets, function(p) return p.id end) - local avail_cards = table.filter(player:getCardIds{ Player.Hand, Player.Equip }, function(id) - return filter_func(skill, id, selected_cards, selected_targets) - end) - if #avail_targets == 0 and #avail_cards == 0 then break end - table.insertIfNeed(selected_targets, table.random(avail_targets)) - table.insertIfNeed(selected_cards, table.random(avail_cards)) + -- TODO: ng that 'must_targets' & 'exclusive_targets' should be rebuilt later + local limited_targets = {} + for _, name in ipairs({"must_targets","exclusive_targets","include_targets"}) do + if type(extra_data[name]) == "table" then + table.insertTableIfNeed(limited_targets, extra_data[name]) + end end - if skill:feasible(selected_targets, selected_cards, self.player, card) then + + local all_cards = player:getCardIds{ Player.Hand, Player.Equip } + if skill.expand_pile then + if type(skill.expand_pile) == "string" then + table.insertTableIfNeed(all_cards, player.special_cards[skill.expand_pile] or {}) + elseif type(skill.expand_pile) == "table" then + table.insertTableIfNeed(all_cards, skill.expand_pile) + end + end + + --local max_target_num = skill:getMaxTargetNum(player, card) + local card_filter_func = card and Util.FalseFunc or skill.cardFilter + local firstTry + for _ = 0, max_try_times do + if not firstTry and skill:feasible(selected_targets, selected_cards, self.player, card) then + firstTry = {table.simpleClone(selected_targets), table.simpleClone(selected_cards)} + end + if firstTry and math.random() < 0.1 then break end + local avail_targets = table.filter(room.alive_players, function(p) + return not table.contains(selected_targets, p.id) and (#limited_targets == 0 or table.contains(limited_targets, p.id)) + and skill:targetFilter(p.id, selected_targets, selected_cards, card) + and (not card or not player:isProhibited(p, card)) + end) + local avail_cards = table.filter(all_cards, function(id) + return not table.contains(selected_cards, id) and card_filter_func(skill, id, selected_cards, selected_targets) + end) + local random_list = table.connect(avail_targets, avail_cards) + if #random_list == 0 then break end + local randomIndex = math.random(#random_list) + if randomIndex <= #avail_targets then + table.insertIfNeed(selected_targets, random_list[randomIndex].id) + else + table.insertIfNeed(selected_cards, random_list[randomIndex]) + end + end + local feasibleCheck = function () return skill:feasible(selected_targets, selected_cards, self.player, card) end + if firstTry and not feasibleCheck() then + selected_targets = firstTry[1] + selected_cards = firstTry[2] + end + if feasibleCheck() then local ret = json.encode{ card = card and card.id or json.encode{ skill = skill.name, subcards = selected_cards, }, targets = selected_targets, + interaction_data = interaction_data, } - -- print(ret) return ret end return "" end ----@param self RandomAI + ---@param skill ViewAsSkill -function RandomAI:useVSSkill(skill, pattern, cancelable, extra_data) +---@param pattern? string @ no 'pattern' means it needs to pass the 'canUse' check +---@param cancelable? bool +---@param extra_data? table +---@param cardResponsing? bool +function RandomAI:useVSSkill(skill, pattern, cancelable, extra_data, cardResponsing) local player = self.player local room = self.room local precondition - if not skill then return nil end + cancelable = cancelable or (cancelable == nil) + extra_data = extra_data or Util.DummyTable + if not skill then return "" end - if self.command == "PlayCard" then + if not pattern then precondition = skill:enabledAtPlay(player) - if not precondition then return nil end + if not precondition then return "" end local exp = Exppattern:Parse(skill.pattern) local cnames = {} for _, m in ipairs(exp.matchers) do @@ -82,35 +125,107 @@ function RandomAI:useVSSkill(skill, pattern, cancelable, extra_data) end for _, n in ipairs(cnames) do local c = Fk:cloneCard(n) - precondition = c.skill:canUse(Self, c) + precondition = c.skill:canUse(player, c, extra_data) if precondition then break end end else - precondition = skill:enabledAtResponse(player) - if not precondition then return nil end - local exp = Exppattern:Parse(pattern) - precondition = exp:matchExp(skill.pattern) + precondition = skill:enabledAtResponse(player, cardResponsing) and Exppattern:Parse(pattern):matchExp(skill.pattern) end - if (not precondition) or math.random() < 0.2 then return nil end + if (not precondition) or (cancelable and math.random() < 0.2) then return "" end + + local interaction_data + if skill.interaction then + skill.interaction.data = nil + interaction_data = skill:interaction() + if type(interaction_data) == "table" then + if interaction_data.type == "spin" then + interaction_data = math.random(interaction_data.from, interaction_data.to) + elseif interaction_data.type == "combo" then + interaction_data = interaction_data.default + else + -- use default data when handling custom interaction + interaction_data = interaction_data.default or interaction_data.default_choice or nil + end + end + if interaction_data == nil then return "" end + skill.interaction.data = interaction_data + end local selected_cards = {} + local selected_targets = {} + local card local max_try_time = 100 + local all_cards = player:getCardIds{ Player.Hand, Player.Equip } + if skill.expand_pile then + if type(skill.expand_pile) == "string" then + table.insertTableIfNeed(all_cards, player.special_cards[skill.expand_pile] or {}) + elseif type(skill.expand_pile) == "table" then + table.insertTableIfNeed(all_cards, skill.expand_pile) + end + end for _ = 0, max_try_time do - local avail_cards = table.filter(player:getCardIds{ Player.Hand, Player.Equip }, function(id) - return skill:cardFilter(id, selected_cards) + card = skill:viewAs(selected_cards) + if card then break end + local avail_cards = table.filter(all_cards, function(id) + return not table.contains(selected_cards, id) and skill:cardFilter(id, selected_cards) end) if #avail_cards == 0 then break end table.insert(selected_cards, table.random(avail_cards)) - if skill:viewAs(selected_cards) then - return { - skill = skill.name, - subcards = selected_cards, + end + + if not card then return "" end + + if cardResponsing then + if not player:prohibitResponse(card) then + return json.encode{ + card = json.encode{ + skill = skill.name, + subcards = selected_cards, + }, + targets = {}, + interaction_data = interaction_data, } end + return "" end - return nil + + if player:prohibitUse(card) then return "" end + + if pattern or player:canUse(card, extra_data) then + + local limited_targets = {} + for _, name in ipairs({"must_targets","exclusive_targets","include_targets"}) do + if type(extra_data[name]) == "table" then + table.insertTableIfNeed(limited_targets, extra_data[name]) + end + end + + for _ = 0, max_try_time do + if card.skill:feasible(selected_targets, selected_cards, player, card) then break end + local avail_targets = table.filter(room.alive_players, function(p) + return not table.contains(selected_targets, p.id) and (#limited_targets == 0 or table.contains(limited_targets, p.id)) + and card.skill:targetFilter(p.id, selected_targets, selected_cards, card, extra_data) + and not player:isProhibited(p, card) + end) + if #avail_targets == 0 then break end + table.insert(selected_targets, table.random(avail_targets).id) + end + if card.skill:feasible(selected_targets, selected_cards, player, card, extra_data) then + local ret = json.encode{ + card = json.encode{ + skill = skill.name, + subcards = selected_cards, + }, + targets = selected_targets, + interaction_data = interaction_data, + } + return ret + end + end + + return "" end ---@type table @@ -119,6 +234,7 @@ local random_cb = {} random_cb["AskForUseActiveSkill"] = function(self, jsonData) local data = json.decode(jsonData) local skill = Fk.skills[data[1]] + if not skill then return "" end local cancelable = data[3] if cancelable and math.random() < 0.25 then return "" end local extra_data = data[4] @@ -126,13 +242,47 @@ random_cb["AskForUseActiveSkill"] = function(self, jsonData) skill[k] = v end if skill:isInstanceOf(ViewAsSkill) then - return RandomAI.useVSSkill(skill) + return self:useVSSkill(skill, nil, cancelable, extra_data) end - return RandomAI.useActiveSkill(self, skill) + local player = self.player + if skill.name == "choose_cards_skill" then + local exp = Exppattern:Parse(extra_data.pattern) + local cards = table.filter(player:getCardIds(extra_data.include_equip and "he" or "h"), function(cid) + return exp:match(Fk:getCardById(cid)) + end) + local maxNum = extra_data.num + local minNum = extra_data.min_num + cards = table.random(cards, math.random(minNum, maxNum)) + return json.encode{ + card = json.encode{ + skill = skill.name, + subcards = cards, + }, + targets = {}, + } + end + return self:useActiveSkill(skill) end random_cb["AskForSkillInvoke"] = function(self, jsonData) - return table.random{"1", ""} + local skill_name, prompt = table.unpack(json.decode(jsonData)) + local chance = 0.55 + if Fk.skills[skill_name] ~= nil and self.player:hasSkill(skill_name) then + chance = 0.8 + end + if math.random() < chance then + return "1" + end + return "" +end + +random_cb["AskForChoice"] = function(self, jsonData) + local data = json.decode(jsonData) + local choices = data[1] + if table.contains(choices, "Cancel") and #choices > 1 and math.random() < 0.6 then + table.removeOne(choices, "Cancel") + end + return table.random(choices) end random_cb["AskForUseCard"] = function(self, jsonData) @@ -140,61 +290,74 @@ random_cb["AskForUseCard"] = function(self, jsonData) local data = json.decode(jsonData) local card_name = data[1] local pattern = data[2] or card_name - local cancelable = data[4] or true - local exp = Exppattern:Parse(pattern) + local prompt = data[3] + local cancelable = data[4] + local extra_data = data[5] or Util.DummyTable - local avail_cards = table.map(player:getCardIds("he"), Util.Id2CardMapper) - avail_cards = table.filter(avail_cards, function(c) - return exp:match(c) and not player:prohibitUse(c) - end) - if #avail_cards > 0 then - if math.random() < 0.25 then return "" end - for _, card in ipairs(avail_cards) do - local skill = card.skill - local max_try_times = 100 - local selected_targets = {} - local min = skill:getMinTargetNum() - local max = skill:getMaxTargetNum(player, card) - local min_card = skill:getMinCardNum() - local max_card = skill:getMaxCardNum() - for _ = 0, max_try_times do - if skill:feasible(selected_targets, { card.id }, self.player, card) then - return json.encode{ - card = table.random(avail_cards).id, - targets = selected_targets, - } - end - local avail_targets = table.filter(self.room:getAlivePlayers(), function(p) - return skill:targetFilter(p.id, selected_targets, {card.id}, card or Fk:cloneCard'zixing') - end) - avail_targets = table.map(avail_targets, function(p) return p.id end) - - if #avail_targets == 0 and #avail_cards == 0 then break end - table.insertIfNeed(selected_targets, table.random(avail_targets)) - end + if card_name == "peach" then + if type(extra_data.must_targets) == "table" and extra_data.must_targets[1] ~= player.id and math.random() < 0.8 then + return "" end end + + if (cancelable and math.random() < 0.15) then return "" end + + local cards = table.map(self.player:getCardIds("he&"), Util.Id2CardMapper) + local exp = Exppattern:Parse(pattern) + cards = table.filter(cards, function(c) + return exp:match(c) and not player:prohibitUse(c) + end) + local vss = table.filter(player:getAllSkills(), function(s) + return s:isInstanceOf(ViewAsSkill) + end) + table.insertTable(cards, vss) + + while #cards > 0 do + local sth = table.remove(cards, math.random(#cards)) + if sth:isInstanceOf(Card) then + local ret = self:useActiveSkill(sth.skill, sth, extra_data) + if ret ~= "" then return ret end + else + local ret = self:useVSSkill(sth, pattern, cancelable, extra_data) + if ret ~= "" then return ret end + end + end + return "" end random_cb["AskForResponseCard"] = function(self, jsonData) local data = json.decode(jsonData) local pattern = data[2] - local cancelable = true + local cancelable = data[4] or true + local extra_data = data[5] or Util.DummyTable + local player = self.player + + local cards = table.map(self.player:getCardIds("he&"), Util.Id2CardMapper) local exp = Exppattern:Parse(pattern) - local avail_cards = table.filter(self.player:getCardIds{ Player.Hand, Player.Equip }, function(id) - return exp:match(Fk:getCardById(id)) + cards = table.filter(cards, function(c) + return exp:match(c) and not player:prohibitResponse(c) end) - if #avail_cards > 0 then return json.encode{ - card = table.random(avail_cards), - targets = {}, - } end - -- TODO: vs skill + + local vss = table.filter(player:getAllSkills(), function(s) + return s:isInstanceOf(ViewAsSkill) + end) + table.insertTable(cards, vss) + + while #cards > 0 do + local sth = table.remove(cards, math.random(#cards)) + if sth:isInstanceOf(Card) then + return json.encode{ card = sth.id, targets = {} } + else + local ret = self:useVSSkill(sth, pattern, cancelable, extra_data, true) + if ret ~= "" then return ret end + end + end return "" end random_cb["PlayCard"] = function(self, jsonData) - local cards = table.map(self.player:getCardIds(Player.Hand), Util.Id2CardMapper) + local cards = table.map(self.player:getCardIds("h&"), Util.Id2CardMapper) local actives = table.filter(self.player:getAllSkills(), function(s) return s:isInstanceOf(ActiveSkill) end) @@ -205,16 +368,13 @@ random_cb["PlayCard"] = function(self, jsonData) table.insertTable(cards, vss) while #cards > 0 do - local sth = table.random(cards) + local sth = table.remove(cards, math.random(#cards)) if sth:isInstanceOf(Card) then local card = sth local skill = card.skill ---@type ActiveSkill if math.random() > 0.15 then local ret = RandomAI.useActiveSkill(self, skill, card) if ret ~= "" then return ret end - table.removeOne(cards, card) - else - table.removeOne(cards, card) end elseif sth:isInstanceOf(ActiveSkill) then local active = sth @@ -222,14 +382,12 @@ random_cb["PlayCard"] = function(self, jsonData) local ret = RandomAI.useActiveSkill(self, active, nil) if ret ~= "" then return ret end end - table.removeOne(cards, active) else local vs = sth if math.random() > 0.20 then local ret = self:useVSSkill(vs) - -- TODO: handle vs result + if ret ~= "" then return ret end end - table.removeOne(cards, vs) end end diff --git a/lua/server/event.lua b/lua/server/event.lua index 1a977f02..2abb7bbf 100644 --- a/lua/server/event.lua +++ b/lua/server/event.lua @@ -12,6 +12,7 @@ fk.BeforeTurnStart = 83 fk.TurnStart = 3 fk.TurnEnd = 73 fk.AfterTurnEnd = 84 +fk.EventTurnChanging = 96 fk.EventPhaseStart = 4 fk.EventPhaseProceeding = 5 fk.EventPhaseEnd = 6 @@ -95,6 +96,10 @@ fk.GameFinished = 66 fk.AskForCardUse = 67 fk.AskForCardResponse = 68 +fk.HandleAskForPlayCard = 97 +fk.AfterAskForCardUse = 98 +fk.AfterAskForCardResponse = 99 +fk.AfterAskForNullification = 100 fk.StartPindian = 69 fk.PindianCardsDisplayed = 70 @@ -138,4 +143,10 @@ fk.AfterPropertyChange = 94 fk.AfterPlayerRevived = 95 -fk.NumOfEvents = 96 +-- 96 = EventTurnChanging +-- 97 = HandleAskForPlayCard +-- 98 = AfterAskForCardUse +-- 99 = AfterAskForCardResponse +-- 100 = AfterAskForNullification + +fk.NumOfEvents = 101 diff --git a/lua/server/events/death.lua b/lua/server/events/death.lua index 84ff8d5a..3c9978e4 100644 --- a/lua/server/events/death.lua +++ b/lua/server/events/death.lua @@ -1,5 +1,15 @@ -- SPDX-License-Identifier: GPL-3.0-or-later +---@class DeathEventWrappers: Object +local DeathEventWrappers = {} -- mixin + +---@return boolean +local function exec(tp, ...) + local event = tp:create(...) + local _, ret = event:exec() + return ret +end + ---@class GameEvent.Dying : GameEvent local Dying = GameEvent:subclass("GameEvent.Dying") function Dying:main() @@ -43,6 +53,12 @@ function Dying:exit() logic:trigger(fk.AfterDying, dyingPlayer, dyingStruct, self.interrupted) end +--- 根据濒死数据让人进入濒死。 +---@param dyingStruct DyingStruct +function DeathEventWrappers:enterDying(dyingStruct) + return exec(Dying, dyingStruct) +end + ---@class GameEvent.Death : GameEvent local Death = GameEvent:subclass("GameEvent.Death") function Death:prepare() @@ -103,6 +119,12 @@ function Death:main() logic:trigger(fk.Deathed, victim, deathStruct) end +--- 根据死亡数据杀死角色。 +---@param deathStruct DeathStruct +function DeathEventWrappers:killPlayer(deathStruct) + return exec(Death, deathStruct) +end + ---@class GameEvent.Revive : GameEvent local Revive = GameEvent:subclass("GameEvent.Revive") function Revive:main() @@ -125,4 +147,10 @@ function Revive:main() room.logic:trigger(fk.AfterPlayerRevived, player, { reason = reason }) end -return { Dying, Death, Revive } +---@param player ServerPlayer +---@param sendLog? bool +function DeathEventWrappers:revivePlayer(player, sendLog, reason) + return exec(Revive, player, sendLog, reason) +end + +return { Dying, Death, Revive, DeathEventWrappers } diff --git a/lua/server/events/gameflow.lua b/lua/server/events/gameflow.lua index 7da6960b..433a97c8 100644 --- a/lua/server/events/gameflow.lua +++ b/lua/server/events/gameflow.lua @@ -80,61 +80,20 @@ function DrawInitial:main() return end - room:setTag("LuckCardData", luck_data) room:notifyMoveFocus(room.alive_players, "AskForLuckCard") - room:doBroadcastNotify("AskForLuckCard", room.settings.luckTime or 4) - room.room:setRequestTimer(room.timeout * 1000 + 1000) - - local remainTime = room.timeout + 1 - local currentTime = os.time() - local elapsed = 0 - - for _, id in ipairs(luck_data.playerList) do - local pl = room:getPlayerById(id) - if luck_data[id].luckTime > 0 then - pl.serverplayer:setThinking(true) - end + local request = Request:new("AskForSkillInvoke", room.alive_players) + for _, p in ipairs(room.alive_players) do + request:setData(p, { "AskForLuckCard", "#AskForLuckCard:::" .. room.settings.luckTime }) end - - while true do - elapsed = os.time() - currentTime - if remainTime - elapsed <= 0 then - break - end - - -- local ldata = room:getTag("LuckCardData") - local ldata = luck_data - - if table.every(ldata.playerList, function(id) - return ldata[id].luckTime == 0 - end) then - break - end - - for _, id in ipairs(ldata.playerList) do - local pl = room:getPlayerById(id) - if pl._splayer:getState() ~= fk.Player_Online then - ldata[id].luckTime = 0 - pl.serverplayer:setThinking(false) - end - end - - -- room:setTag("LuckCardData", ldata) - - room:checkNoHuman() - - coroutine.yield("__handleRequest", (remainTime - elapsed) * 1000) - end - - room.room:destroyRequestTimer() + request.luck_data = luck_data + request.accept_cancel = true + request:ask() for _, player in ipairs(room.alive_players) do local draw_data = luck_data[player.id] draw_data.luckTime = nil room.logic:trigger(fk.AfterDrawInitialCards, player, draw_data) end - - room:removeTag("LuckCardData") end ---@class GameEvent.Round : GameEvent @@ -143,12 +102,27 @@ local Round = GameEvent:subclass("GameEvent.Round") function Round:action() local room = self.room local p + local nextTurnOwner + local skipRoundPlus = false repeat + nextTurnOwner = nil + skipRoundPlus = false p = room.current GameEvent.Turn:create(p):exec() if room.game_finished then break end - room.current = room.current:getNextAlive(true, nil, true) - until p.seat >= p:getNextAlive(true, nil, true).seat + + local changingData = { from = room.current, to = room.current:getNextAlive(true, nil, true), skipRoundPlus = false } + room.logic:trigger(fk.EventTurnChanging, room.current, changingData, true) + + skipRoundPlus = changingData.skipRoundPlus + local nextAlive = room.current:getNextAlive(true, nil, true) + if nextAlive ~= changingData.to and not changingData.to.dead then + room.current = changingData.to + nextTurnOwner = changingData.to + else + room.current = nextAlive + end + until p.seat >= (nextTurnOwner or p:getNextAlive(true, nil, true)).seat and not skipRoundPlus end function Round:main() @@ -269,6 +243,8 @@ function Turn:clear() logic:trigger(fk.AfterTurnEnd, current, nil, self.interrupted) current.phase = Player.NotActive + room:setTag("endTurn", false) + for _, p in ipairs(room.players) do p:setCardUseHistory("", 0, Player.HistoryTurn) p:setSkillUseHistory("", 0, Player.HistoryTurn) @@ -317,6 +293,7 @@ function Phase:main() [Player.Judge] = function() local cards = player:getCardIds(Player.Judge) while #cards > 0 do + if player._phase_end then break end local cid = table.remove(cards) if not cid then return end local card = player:removeVirtualEquip(cid) @@ -344,12 +321,17 @@ function Phase:main() n = 2 } room.logic:trigger(fk.DrawNCards, player, data) - room:drawCards(player, data.n, "game_rule") + if not player._phase_end then + room:drawCards(player, data.n, "game_rule") + end room.logic:trigger(fk.AfterDrawNCards, player, data) end, [Player.Play] = function() player._play_phase_end = false + room:doBroadcastNotify("UpdateSkill", "", {player}) + while not player.dead do + if player._phase_end then break end logic:trigger(fk.StartPlayCard, player, nil, true) room:notifyMoveFocus(player, "PlayCard") local result = room:doRequest(player, "PlayCard", player.id) @@ -359,14 +341,10 @@ function Phase:main() if type(useResult) == "table" then room:useCard(useResult) end - - if player._play_phase_end then - player._play_phase_end = false - break - end end end, [Player.Discard] = function() + if player._phase_end then return end local discardNum = #table.filter( player:getCardIds(Player.Hand), function(id) local card = Fk:getCardById(id) @@ -408,6 +386,7 @@ function Phase:clear() room:setPlayerMark(p, name, 0) end end + p._phase_end = false end for cid, cmark in pairs(room.card_marks) do diff --git a/lua/server/events/hp.lua b/lua/server/events/hp.lua index 15523009..148a0a76 100644 --- a/lua/server/events/hp.lua +++ b/lua/server/events/hp.lua @@ -1,32 +1,43 @@ -- SPDX-License-Identifier: GPL-3.0-or-later -local damage_nature_table = { - [fk.NormalDamage] = "normal_damage", - [fk.FireDamage] = "fire_damage", - [fk.ThunderDamage] = "thunder_damage", - [fk.IceDamage] = "ice_damage", -} +---@class HpEventWrappers: Object +local HpEventWrappers = {} -- mixin + +---@return boolean +local function exec(tp, ...) + local event = tp:create(...) + local _, ret = event:exec() + return ret +end + +-- local damage_nature_table = { +-- [fk.NormalDamage] = "normal_damage", +-- [fk.FireDamage] = "fire_damage", +-- [fk.ThunderDamage] = "thunder_damage", +-- [fk.IceDamage] = "ice_damage", +-- } local function sendDamageLog(room, damageStruct) + local damageName = Fk:getDamageNatureName(damageStruct.damageType) if damageStruct.from then room:sendLog{ type = "#Damage", to = {damageStruct.from.id}, from = damageStruct.to.id, arg = damageStruct.damage, - arg2 = damage_nature_table[damageStruct.damageType], + arg2 = damageName, } else room:sendLog{ type = "#DamageWithNoFrom", from = damageStruct.to.id, arg = damageStruct.damage, - arg2 = damage_nature_table[damageStruct.damageType], + arg2 = damageName, } end room:sendLogEvent("Damage", { to = damageStruct.to.id, - damageType = damage_nature_table[damageStruct.damageType], + damageType = damageName, damageNum = damageStruct.damage, }) end @@ -51,6 +62,17 @@ function ChangeHp:main() } if reason == "damage" then + if damageStruct then + if Fk:canChain(damageStruct.damageType) and damageStruct.to.chained then + damageStruct.to:setChainState(false) + if not damageStruct.chain then + damageStruct.beginnerOfTheDamage = true + damageStruct.chain_table = table.filter(room:getOtherPlayers(damageStruct.to), function(p) + return p.chained + end) + end + end + end data.shield_lost = math.min(-num, player.shield) data.num = num + data.shield_lost end @@ -114,6 +136,17 @@ function ChangeHp:main() return true end +--- 改变一名玩家的体力。 +---@param player ServerPlayer @ 玩家 +---@param num integer @ 变化量 +---@param reason? string @ 原因 +---@param skillName? string @ 技能名 +---@param damageStruct? DamageStruct @ 伤害数据 +---@return boolean +function HpEventWrappers:changeHp(player, num, reason, skillName, damageStruct) + return exec(ChangeHp, player, num, reason, skillName, damageStruct) +end + ---@class GameEvent.Damage : GameEvent local Damage = GameEvent:subclass("GameEvent.Damage") function Damage:main() @@ -178,11 +211,6 @@ function Damage:main() end end - if damageStruct.damageType ~= fk.NormalDamage and damageStruct.to.chained then - if not damageStruct.chain then damageStruct.beginnerOfTheDamage = true end - damageStruct.to:setChainState(false) - end - if not room:changeHp( damageStruct.to, -damageStruct.damage, @@ -213,11 +241,11 @@ function Damage:exit() logic:trigger(fk.DamageFinished, damageStruct.to, damageStruct) - if damageStruct.beginnerOfTheDamage and not damageStruct.chain then - local targets = table.filter(room:getOtherPlayers(damageStruct.to), function(p) - return p.chained + if damageStruct.chain_table and #damageStruct.chain_table > 0 then + damageStruct.chain_table = table.filter(damageStruct.chain_table, function(p) + return p:isAlive() and p.chained end) - for _, p in ipairs(targets) do + for _, p in ipairs(damageStruct.chain_table) do room:sendLog{ type = "#ChainDamage", from = p.id @@ -238,6 +266,13 @@ function Damage:exit() end end +--- 根据伤害数据造成伤害。 +---@param damageStruct DamageStruct +---@return boolean +function HpEventWrappers:damage(damageStruct) + return exec(Damage, damageStruct) +end + ---@class GameEvent.LoseHp : GameEvent local LoseHp = GameEvent:subclass("GameEvent.LoseHp") function LoseHp:main() @@ -268,6 +303,15 @@ function LoseHp:main() return true end +--- 令一名玩家失去体力。 +---@param player ServerPlayer @ 玩家 +---@param num integer @ 失去的数量 +---@param skillName? string @ 技能名 +---@return boolean +function HpEventWrappers:loseHp(player, num, skillName) + return exec(LoseHp, player, num, skillName) +end + ---@class GameEvent.Recover : GameEvent local Recover = GameEvent:subclass("GameEvent.Recover") function Recover:prepare() @@ -316,6 +360,13 @@ function Recover:main() return true end +--- 根据回复数据回复体力。 +---@param recoverStruct RecoverStruct +---@return boolean +function HpEventWrappers:recover(recoverStruct) + return exec(Recover, recoverStruct) +end + ---@class GameEvent.ChangeMaxHp : GameEvent local ChangeMaxHp = GameEvent:subclass("GameEvent.ChangeMaxHp") function ChangeMaxHp:main() @@ -374,4 +425,12 @@ function ChangeMaxHp:main() return true end -return { ChangeHp, Damage, LoseHp, Recover, ChangeMaxHp } +--- 改变一名玩家的体力上限。 +---@param player ServerPlayer @ 玩家 +---@param num integer @ 变化量 +---@return boolean +function HpEventWrappers:changeMaxHp(player, num) + return exec(ChangeMaxHp, player, num) +end + +return { ChangeHp, Damage, LoseHp, Recover, ChangeMaxHp, HpEventWrappers } diff --git a/lua/server/events/init.lua b/lua/server/events/init.lua index 4575b665..41536a49 100644 --- a/lua/server/events/init.lua +++ b/lua/server/events/init.lua @@ -1,3 +1,4 @@ +---@diagnostic disable -- SPDX-License-Identifier: GPL-3.0-or-later -- Definitions of game events @@ -5,11 +6,16 @@ -- 某类事件对应的结束事件,其id刚好就是那个事件的相反数 -- GameEvent.EventFinish = -1 +--- 给Room.lua瘦身:一系列GameEvent的封装 +---@class GameEventWrappers: MiscEventWrappers, HpEventWrappers, DeathEventWrappers, MoveEventWrappers, UseCardEventWrappers, SkillEventWrappers, JudgeEventWrappers, PindianEventWrappers +local GameEventWrappers = {} -- mixin + local tmp tmp = require "server.events.misc" GameEvent.Game = tmp[1] GameEvent.ChangeProperty = tmp[2] GameEvent.ClearEvent = tmp[3] +table.assign(GameEventWrappers, tmp[4]) tmp = require "server.events.hp" GameEvent.ChangeHp = tmp[1] @@ -17,25 +23,31 @@ GameEvent.Damage = tmp[2] GameEvent.LoseHp = tmp[3] GameEvent.Recover = tmp[4] GameEvent.ChangeMaxHp = tmp[5] +table.assign(GameEventWrappers, tmp[6]) tmp = require "server.events.death" GameEvent.Dying = tmp[1] GameEvent.Death = tmp[2] GameEvent.Revive = tmp[3] +table.assign(GameEventWrappers, tmp[4]) tmp = require "server.events.movecard" -GameEvent.MoveCards = tmp +GameEvent.MoveCards = tmp[1] +table.assign(GameEventWrappers, tmp[2]) tmp = require "server.events.usecard" GameEvent.UseCard = tmp[1] GameEvent.RespondCard = tmp[2] GameEvent.CardEffect = tmp[3] +table.assign(GameEventWrappers, tmp[4]) tmp = require "server.events.skill" -GameEvent.SkillEffect = tmp +GameEvent.SkillEffect = tmp[1] +table.assign(GameEventWrappers, tmp[2]) tmp = require "server.events.judge" -GameEvent.Judge = tmp +GameEvent.Judge = tmp[1] +table.assign(GameEventWrappers, tmp[2]) tmp = require "server.events.gameflow" GameEvent.DrawInitial = tmp[1] @@ -44,7 +56,8 @@ GameEvent.Turn = tmp[3] GameEvent.Phase = tmp[4] tmp = require "server.events.pindian" -GameEvent.Pindian = tmp +GameEvent.Pindian = tmp[1] +table.assign(GameEventWrappers, tmp[2]) for _, l in ipairs(Fk._custom_events) do local name, p, m, c, e = l.name, l.p, l.m, l.c, l.e @@ -53,3 +66,5 @@ for _, l in ipairs(Fk._custom_events) do GameEvent.cleaners[name] = c GameEvent.exit_funcs[name] = e end + +return GameEventWrappers diff --git a/lua/server/events/judge.lua b/lua/server/events/judge.lua index b561be9b..23602633 100644 --- a/lua/server/events/judge.lua +++ b/lua/server/events/judge.lua @@ -1,5 +1,15 @@ -- SPDX-License-Identifier: GPL-3.0-or-later +---@class JudgeEventWrappers: Object +local JudgeEventWrappers = {} -- mixin + +---@return boolean +local function exec(tp, ...) + local event = tp:create(...) + local _, ret = event:exec() + return ret +end + ---@class GameEvent.Judge : GameEvent local Judge = GameEvent:subclass("GameEvent.Judge") function Judge:main() @@ -74,4 +84,66 @@ function Judge:clear() }) end -return Judge +-- 判定 + +--- 根据判定数据进行判定。判定的结果直接保存在这个数据中。 +---@param data JudgeStruct +function JudgeEventWrappers:judge(data) + return exec(Judge, data) +end + +--- 改判。 +---@param card Card @ 改判的牌 +---@param player ServerPlayer @ 改判的玩家 +---@param judge JudgeStruct @ 要被改判的判定数据 +---@param skillName? string @ 技能名 +---@param exchange? boolean @ 是否要替换原有判定牌(即类似鬼道那样) +function JudgeEventWrappers:retrial(card, player, judge, skillName, exchange) + if not card then return end + local triggerResponded = self.owner_map[card:getEffectiveId()] == player + local isHandcard = (triggerResponded and self:getCardArea(card:getEffectiveId()) == Card.PlayerHand) + + if triggerResponded then + local resp = {} ---@type CardResponseEvent + resp.from = player.id + resp.card = card + resp.skipDrop = true + self:responseCard(resp) + else + local move1 = {} ---@type CardsMoveInfo + move1.ids = { card:getEffectiveId() } + move1.from = player.id + move1.toArea = Card.Processing + move1.moveReason = fk.ReasonJustMove + move1.skillName = skillName + self:moveCards(move1) + end + + local oldJudge = judge.card + judge.card = card + local rebyre = judge.retrial_by_response + judge.retrial_by_response = player + + self:sendLog{ + type = "#ChangedJudge", + from = player.id, + to = { judge.who.id }, + arg2 = card:toLogString(), + arg = skillName, + } + + Fk:filterCard(judge.card.id, judge.who, judge) + + exchange = exchange and not player.dead + + local move2 = {} ---@type CardsMoveInfo + move2.ids = { oldJudge:getEffectiveId() } + move2.toArea = exchange and Card.PlayerHand or Card.DiscardPile + move2.moveReason = exchange and fk.ReasonJustMove or fk.ReasonJudge + move2.to = exchange and player.id or nil + move2.skillName = skillName + + self:moveCards(move2) +end + +return { Judge, JudgeEventWrappers } diff --git a/lua/server/events/misc.lua b/lua/server/events/misc.lua index f6e42f16..8e599d13 100644 --- a/lua/server/events/misc.lua +++ b/lua/server/events/misc.lua @@ -1,5 +1,8 @@ -- SPDX-License-Identifier: GPL-3.0-or-later +---@class MiscEventWrappers: Object +local MiscEventWrappers = {} -- mixin + ---@class GameEvent.Game : GameEvent local Game = GameEvent:subclass("GameEvent.Game") function Game:main() @@ -129,6 +132,63 @@ function ChangeProperty:main() logic:trigger(fk.AfterPropertyChange, player, data) end +---@param player ServerPlayer @ 要换将的玩家 +---@param new_general string @ 要变更的武将,若不存在则变身为孙策,孙策不存在变身为士兵 +---@param full? boolean @ 是否血量满状态变身 +---@param isDeputy? boolean @ 是否变的是副将 +---@param sendLog? boolean @ 是否发Log +---@param maxHpChange? boolean @ 是否改变体力上限,默认改变 +function MiscEventWrappers:changeHero(player, new_general, full, isDeputy, sendLog, maxHpChange, kingdomChange) + local new = Fk.generals[new_general] or Fk.generals["sunce"] or Fk.generals["blank_shibing"] + + kingdomChange = (kingdomChange == nil) and true or kingdomChange + local kingdom = (isDeputy or not kingdomChange) and player.kingdom or new.kingdom + if not isDeputy and kingdomChange then + local allKingdoms = {} + if new.subkingdom then + allKingdoms = { new.kingdom, new.subkingdom } + else + allKingdoms = Fk:getKingdomMap(new.kingdom) + end + if #allKingdoms > 0 then + kingdom = self:askForChoice(player, allKingdoms, "AskForKingdom", "#ChooseInitialKingdom") + end + end + + ChangeProperty:create({ + from = player, + general = not isDeputy and new_general or nil, + deputyGeneral = isDeputy and new_general or nil, + gender = isDeputy and player.gender or new.gender, + kingdom = kingdom, + sendLog = sendLog, + results = {}, + }):exec() + + maxHpChange = (maxHpChange == nil) and true or maxHpChange + if maxHpChange then + self:setPlayerProperty(player, "maxHp", player:getGeneralMaxHp()) + end + if full or player.hp > player.maxHp then + self:setPlayerProperty(player, "hp", player.maxHp) + end +end + +---@param player ServerPlayer @ 要变更势力的玩家 +---@param kingdom string @ 要变更的势力 +---@param sendLog? boolean @ 是否发Log +function MiscEventWrappers:changeKingdom(player, kingdom, sendLog) + if kingdom == player.kingdom then return end + sendLog = sendLog or false + + ChangeProperty:create({ + from = player, + kingdom = kingdom, + sendLog = sendLog, + results = {}, + }):exec() +end + ---@class GameEvent.ClearEvent : GameEvent local ClearEvent = GameEvent:subclass("GameEvent.ClearEvent") function ClearEvent:main() @@ -154,4 +214,4 @@ function ClearEvent:main() logic.cleaner_stack:pop() end -return { Game, ChangeProperty, ClearEvent } +return { Game, ChangeProperty, ClearEvent, MiscEventWrappers } diff --git a/lua/server/events/movecard.lua b/lua/server/events/movecard.lua index e6c4a2c3..086bb8e3 100644 --- a/lua/server/events/movecard.lua +++ b/lua/server/events/movecard.lua @@ -1,5 +1,15 @@ -- SPDX-License-Identifier: GPL-3.0-or-later +---@class MoveEventWrappers: Object +local MoveEventWrappers = {} -- mixin + +---@return boolean +local function exec(tp, ...) + local event = tp:create(...) + local _, ret = event:exec() + return ret +end + ---@class GameEvent.MoveCards : GameEvent local MoveCards = GameEvent:subclass("GameEvent.MoveCards") function MoveCards:main() @@ -102,58 +112,7 @@ function MoveCards:main() ---@param info MoveInfo for _, info in ipairs(data.moveInfo) do - local realFromArea = room:getCardArea(info.cardId) - local playerAreas = { Player.Hand, Player.Equip, Player.Judge, Player.Special } - - if table.contains(playerAreas, realFromArea) and data.from then - local from = room:getPlayerById(data.from) - from:removeCards(realFromArea, { info.cardId }, info.fromSpecialName) - - elseif realFromArea ~= Card.Unknown then - local fromAreaIds = {} - if realFromArea == Card.Processing then - fromAreaIds = room.processing_area - elseif realFromArea == Card.DrawPile then - fromAreaIds = room.draw_pile - elseif realFromArea == Card.DiscardPile then - fromAreaIds = room.discard_pile - elseif realFromArea == Card.Void then - fromAreaIds = room.void - end - - table.removeOne(fromAreaIds, info.cardId) - end - - if table.contains(playerAreas, data.toArea) and data.to then - local to = room:getPlayerById(data.to) - to:addCards(data.toArea, { info.cardId }, data.specialName) - - else - local toAreaIds = {} - if data.toArea == Card.Processing then - toAreaIds = room.processing_area - elseif data.toArea == Card.DrawPile then - toAreaIds = room.draw_pile - elseif data.toArea == Card.DiscardPile then - toAreaIds = room.discard_pile - elseif data.toArea == Card.Void then - toAreaIds = room.void - end - - if data.toArea == Card.DrawPile then - local putIndex = data.drawPilePosition or 1 - if putIndex == -1 then - putIndex = #room.draw_pile + 1 - elseif putIndex < 1 or putIndex > #room.draw_pile + 1 then - putIndex = 1 - end - - table.insert(toAreaIds, putIndex, info.cardId) - else - table.insert(toAreaIds, info.cardId) - end - end - room:setCardArea(info.cardId, data.toArea, data.to) + room:applyMoveInfo(data, info) if data.toArea == Card.DrawPile or realFromArea == Card.DrawPile then room:doBroadcastNotify("UpdateDrawPile", #room.draw_pile) end @@ -212,4 +171,259 @@ function MoveCards:main() return true end -return MoveCards +--- 传入一系列移牌信息,去实际移动这些牌 +---@vararg CardsMoveInfo +---@return boolean? +function MoveEventWrappers:moveCards(...) + return exec(MoveCards, ...) +end + +--- 让一名玩家获得一张牌 +---@param player integer|ServerPlayer @ 要拿牌的玩家 +---@param card integer|integer[]|Card|Card[] @ 要拿到的卡牌 +---@param unhide? boolean @ 是否明着拿 +---@param reason? CardMoveReason @ 卡牌移动的原因 +---@param proposer? integer @ 移动操作者的id +---@param skill_name? string @ 技能名 +---@param moveMark? table|string @ 移动后自动赋予标记,格式:{标记名(支持-inarea后缀,移出值代表区域后清除), 值} +---@param visiblePlayers? integer|integer[] @ 控制移动对特定角色可见(在moveVisible为false时生效) +function MoveEventWrappers:obtainCard(player, card, unhide, reason, proposer, skill_name, moveMark, visiblePlayers) + local pid = type(player) == "number" and player or player.id + self:moveCardTo(card, Card.PlayerHand, player, reason, skill_name, nil, unhide, proposer or pid, moveMark, visiblePlayers) +end + +--- 让玩家摸牌 +---@param player ServerPlayer @ 摸牌的玩家 +---@param num integer @ 摸牌数 +---@param skillName? string @ 技能名 +---@param fromPlace? string @ 摸牌的位置,"top" 或者 "bottom" +---@param moveMark? table|string @ 移动后自动赋予标记,格式:{标记名(支持-inarea后缀,移出值代表区域后清除), 值} +---@return integer[] @ 摸到的牌 +function MoveEventWrappers:drawCards(player, num, skillName, fromPlace, moveMark) + local drawData = { + who = player, + num = num, + skillName = skillName, + fromPlace = fromPlace, + } + if self.logic:trigger(fk.BeforeDrawCard, player, drawData) then + return {} + end + + num = drawData.num + fromPlace = drawData.fromPlace + player = drawData.who + + local topCards = self:getNCards(num, fromPlace) + self:moveCards({ + ids = topCards, + to = player.id, + toArea = Card.PlayerHand, + moveReason = fk.ReasonDraw, + proposer = player.id, + skillName = skillName, + moveMark = moveMark, + }) + + return { table.unpack(topCards) } +end + +--- 将一张或多张牌移动到某处 +---@param card integer | integer[] | Card | Card[] @ 要移动的牌 +---@param to_place integer @ 移动的目标位置 +---@param target? ServerPlayer|integer @ 移动的目标角色 +---@param reason? integer @ 移动时使用的移牌原因 +---@param skill_name? string @ 技能名 +---@param special_name? string @ 私人牌堆名 +---@param visible? boolean @ 是否明置 +---@param proposer? integer @ 移动操作者的id +---@param moveMark? table|string @ 移动后自动赋予标记,格式:{标记名(支持-inarea后缀,移出值代表区域后清除), 值} +---@param visiblePlayers? integer|integer[] @ 控制移动对特定角色可见(在moveVisible为false时生效) +function MoveEventWrappers:moveCardTo(card, to_place, target, reason, skill_name, special_name, visible, proposer, moveMark, visiblePlayers) + reason = reason or fk.ReasonJustMove + skill_name = skill_name or "" + special_name = special_name or "" + local ids = Card:getIdList(card) + + local to + if table.contains( + {Card.PlayerEquip, Card.PlayerHand, + Card.PlayerJudge, Card.PlayerSpecial}, to_place) then + assert(target) + if type(target) == "number" then + to = target + else + to = target.id + end + end + + local movesSplitedByOwner = {} + for _, cardId in ipairs(ids) do + local moveFound = table.find(movesSplitedByOwner, function(move) + return move.from == self.owner_map[cardId] + end) + + if moveFound then + table.insert(moveFound.ids, cardId) + else + table.insert(movesSplitedByOwner, { + ids = { cardId }, + from = self.owner_map[cardId], + to = to, + toArea = to_place, + moveReason = reason, + skillName = skill_name, + specialName = special_name, + moveVisible = visible, + proposer = proposer, + moveMark = moveMark, + visiblePlayers = visiblePlayers, + }) + end + end + + self:moveCards(table.unpack(movesSplitedByOwner)) +end + +--- 弃置一名角色的牌。 +---@param card_ids integer[]|integer|Card|Card[] @ 被弃掉的牌 +---@param skillName? string @ 技能名 +---@param who ServerPlayer @ 被弃牌的人 +---@param thrower? ServerPlayer @ 弃别人牌的人 +function MoveEventWrappers:throwCard(card_ids, skillName, who, thrower) + skillName = skillName or "" + thrower = thrower or who + self:moveCards({ + ids = Card:getIdList(card_ids), + from = who.id, + toArea = Card.DiscardPile, + moveReason = fk.ReasonDiscard, + proposer = thrower.id, + skillName = skillName + }) +end + +--- 重铸一名角色的牌。 +---@param card_ids integer[] @ 被重铸的牌 +---@param who ServerPlayer @ 重铸的角色 +---@param skillName? string @ 技能名,默认为“重铸” +---@return integer[] @ 摸到的牌 +function MoveEventWrappers:recastCard(card_ids, who, skillName) + if type(card_ids) == "number" then + card_ids = {card_ids} + end + skillName = skillName or "recast" + self:moveCards({ + ids = card_ids, + from = who.id, + toArea = Card.DiscardPile, + skillName = skillName, + moveReason = fk.ReasonRecast, + proposer = who.id + }) + self:sendFootnote(card_ids, { + type = "##RecastCard", + from = who.id, + }) + self:broadcastPlaySound("./audio/system/recast") + self:sendLog{ + type = skillName == "recast" and "#Recast" or "#RecastBySkill", + from = who.id, + card = card_ids, + arg = skillName, + } + return self:drawCards(who, #card_ids, skillName) +end + +--- 将一些卡牌同时分配给一些角色。 +---@param room Room @ 房间 +---@param list table @ 分配牌和角色的数据表,键为角色id,值为分配给其的牌id数组 +---@param proposer? integer @ 操作者的id。默认为空 +---@param skillName? string @ 技能名。默认为“分配” +---@return table @ 返回成功分配的卡牌 +function MoveEventWrappers:doYiji(room, list, proposer, skillName) + skillName = skillName or "distribution_skill" + local moveInfos = {} + local move_ids = {} + for to, cards in pairs(list) do + local toP = room:getPlayerById(to) + local handcards = toP:getCardIds("h") + cards = table.filter(cards, function (id) return not table.contains(handcards, id) end) + if #cards > 0 then + table.insertTable(move_ids, cards) + local moveMap = {} + local noFrom = {} + for _, id in ipairs(cards) do + local from = room.owner_map[id] + if from then + moveMap[from] = moveMap[from] or {} + table.insert(moveMap[from], id) + else + table.insert(noFrom, id) + end + end + for from, _cards in pairs(moveMap) do + table.insert(moveInfos, { + ids = _cards, + moveInfo = table.map(_cards, function(id) + return {cardId = id, fromArea = room:getCardArea(id), fromSpecialName = room:getPlayerById(from):getPileNameOfId(id)} + end), + from = from, + to = to, + toArea = Card.PlayerHand, + moveReason = fk.ReasonGive, + proposer = proposer, + skillName = skillName, + }) + end + if #noFrom > 0 then + table.insert(moveInfos, { + ids = noFrom, + to = to, + toArea = Card.PlayerHand, + moveReason = fk.ReasonGive, + proposer = proposer, + skillName = skillName, + }) + end + end + end + if #moveInfos > 0 then + room:moveCards(table.unpack(moveInfos)) + end + return move_ids +end + +--- 将一张牌移动至某角色的装备区,若不合法则置入弃牌堆。目前没做相同副类别装备同时置入的适配(甘露神典韦) +---@param target ServerPlayer @ 接受牌的角色 +---@param cards integer|integer[] @ 移动的牌 +---@param skillName? string @ 技能名 +---@param convert? boolean @ 是否可以替换装备(默认可以) +---@param proposer? ServerPlayer @ 操作者 +function MoveEventWrappers:moveCardIntoEquip(target, cards, skillName, convert, proposer) + convert = (convert == nil) and true or convert + skillName = skillName or "" + cards = type(cards) == "table" and cards or {cards} + local moves = {} + for _, cardId in ipairs(cards) do + local card = Fk:getCardById(cardId) + local fromId = self.owner_map[cardId] + local proposerId = proposer and proposer.id or nil + if target:canMoveCardIntoEquip(cardId, convert) then + if target:hasEmptyEquipSlot(card.sub_type) then + table.insert(moves,{ids = {cardId}, from = fromId, to = target.id, toArea = Card.PlayerEquip, moveReason = fk.ReasonPut,skillName = skillName,proposer = proposerId}) + else + local existingEquip = target:getEquipments(card.sub_type) + local throw = #existingEquip == 1 and existingEquip[1] or + self:askForCardChosen(proposer or target, target, {card_data = { {Util.convertSubtypeAndEquipSlot(card.sub_type),existingEquip} } }, "replaceEquip","#replaceEquip") + table.insert(moves,{ids = {throw}, from = target.id, toArea = Card.DiscardPile, moveReason = fk.ReasonPutIntoDiscardPile, skillName = skillName,proposer = proposerId}) + table.insert(moves,{ids = {cardId}, from = fromId, to = target.id, toArea = Card.PlayerEquip, moveReason = fk.ReasonPut,skillName = skillName,proposer = proposerId}) + end + else + table.insert(moves,{ids = {cardId}, from = fromId, toArea = Card.DiscardPile, moveReason = fk.ReasonPutIntoDiscardPile,skillName = skillName}) + end + end + self:moveCards(table.unpack(moves)) +end + +return { MoveCards, MoveEventWrappers } diff --git a/lua/server/events/pindian.lua b/lua/server/events/pindian.lua index 64c1bec1..0d4d4ad9 100644 --- a/lua/server/events/pindian.lua +++ b/lua/server/events/pindian.lua @@ -1,5 +1,15 @@ -- SPDX-License-Identifier: GPL-3.0-or-later +---@class PindianEventWrappers: Object +local PindianEventWrappers = {} -- mixin + +---@return boolean +local function exec(tp, ...) + local event = tp:create(...) + local _, ret = event:exec() + return ret +end + ---@class GameEvent.Pindian : GameEvent local Pindian = GameEvent:subclass("GameEvent.Pindian") function Pindian:main() @@ -190,4 +200,11 @@ function Pindian:clear() if not self.interrupted then return end end -return Pindian + +--- 根据拼点信息开始拼点。 +---@param pindianData PindianStruct +function PindianEventWrappers:pindian(pindianData) + return exec(Pindian, pindianData) +end + +return { Pindian, PindianEventWrappers } diff --git a/lua/server/events/skill.lua b/lua/server/events/skill.lua index df4ffea4..be30874f 100644 --- a/lua/server/events/skill.lua +++ b/lua/server/events/skill.lua @@ -1,25 +1,187 @@ -- SPDX-License-Identifier: GPL-3.0-or-later +---@class SkillEventWrappers: Object +local SkillEventWrappers = {} -- mixin + +---@return boolean +local function exec(tp, ...) + local event = tp:create(...) + local _, ret = event:exec() + return ret +end + ---@class GameEvent.SkillEffect : GameEvent local SkillEffect = GameEvent:subclass("GameEvent.SkillEffect") function SkillEffect:main() - local effect_cb, player, _skill = table.unpack(self.data) + local effect_cb, player, skill, skill_data = table.unpack(self.data) local room = self.room local logic = room.logic - local skill = _skill.main_skill and _skill.main_skill or _skill + local main_skill = skill.main_skill and skill.main_skill or skill + skill_data = skill_data or Util.DummyTable + local cost_data = skill_data.cost_data or Util.DummyTable - if player then - player:addSkillUseHistory(skill.name) + if player and not skill.cardSkill then + player:revealBySkillName(main_skill.name) + + local tos = skill_data.tos or {} + local mute, no_indicate = skill.mute, skill.no_indicate + if type(cost_data) == "table" then + if cost_data.mute then mute = cost_data.mute end + if cost_data.no_indicate then no_indicate = cost_data.no_indicate end + end + if not mute then + if skill.attached_equip then + local equip = Fk.all_card_types[skill.attached_equip] + if equip then + local pkgPath = "./packages/" .. equip.package.extensionName + local soundName = pkgPath .. "/audio/card/" .. equip.name + room:broadcastPlaySound(soundName) + if not no_indicate and #tos > 0 then + room:sendLog{ + type = "#InvokeSkillTo", + from = player.id, + arg = skill.name, + to = tos, + } + else + room:sendLog{ + type = "#InvokeSkill", + from = player.id, + arg = skill.name, + } + end + room:setEmotion(player, pkgPath .. "/image/anim/" .. equip.name) + end + else + player:broadcastSkillInvoke(skill.name) + room:notifySkillInvoked(player, skill.name, nil, no_indicate and {} or tos) + end + end + if not no_indicate then + room:doIndicate(player.id, tos) + end + + if skill:isSwitchSkill() then + local switchSkillName = skill.switchSkillName + room:setPlayerMark( + player, + MarkEnum.SwithSkillPreName .. switchSkillName, + player:getSwitchSkillState(switchSkillName, true) + ) + end + + player:addSkillUseHistory(main_skill.name) end local cost_data_bak = skill.cost_data - logic:trigger(fk.SkillEffect, player, skill) + logic:trigger(fk.SkillEffect, player, main_skill) skill.cost_data = cost_data_bak - local ret = effect_cb() - - logic:trigger(fk.AfterSkillEffect, player, skill) + local ret = effect_cb and effect_cb() or false + logic:trigger(fk.AfterSkillEffect, player, main_skill) return ret end -return SkillEffect +--- 使用技能。先增加技能发动次数,再执行相应的函数。 +---@param player ServerPlayer @ 发动技能的玩家 +---@param skill Skill @ 发动的技能 +---@param effect_cb fun() @ 实际要调用的函数 +---@param skill_data? table @ 技能的信息 +function SkillEventWrappers:useSkill(player, skill, effect_cb, skill_data) + return exec(SkillEffect, effect_cb, player, skill, skill_data or Util.DummyTable) +end + +--- 令一名玩家获得/失去技能。 +--- +--- skill_names 是字符串数组或者用管道符号(|)分割的字符串。 +--- +--- 每个skill_name都是要获得的技能的名。如果在skill_name前面加上"-",那就是失去技能。 +---@param player ServerPlayer @ 玩家 +---@param skill_names string[] | string @ 要获得/失去的技能 +---@param source_skill? string | Skill @ 源技能 +---@param no_trigger? boolean @ 是否不触发相关时机 +function SkillEventWrappers:handleAddLoseSkills(player, skill_names, source_skill, sendlog, no_trigger) + if type(skill_names) == "string" then + skill_names = skill_names:split("|") + end + + if sendlog == nil then sendlog = true end + + if #skill_names == 0 then return end + local losts = {} ---@type boolean[] + local triggers = {} ---@type Skill[] + local lost_piles = {} ---@type integer[] + for _, skill in ipairs(skill_names) do + if string.sub(skill, 1, 1) == "-" then + local actual_skill = string.sub(skill, 2, #skill) + if player:hasSkill(actual_skill, true, true) then + local lost_skills = player:loseSkill(actual_skill, source_skill) + for _, s in ipairs(lost_skills) do + self:doBroadcastNotify("LoseSkill", json.encode{ + player.id, + s.name + }) + + if sendlog and s.visible then + self:sendLog{ + type = "#LoseSkill", + from = player.id, + arg = s.name + } + end + + table.insert(losts, true) + table.insert(triggers, s) + if s.derived_piles then + for _, pile_name in ipairs(s.derived_piles) do + table.insertTableIfNeed(lost_piles, player:getPile(pile_name)) + end + end + end + end + else + local sk = Fk.skills[skill] + if sk and not player:hasSkill(sk, true, true) then + local got_skills = player:addSkill(sk, source_skill) + + for _, s in ipairs(got_skills) do + -- TODO: limit skill mark + + self:doBroadcastNotify("AddSkill", json.encode{ + player.id, + s.name + }) + + if sendlog and s.visible then + self:sendLog{ + type = "#AcquireSkill", + from = player.id, + arg = s.name + } + end + + table.insert(losts, false) + table.insert(triggers, s) + end + end + end + end + + if (not no_trigger) and #triggers > 0 then + for i = 1, #triggers do + local event = losts[i] and fk.EventLoseSkill or fk.EventAcquireSkill + self.logic:trigger(event, player, triggers[i]) + end + end + + if #lost_piles > 0 then + self:moveCards({ + ids = lost_piles, + from = player.id, + toArea = Card.DiscardPile, + moveReason = fk.ReasonPutIntoDiscardPile, + }) + end +end + +return { SkillEffect, SkillEventWrappers } diff --git a/lua/server/events/usecard.lua b/lua/server/events/usecard.lua index 03cea326..3a8a9e27 100644 --- a/lua/server/events/usecard.lua +++ b/lua/server/events/usecard.lua @@ -1,5 +1,15 @@ -- SPDX-License-Identifier: GPL-3.0-or-later +---@class UseCardEventWrappers: Object +local UseCardEventWrappers = {} -- mixin + +---@return boolean +local function exec(tp, ...) + local event = tp:create(...) + local _, ret = event:exec() + return ret +end + local playCardEmotionAndSound = function(room, player, card) if card.type ~= Card.TypeEquip then local anim_path = "./packages/" .. card.package.extensionName .. "/image/anim/" .. card.name @@ -397,4 +407,512 @@ function CardEffect:main() end end -return { UseCard, RespondCard, CardEffect } + +--- 根据卡牌使用数据,去实际使用这个卡牌。 +---@param cardUseEvent CardUseStruct @ 使用数据 +---@return boolean +function UseCardEventWrappers:useCard(cardUseEvent) + return exec(UseCard, cardUseEvent) +end + +---@param room Room +---@param cardUseEvent CardUseStruct +---@param aimEventCollaborators table +---@return boolean +local onAim = function(room, cardUseEvent, aimEventCollaborators) + local eventStages = { fk.TargetSpecifying, fk.TargetConfirming, fk.TargetSpecified, fk.TargetConfirmed } + for _, stage in ipairs(eventStages) do + if (not cardUseEvent.tos) or #cardUseEvent.tos == 0 then + return false + end + + room:sortPlayersByAction(cardUseEvent.tos, true) + local aimGroup = AimGroup:initAimGroup(TargetGroup:getRealTargets(cardUseEvent.tos)) + + local collaboratorsIndex = {} + local firstTarget = true + repeat + local toId = AimGroup:getUndoneOrDoneTargets(aimGroup)[1] + ---@type AimStruct + local aimStruct + local initialEvent = false + collaboratorsIndex[toId] = collaboratorsIndex[toId] or 1 + + if not aimEventCollaborators[toId] or collaboratorsIndex[toId] > #aimEventCollaborators[toId] then + aimStruct = { + from = cardUseEvent.from, + card = cardUseEvent.card, + to = toId, + targetGroup = cardUseEvent.tos, + nullifiedTargets = cardUseEvent.nullifiedTargets or {}, + tos = aimGroup, + firstTarget = firstTarget, + additionalDamage = cardUseEvent.additionalDamage, + additionalRecover = cardUseEvent.additionalRecover, + additionalEffect = cardUseEvent.additionalEffect, + extra_data = cardUseEvent.extra_data, + } + + local index = 1 + for _, targets in ipairs(cardUseEvent.tos) do + if index > collaboratorsIndex[toId] then + break + end + + if #targets > 1 then + for i = 2, #targets do + aimStruct.subTargets = {} + table.insert(aimStruct.subTargets, targets[i]) + end + end + end + + collaboratorsIndex[toId] = 1 + initialEvent = true + else + aimStruct = aimEventCollaborators[toId][collaboratorsIndex[toId]] + aimStruct.from = cardUseEvent.from + aimStruct.card = cardUseEvent.card + aimStruct.tos = aimGroup + aimStruct.targetGroup = cardUseEvent.tos + aimStruct.nullifiedTargets = cardUseEvent.nullifiedTargets or {} + aimStruct.firstTarget = firstTarget + aimStruct.additionalEffect = cardUseEvent.additionalEffect + aimStruct.extra_data = cardUseEvent.extra_data + end + + firstTarget = false + + room.logic:trigger(stage, (stage == fk.TargetSpecifying or stage == fk.TargetSpecified) and room:getPlayerById(aimStruct.from) or room:getPlayerById(aimStruct.to), aimStruct) + + AimGroup:removeDeadTargets(room, aimStruct) + + local aimEventTargetGroup = aimStruct.targetGroup + if aimEventTargetGroup then + room:sortPlayersByAction(aimEventTargetGroup, true) + end + + cardUseEvent.from = aimStruct.from + cardUseEvent.tos = aimEventTargetGroup + cardUseEvent.nullifiedTargets = aimStruct.nullifiedTargets + cardUseEvent.additionalEffect = aimStruct.additionalEffect + cardUseEvent.extra_data = aimStruct.extra_data + + if #AimGroup:getAllTargets(aimStruct.tos) == 0 then + return false + end + + local cancelledTargets = AimGroup:getCancelledTargets(aimStruct.tos) + if #cancelledTargets > 0 then + for _, target in ipairs(cancelledTargets) do + aimEventCollaborators[target] = {} + collaboratorsIndex[target] = 1 + end + end + aimStruct.tos[AimGroup.Cancelled] = {} + + aimEventCollaborators[toId] = aimEventCollaborators[toId] or {} + if room:getPlayerById(toId):isAlive() then + if initialEvent then + table.insert(aimEventCollaborators[toId], aimStruct) + else + aimEventCollaborators[toId][collaboratorsIndex[toId]] = aimStruct + end + + collaboratorsIndex[toId] = collaboratorsIndex[toId] + 1 + end + + AimGroup:setTargetDone(aimStruct.tos, toId) + aimGroup = aimStruct.tos + until #AimGroup:getUndoneOrDoneTargets(aimGroup) == 0 + end + + return true +end + +--- 对卡牌使用数据进行生效 +---@param cardUseEvent CardUseStruct +function UseCardEventWrappers:doCardUseEffect(cardUseEvent) + ---@type table + local aimEventCollaborators = {} + if cardUseEvent.tos and not onAim(self, cardUseEvent, aimEventCollaborators) then + return + end + + local realCardIds = self:getSubcardsByRule(cardUseEvent.card, { Card.Processing }) + + self.logic:trigger(fk.BeforeCardUseEffect, self:getPlayerById(cardUseEvent.from), cardUseEvent) + -- If using Equip or Delayed trick, move them to the area and return + if cardUseEvent.card.type == Card.TypeEquip then + if #realCardIds == 0 then + return + end + + local target = TargetGroup:getRealTargets(cardUseEvent.tos)[1] + if not (self:getPlayerById(target).dead or table.contains((cardUseEvent.nullifiedTargets or Util.DummyTable), target)) then + local existingEquipId + if cardUseEvent.toPutSlot and cardUseEvent.toPutSlot:startsWith("#EquipmentChoice") then + local index = cardUseEvent.toPutSlot:split(":")[2] + existingEquipId = self:getPlayerById(target):getEquipments(cardUseEvent.card.sub_type)[tonumber(index)] + elseif not self:getPlayerById(target):hasEmptyEquipSlot(cardUseEvent.card.sub_type) then + existingEquipId = self:getPlayerById(target):getEquipment(cardUseEvent.card.sub_type) + end + + if existingEquipId then + self:moveCards( + { + ids = { existingEquipId }, + from = target, + toArea = Card.DiscardPile, + moveReason = fk.ReasonPutIntoDiscardPile, + }, + { + ids = realCardIds, + to = target, + toArea = Card.PlayerEquip, + moveReason = fk.ReasonUse, + } + ) + else + self:moveCards({ + ids = realCardIds, + to = target, + toArea = Card.PlayerEquip, + moveReason = fk.ReasonUse, + }) + end + end + + return + elseif cardUseEvent.card.sub_type == Card.SubtypeDelayedTrick then + if #realCardIds == 0 then + return + end + + local target = TargetGroup:getRealTargets(cardUseEvent.tos)[1] + if not (self:getPlayerById(target).dead or table.contains((cardUseEvent.nullifiedTargets or Util.DummyTable), target)) then + local findSameCard = false + for _, cardId in ipairs(self:getPlayerById(target):getCardIds(Player.Judge)) do + if Fk:getCardById(cardId).trueName == cardUseEvent.card.trueName then + findSameCard = true + end + end + + if not findSameCard then + if cardUseEvent.card:isVirtual() then + self:getPlayerById(target):addVirtualEquip(cardUseEvent.card) + elseif cardUseEvent.card.name ~= Fk:getCardById(cardUseEvent.card.id, true).name then + local card = Fk:cloneCard(cardUseEvent.card.name) + card.skillNames = cardUseEvent.card.skillNames + card:addSubcard(cardUseEvent.card.id) + self:getPlayerById(target):addVirtualEquip(card) + else + self:getPlayerById(target):removeVirtualEquip(cardUseEvent.card.id) + end + + self:moveCards({ + ids = realCardIds, + to = target, + toArea = Card.PlayerJudge, + moveReason = fk.ReasonUse, + }) + + return + end + end + + return + end + + if not cardUseEvent.card.skill then + return + end + + -- If using card to other card (like jink or nullification), simply effect and return + if cardUseEvent.toCard ~= nil then + ---@class CardEffectEvent + local cardEffectEvent = { + from = cardUseEvent.from, + tos = cardUseEvent.tos, + card = cardUseEvent.card, + toCard = cardUseEvent.toCard, + responseToEvent = cardUseEvent.responseToEvent, + nullifiedTargets = cardUseEvent.nullifiedTargets, + disresponsiveList = cardUseEvent.disresponsiveList, + unoffsetableList = cardUseEvent.unoffsetableList, + additionalDamage = cardUseEvent.additionalDamage, + additionalRecover = cardUseEvent.additionalRecover, + cardsResponded = cardUseEvent.cardsResponded, + prohibitedCardNames = cardUseEvent.prohibitedCardNames, + extra_data = cardUseEvent.extra_data, + } + self:doCardEffect(cardEffectEvent) + + if cardEffectEvent.cardsResponded then + cardUseEvent.cardsResponded = cardUseEvent.cardsResponded or {} + for _, card in ipairs(cardEffectEvent.cardsResponded) do + table.insertIfNeed(cardUseEvent.cardsResponded, card) + end + end + return + end + + local i = 0 + while i < (cardUseEvent.additionalEffect or 0) + 1 do + if #TargetGroup:getRealTargets(cardUseEvent.tos) > 0 and cardUseEvent.card.skill.onAction then + cardUseEvent.card.skill:onAction(self, cardUseEvent) + end + + -- Else: do effect to all targets + local collaboratorsIndex = {} + for _, toId in ipairs(TargetGroup:getRealTargets(cardUseEvent.tos)) do + if not table.contains(cardUseEvent.nullifiedTargets, toId) and self:getPlayerById(toId):isAlive() then + ---@class CardEffectEvent + local cardEffectEvent = { + from = cardUseEvent.from, + tos = cardUseEvent.tos, + card = cardUseEvent.card, + toCard = cardUseEvent.toCard, + responseToEvent = cardUseEvent.responseToEvent, + nullifiedTargets = cardUseEvent.nullifiedTargets, + disresponsiveList = cardUseEvent.disresponsiveList, + unoffsetableList = cardUseEvent.unoffsetableList, + additionalDamage = cardUseEvent.additionalDamage, + additionalRecover = cardUseEvent.additionalRecover, + cardsResponded = cardUseEvent.cardsResponded, + prohibitedCardNames = cardUseEvent.prohibitedCardNames, + extra_data = cardUseEvent.extra_data, + } + + if aimEventCollaborators[toId] then + cardEffectEvent.to = toId + collaboratorsIndex[toId] = collaboratorsIndex[toId] or 1 + local curAimEvent = aimEventCollaborators[toId][collaboratorsIndex[toId]] + + cardEffectEvent.subTargets = curAimEvent.subTargets + cardEffectEvent.additionalDamage = curAimEvent.additionalDamage + cardEffectEvent.additionalRecover = curAimEvent.additionalRecover + + if curAimEvent.disresponsiveList then + cardEffectEvent.disresponsiveList = cardEffectEvent.disresponsiveList or {} + + for _, disresponsivePlayer in ipairs(curAimEvent.disresponsiveList) do + if not table.contains(cardEffectEvent.disresponsiveList, disresponsivePlayer) then + table.insert(cardEffectEvent.disresponsiveList, disresponsivePlayer) + end + end + end + + if curAimEvent.unoffsetableList then + cardEffectEvent.unoffsetableList = cardEffectEvent.unoffsetableList or {} + + for _, unoffsetablePlayer in ipairs(curAimEvent.unoffsetableList) do + if not table.contains(cardEffectEvent.unoffsetableList, unoffsetablePlayer) then + table.insert(cardEffectEvent.unoffsetableList, unoffsetablePlayer) + end + end + end + + cardEffectEvent.disresponsive = curAimEvent.disresponsive + cardEffectEvent.unoffsetable = curAimEvent.unoffsetable + cardEffectEvent.fixedResponseTimes = curAimEvent.fixedResponseTimes + cardEffectEvent.fixedAddTimesResponsors = curAimEvent.fixedAddTimesResponsors + + collaboratorsIndex[toId] = collaboratorsIndex[toId] + 1 + + local curCardEffectEvent = table.simpleClone(cardEffectEvent) + self:doCardEffect(curCardEffectEvent) + + if curCardEffectEvent.cardsResponded then + cardUseEvent.cardsResponded = cardUseEvent.cardsResponded or {} + for _, card in ipairs(curCardEffectEvent.cardsResponded) do + table.insertIfNeed(cardUseEvent.cardsResponded, card) + end + end + + if type(curCardEffectEvent.nullifiedTargets) == 'table' then + table.insertTableIfNeed(cardUseEvent.nullifiedTargets, curCardEffectEvent.nullifiedTargets) + end + end + end + end + + if #TargetGroup:getRealTargets(cardUseEvent.tos) > 0 and cardUseEvent.card.skill.onAction then + cardUseEvent.card.skill:onAction(self, cardUseEvent, true) + end + + i = i + 1 + end +end + +--- 对卡牌效果数据进行生效 +---@param cardEffectEvent CardEffectEvent +function UseCardEventWrappers:doCardEffect(cardEffectEvent) + return exec(CardEffect, cardEffectEvent) +end + +---@param cardEffectEvent CardEffectEvent +function UseCardEventWrappers:handleCardEffect(event, cardEffectEvent) + if event == fk.PreCardEffect then + if cardEffectEvent.card.skill:aboutToEffect(self, cardEffectEvent) then return end + if + cardEffectEvent.card.trueName == "slash" and + not (cardEffectEvent.unoffsetable or table.contains(cardEffectEvent.unoffsetableList or Util.DummyTable, cardEffectEvent.to)) + then + local loopTimes = 1 + if cardEffectEvent.fixedResponseTimes then + if type(cardEffectEvent.fixedResponseTimes) == "table" then + loopTimes = cardEffectEvent.fixedResponseTimes["jink"] or 1 + elseif type(cardEffectEvent.fixedResponseTimes) == "number" then + loopTimes = cardEffectEvent.fixedResponseTimes + end + end + Fk.currentResponsePattern = "jink" + + for i = 1, loopTimes do + local to = self:getPlayerById(cardEffectEvent.to) + local prompt = "" + if cardEffectEvent.from then + if loopTimes == 1 then + prompt = "#slash-jink:" .. cardEffectEvent.from + else + prompt = "#slash-jink-multi:" .. cardEffectEvent.from .. "::" .. i .. ":" .. loopTimes + end + end + + local use = self:askForUseCard( + to, + "jink", + nil, + prompt, + true, + nil, + cardEffectEvent + ) + if use then + use.toCard = cardEffectEvent.card + use.responseToEvent = cardEffectEvent + self:useCard(use) + end + + if not cardEffectEvent.isCancellOut then + break + end + + cardEffectEvent.isCancellOut = i == loopTimes + end + elseif + cardEffectEvent.card.type == Card.TypeTrick and + not (cardEffectEvent.disresponsive or cardEffectEvent.unoffsetable) and + not table.contains(cardEffectEvent.prohibitedCardNames or Util.DummyTable, "nullification") + then + local players = {} + Fk.currentResponsePattern = "nullification" + local cardCloned = Fk:cloneCard("nullification") + for _, p in ipairs(self.alive_players) do + if not p:prohibitUse(cardCloned) then + local cards = p:getHandlyIds() + for _, cid in ipairs(cards) do + if + Fk:getCardById(cid).trueName == "nullification" and + not ( + table.contains(cardEffectEvent.disresponsiveList or Util.DummyTable, p.id) or + table.contains(cardEffectEvent.unoffsetableList or Util.DummyTable, p.id) + ) + then + table.insert(players, p) + break + end + end + if not table.contains(players, p) then + Self = p -- for enabledAtResponse + for _, s in ipairs(table.connect(p.player_skills, p._fake_skills)) do + if + s.pattern and + Exppattern:Parse("nullification"):matchExp(s.pattern) and + not (s.enabledAtResponse and not s:enabledAtResponse(p)) and + not ( + table.contains(cardEffectEvent.disresponsiveList or Util.DummyTable, p.id) or + table.contains(cardEffectEvent.unoffsetableList or Util.DummyTable, p.id) + ) + then + table.insert(players, p) + break + end + end + end + end + end + + local prompt = "" + if cardEffectEvent.to then + prompt = "#AskForNullification::" .. cardEffectEvent.to .. ":" .. cardEffectEvent.card.name + elseif cardEffectEvent.from then + prompt = "#AskForNullificationWithoutTo:" .. cardEffectEvent.from .. "::" .. cardEffectEvent.card.name + end + + local extra_data + if #TargetGroup:getRealTargets(cardEffectEvent.tos) > 1 then + local parentUseEvent = self.logic:getCurrentEvent():findParent(GameEvent.UseCard) + if parentUseEvent then + extra_data = { useEventId = parentUseEvent.id, effectTo = cardEffectEvent.to } + end + end + local use = self:askForNullification(players, nil, nil, prompt, true, extra_data, cardEffectEvent) + if use then + use.toCard = cardEffectEvent.card + use.responseToEvent = cardEffectEvent + self:useCard(use) + end + end + Fk.currentResponsePattern = nil + elseif event == fk.CardEffecting then + if cardEffectEvent.card.skill then + exec(GameEvent.SkillEffect, function () + cardEffectEvent.card.skill:onEffect(self, cardEffectEvent) + end, self:getPlayerById(cardEffectEvent.from), cardEffectEvent.card.skill) + end + end +end + +--- 对“打出牌”进行处理 +---@param cardResponseEvent CardResponseEvent +function UseCardEventWrappers:responseCard(cardResponseEvent) + return exec(RespondCard, cardResponseEvent) +end + +---@param card_name string @ 想要视为使用的牌名 +---@param subcards? integer[] @ 子卡,可以留空或者直接nil +---@param from ServerPlayer @ 使用来源 +---@param tos ServerPlayer | ServerPlayer[] @ 目标角色(列表) +---@param skillName? string @ 技能名 +---@param extra? boolean @ 是否不计入次数 +---@return CardUseStruct +function UseCardEventWrappers:useVirtualCard(card_name, subcards, from, tos, skillName, extra) + local card = Fk:cloneCard(card_name) + card.skillName = skillName + + if from:prohibitUse(card) then return false end + + if tos.class then tos = { tos } end + for i, p in ipairs(tos) do + if from:isProhibited(p, card) then + table.remove(tos, i) + end + end + + if #tos == 0 then return false end + + if subcards then card:addSubcards(Card:getIdList(subcards)) end + + local use = {} ---@type CardUseStruct + use.from = from.id + use.tos = table.map(tos, function(p) return { p.id } end) + use.card = card + use.extraUse = extra + self:useCard(use) + + return use +end + +return { UseCard, RespondCard, CardEffect, UseCardEventWrappers } diff --git a/lua/server/gamelogic.lua b/lua/server/gamelogic.lua index 9388b89a..b074c441 100644 --- a/lua/server/gamelogic.lua +++ b/lua/server/gamelogic.lua @@ -96,11 +96,9 @@ function GameLogic:assignRoles() local p = room.players[i] p.role = roles[i] if p.role == "lord" then - p.role_shown = true - room:broadcastProperty(p, "role") - else - room:notifyProperty(p, p, "role") + room:setPlayerProperty(p, "role_shown", true) end + room:broadcastProperty(p, "role") end end @@ -212,22 +210,9 @@ end function GameLogic:prepareDrawPile() local room = self.room - local allCardIds = Fk:getAllCardIds() - - for i = #allCardIds, 1, -1 do - if Fk:getCardById(allCardIds[i]).is_derived then - local id = allCardIds[i] - table.removeOne(allCardIds, id) - table.insert(room.void, id) - room:setCardArea(id, Card.Void, nil) - end - end - - table.shuffle(allCardIds) - room.draw_pile = allCardIds - for _, id in ipairs(room.draw_pile) do - self.room:setCardArea(id, Card.DrawPile, nil) - end + local seed = math.random(2 << 32 - 1) + room:prepareDrawPile(seed) + room:doBroadcastNotify("PrepareDrawPile", seed) end function GameLogic:attachSkillToPlayers() @@ -774,7 +759,7 @@ function GameLogic:damageByCardEffect(is_exact) local c_event = d_event:findParent(GameEvent.CardEffect, false, 2) if c_event == nil then return false end return damage.card == c_event.data[1].card and - (not is_exact or d_event.data[1].from.id == c_event.data[1].from) + (not is_exact or (damage.from or {}).id == c_event.data[1].from) end function GameLogic:dumpEventStack(detailed) diff --git a/lua/server/mark_enum.lua b/lua/server/mark_enum.lua index 0c8f4ad8..dd6eba1d 100644 --- a/lua/server/mark_enum.lua +++ b/lua/server/mark_enum.lua @@ -29,10 +29,14 @@ MarkEnum.BypassTimesLimitTo = "BypassTimesLimitTo" MarkEnum.BypassDistancesLimitTo = "BypassDistancesLimitTo" ---非锁定技失效 MarkEnum.UncompulsoryInvalidity = "UncompulsoryInvalidity" +---失效技能表 +MarkEnum.InvalidSkills = "InvalidSkills" ---不可明置(值为表,m - 主将, d - 副将) MarkEnum.RevealProhibited = "RevealProhibited" ---不计入距离、座次后缀 MarkEnum.PlayerRemoved = "PlayerRemoved" +---不能调整手牌 +MarkEnum.SortProhibited = "SortProhibited" ---各种清除标记后缀 --- diff --git a/lua/server/network.lua b/lua/server/network.lua new file mode 100644 index 00000000..19385977 --- /dev/null +++ b/lua/server/network.lua @@ -0,0 +1,316 @@ +---@class Request : Object +---@field public room Room +---@field public players ServerPlayer[] +---@field public n integer @ n个人做出回复后,询问结束 +---@field public accept_cancel? boolean @ 是否将取消也算作是收到回复 +---@field public ai_start_time integer? @ 只剩AI思考开始的时间(微秒),delay专用 +---@field public timeout? integer @ 本次耗时(秒),默认为房间内配置的出手时间 +---@field public command string @ command自然就是command +---@field public data table @ 每个player对应的询问数据 +---@field public default_reply table @ 玩家id - 默认回复内容 +---@field public send_json boolean? @ 是否需要对data使用json.encode,默认true +---@field public receive_json boolean? @ 是否需要对reply使用json.decode,默认true +---@field private send_success table @ 数据是否发送成功,不成功的后面全部视为AI +---@field public result table @ 玩家id - 回复内容 nil表示完全未回复 +---@field public luck_data any? @ 是否是询问手气卡 TODO: 有需求的话把这个通用化一点 +---@field private pending_requests table @ 一控多时暂存的请求 +local Request = class("Request") + +-- TODO: 懒得思考了 +-- 手气卡用:目前暂时写死一个属性而不是给函数参数; +-- player有个属性保存自己剩余手气卡次数 +-- request有个luck_data属性来处理OK消息 +-- 若还能再用一次,那就重新发Request并继续等 + +---@param command string +---@param players ServerPlayer[] +---@param n? integer +function Request:initialize(command, players, n) + assert(#players > 0) + self.command = command + self.players = players + self.n = n or #players + + -- 剩下的需要自己构造后修改相关值,构造函数只给四个 + local room = players[1].room + self.room = room + self.data = {} + self.default_reply = {} + for _, p in ipairs(players) do self.default_reply[p.id] = "__cancel" end + self.timestamp = math.ceil(os.getms() / 1000) + self.timeout = room.timeout + self.send_json = true + self.receive_json = true -- 除了几个特殊字符串之外都decode + + self.pending_requests = setmetatable({}, { __mode = "k" }) + self.send_success = setmetatable({}, { __mode = "k" }) + self.result = {} +end + +function Request:__tostring() + return "" +end + +function Request:setData(player, data) + self.data[player.id] = data +end + +function Request:setDefaultReply(player, data) + self.default_reply[player.id] = data +end + +-- 将相应请求数据发给player +-- 不能向thinking中的玩家发送,这种情况下暂存起来等待收到答复后 +---@param player ServerPlayer +function Request:_sendPacket(player) + local controller = player.serverplayer + + -- 若正在烧条,则不发,将这个需要请求的玩家id存起来后面用 + if controller:thinking() then + self.pending_requests[controller] = self.pending_requests[controller] or {} + table.insert(self.pending_requests[controller], player.id) + return + end + + -- 若控制者目前的视角不是player,先发个数据指示切换视角 + if not table.contains(player._observers, controller) then + local from = table.find(self.room.players, function(p) + return table.contains(p._observers, controller) + end) + + -- 切换视角 + table.removeOne(from._observers, controller) + table.insert(player._observers, controller) + controller:doNotify("ChangeSelf", tostring(player.id)) + end + + -- 发送请求数据并将控制者标记为烧条中 + local jsonData = self.data[player.id] + if self.send_json then jsonData = json.encode(jsonData) end + -- FIXME: 这里确认数据是否发送的环节一定要写在C++代码中 + self.send_success[controller] = controller:getState() == fk.Player_Online + controller:doRequest(self.command, jsonData, self.timeout, self.timestamp) + controller:setThinking(true) +end + +-- 检查一下该玩家是否已经有答复了,若为AI则直接计算出回复 +-- 一般来说,在一次同时询问中,需要人类玩家全部回复完了,AI才进行回复 +---@param player ServerPlayer +---@param use_ai boolean +function Request:_checkReply(player, use_ai) + local room = self.room + + -- 若被人类玩家控制着,靠人类玩家自己分析了 + -- 否则交给该Player自己的AI进行考虑,换言之AI控人没有效果(不会故意杀队友) + local controller = player.serverplayer + local state = controller:getState() + local reply + + if state == fk.Player_Online and self.send_success[controller] then + if not table.contains(player._observers, controller) then + -- 若控制者的视角不在自己,那就不管了 + reply = "__notready" + else + reply = controller:waitForReply(0) + if reply ~= "__notready" then + controller:setThinking(false) + -- FIXME: 写的依托且不考虑控制 后面看情况改! + if self.luck_data then + local luck_data = self.luck_data + if reply ~= "__cancel" then + local pdata = luck_data[player.id] + pdata.luckTime = pdata.luckTime - 1 + luck_data.discardInit(room, player) + luck_data.drawInit(room, player, pdata.num) + if pdata.luckTime > 0 then + self:setData(player, { "AskForLuckCard", "#AskForLuckCard:::" .. pdata.luckTime }) + self:_sendPacket(player) + reply = "__notready" + end + end + else + local pending_list = self.pending_requests[controller] + if pending_list and #pending_list > 0 then + local pid = table.remove(pending_list, 1) + self:_sendPacket(room:getPlayerById(pid)) + end + end + end + end + else + room:checkNoHuman() + if use_ai then + player.ai.command = self.command + -- FIXME: 后面进行SmartAI的时候准备爆破此处 + -- player.ai.data = self.data[player.id] + player.ai.jsonData = self.data[player.id] + if player.ai:isInstanceOf(RandomAI) then + reply = "__cancel" + else + reply = player.ai:makeReply() + end + else + -- 还没轮到AI呢,所以需要标记为未答复 + reply = "__notready" + end + end + + return reply +end + +function Request:getWinners() + local ret = {} + for _, p in ipairs(self.players) do + local result = self.result[p.id] + if result and result ~= "" then + table.insert(ret, p) + end + end + return ret +end + +function Request:ask() + local room = self.room + -- 0. 设置计时器,防止因无人回复一直等下去 + room.room:setRequestTimer(self.timeout * 1000 + 500) + + local players = table.simpleClone(self.players) + local currentTime = os.time() + + -- 1. 向所有人发送询问请求 + for _, p in ipairs(players) do + self:_sendPacket(p) + end + + -- 2. 进入循环等待,结束条件为已有n个回复或者超时或者有人点了 + -- 若很多人都取消了导致最多回复数达不到n了,那么也结束 + local replied_players = 0 + local ready_players = 0 + while true do + local changed = false + -- 判断1:若投降则直接结束全部询问,若超时则踢掉所有人类玩家(这样AI还可计算) + if room.hasSurrendered then break end + local elapsed = os.time() - currentTime + if self.timeout - elapsed <= 0 then + for i = #players, 1, -1 do + if self.send_success[players[i].serverplayer] then + table.remove(players, i) + end + end + end + + if table.every(players, function(p) + return p.serverplayer:getState() ~= fk.Player_Online or not + self.send_success[p.serverplayer] + end) then + self.ai_start_time = os.getms() + end + + -- 判断2:收到足够多回复了 + local use_ai = self.ai_start_time ~= nil + + for i = #players, 1, -1 do + local player = players[i] + local reply = self:_checkReply(player, use_ai) + + if reply ~= "__notready" then + if reply ~= "__cancel" and self.receive_json then + reply = json.decode(reply) + end + self.result[player.id] = reply + table.remove(players, i) + replied_players = replied_players + 1 + changed = true + + if reply ~= "__cancel" or self.accept_cancel then + ready_players = ready_players + 1 + if ready_players >= self.n then + for _, p in ipairs(self.players) do + -- 避免触发后续的烧条检测 + if self.result[p.id] == nil then + self.result[p.id] = "__failed_in_race" + end + end + break -- 注意外面还有一层循环 + end + end + end + end + + if #players + ready_players < self.n then break end + if ready_players >= self.n then break end + + -- 防止万一,如果AI算完后还是有机器人notready的话也别等了 + -- 不然就永远别想被唤醒了 + if self.ai_start_time then break end + + -- 需要等待呢,等待被唤醒吧 + if not changed then + coroutine.yield("__handleRequest") + end + end + + room.room:destroyRequestTimer() + self:finish() +end + +local function surrenderCheck(room) + if not room.hasSurrendered then return end + local player = table.find(room.players, function(p) + return p.surrendered + end) + if not player then + room.hasSurrendered = false + return + end + room:broadcastProperty(player, "surrendered") + local mode = Fk.game_modes[room.settings.gameMode] + local winner = Pcall(mode.getWinner, mode, player) + if winner ~= "" then + room:gameOver(winner) + end + + -- 以防万一 + player.surrendered = false + room:broadcastProperty(player, "surrendered") + room.hasSurrendered = false +end + +-- 善后工作,主要是result规范化、投降检测等 +function Request:finish() + local room = self.room + surrenderCheck(room) + -- FIXME: 这里QML中有个bug,这个命令应该是用来暗掉玩家面板的 + -- room:doBroadcastNotify("CancelRequest", "") + for _, p in ipairs(self.players) do + p.serverplayer:setThinking(false) + if self.result[p.id] == nil then + self.result[p.id] = self.default_reply[p.id] + p._timewaste_count = p._timewaste_count + 1 + if p._timewaste_count >= 3 and p.serverplayer:getState() == fk.Player_Online then + p._timewaste_count = 0 + p.serverplayer:emitKick() + end + else + p._timewaste_count = 0 + end + if self.result[p.id] == "__cancel" then + self.result[p.id] = "" + end + if self.result[p.id] == "__failed_in_race" then + self.result[p.id] = nil + end + end + room.last_request = self + + for _, isHuman in pairs(self.send_success) do + if not self.ai_start_time then break end + if not isHuman then + local to_delay = 500 - (os.getms() - self.ai_start_time) / 1000 + room:delay(to_delay) + break + end + end +end + +return Request diff --git a/lua/server/request.lua b/lua/server/request.lua index 2fb07a25..472a62e5 100644 --- a/lua/server/request.lua +++ b/lua/server/request.lua @@ -1,9 +1,12 @@ -- SPDX-License-Identifier: GPL-3.0-or-later +-- 本文件是用来处理各种异步请求的 +-- 与游戏中常见的请求-答复没有什么联系 + local function tellRoomToObserver(self, player) local observee = self.players[1] local start_time = os.getms() - local summary = self:getSummary(observee, true) + local summary = self:toJsonObject(observee) player:doNotify("Observe", json.encode(summary)) fk.qInfo(string.format("[Observe] %d, %s, in %.3fms", @@ -61,54 +64,6 @@ request_handlers["prelight"] = function(room, id, reqlist) end end -request_handlers["luckcard"] = function(room, id, reqlist) - local p = room:getPlayerById(id) - local cancel = reqlist[3] == "false" - local luck_data = room:getTag("LuckCardData") - if not (p and luck_data) then return end - local pdata = luck_data[id] - - if not cancel then - pdata.luckTime = pdata.luckTime - 1 - luck_data.discardInit(room, p) - luck_data.drawInit(room, p, pdata.num) - else - pdata.luckTime = 0 - end - - if pdata.luckTime > 0 then - p:doNotify("AskForLuckCard", pdata.luckTime) - else - p.serverplayer:setThinking(false) - ResumeRoom(room.id) - end - - room:setTag("LuckCardData", luck_data) -end - -request_handlers["changeself"] = function(room, id, reqlist) - local p = room:getPlayerById(id) - local toId = tonumber(reqlist[3]) - local from = p - local to = room:getPlayerById(toId) - local from_sp = from._splayer - - -- 注意发来信息的玩家的主视角可能已经不是自己了 - -- 先换成正确的玩家 - from = table.find(room.players, function(p) - return table.contains(p._observers, from_sp) - end) - - -- 切换视角 - table.removeOne(from._observers, from_sp) - table.insert(to._observers, from_sp) - from_sp:doNotify("ChangeSelf", json.encode { - id = toId, - handcards = to:getCardIds(Player.Hand), - special_cards = to.special_cards, - }) -end - request_handlers["surrender"] = function(room, id, reqlist) local player = room:getPlayerById(id) if not player then return end diff --git a/lua/server/room.lua b/lua/server/room.lua index df6a9f09..bc64f043 100644 --- a/lua/server/room.lua +++ b/lua/server/room.lua @@ -3,7 +3,7 @@ --- Room是fk游戏逻辑运行的主要场所,同时也提供了许多API函数供编写技能使用。 --- --- 一个房间中只有一个Room实例,保存在RoomInstance全局变量中。 ----@class Room : AbstractRoom +---@class Room : AbstractRoom, GameEventWrappers ---@field public room fk.Room @ C++层面的Room类实例,别管他就是了,用不着 ---@field public id integer @ 房间的id ---@field private main_co any @ 本房间的主协程 @@ -13,26 +13,21 @@ ---@field public current ServerPlayer @ 当前回合玩家 ---@field public game_started boolean @ 游戏是否已经开始 ---@field public game_finished boolean @ 游戏是否已经结束 ----@field public timeout integer @ 出牌时长上限 ---@field public tag table @ Tag清单,其实跟Player的标记是差不多的东西 ---@field public general_pile string[] @ 武将牌堆,这是可用武将名的数组 ----@field public draw_pile integer[] @ 摸牌堆,这是卡牌id的数组 ----@field public discard_pile integer[] @ 弃牌堆,也是卡牌id的数组 ----@field public processing_area integer[] @ 处理区,依然是卡牌id数组 ----@field public void integer[] @ 从游戏中除外区,一样的是卡牌id数组 ----@field public card_place table @ 每个卡牌的id对应的区域,一张表 ----@field public owner_map table @ 每个卡牌id对应的主人,表的值是那个玩家的id,可能是nil ----@field public settings table @ 房间的额外设置,差不多是json对象 ---@field public logic GameLogic @ 这个房间使用的游戏逻辑,可能根据游戏模式而变动 ---@field public request_queue table ---@field public request_self table +---@field public last_request Request @ 上一次完成的request ---@field public skill_costs table @ 存放skill.cost_data用 ---@field public card_marks table @ 存放card.mark之用 local Room = AbstractRoom:subclass("Room") -- load classes used by the game +Request = require "server.network" GameEvent = require "server.gameevent" -dofile "lua/server/events/init.lua" +local GameEventWrappers = require "lua/server/events" +Room:include(GameEventWrappers) GameLogic = require "server.gamelogic" ServerPlayer = require "server.serverplayer" @@ -75,12 +70,6 @@ function Room:initialize(_room) self.timeout = _room:getTimeout() self.tag = {} self.general_pile = {} - self.draw_pile = {} - self.discard_pile = {} - self.processing_area = {} - self.void = {} - self.card_place = {} - self.owner_map = {} self.request_queue = {} self.request_self = {} @@ -267,38 +256,6 @@ end -- getters/setters ------------------------------------------------------------------------ ---- 基本算是私有函数,别去用 ----@param cardId integer ----@param cardArea CardArea ----@param owner? integer -function Room:setCardArea(cardId, cardArea, owner) - self.card_place[cardId] = cardArea - self.owner_map[cardId] = owner -end - ---- 获取一张牌所处的区域。 ----@param cardId integer | Card @ 要获得区域的那张牌,可以是Card或者一个id ----@return CardArea @ 这张牌的区域 -function Room:getCardArea(cardId) - local cardIds = {} - for _, cid in ipairs(Card:getIdList(cardId)) do - local place = self.card_place[cid] or Card.Unknown - table.insertIfNeed(cardIds, place) - end - return #cardIds == 1 and cardIds[1] or Card.Unknown -end - ---- 获得拥有某一张牌的玩家。 ----@param cardId integer | Card @ 要获得主人的那张牌,可以是Card实例或者id ----@return ServerPlayer? @ 这张牌的主人,可能返回nil -function Room:getCardOwner(cardId) - if type(cardId) ~= "number" then - assert(cardId and cardId:isInstanceOf(Card)) - cardId = cardId:getEffectiveId() - end - return self.owner_map[cardId] and self:getPlayerById(self.owner_map[cardId]) or nil -end - --- 根据玩家id,获得那名玩家本人。 ---@param id integer @ 玩家的id ---@return ServerPlayer @ 这个id对应的ServerPlayer实例 @@ -579,13 +536,6 @@ function Room:setBanner(name, value) self:doBroadcastNotify("SetBanner", json.encode{ name, value }) end ----@return boolean -local function execGameEvent(tp, ...) - local event = tp:create(...) - local _, ret = event:exec() - return ret -end - ---@param player ServerPlayer ---@param general string ---@param changeKingdom? boolean @@ -650,65 +600,6 @@ function Room:prepareGeneral(player, general, deputy, broadcast) end end ----@param player ServerPlayer @ 要换将的玩家 ----@param new_general string @ 要变更的武将,若不存在则变身为孙策,孙策不存在变身为士兵 ----@param full? boolean @ 是否血量满状态变身 ----@param isDeputy? boolean @ 是否变的是副将 ----@param sendLog? boolean @ 是否发Log ----@param maxHpChange? boolean @ 是否改变体力上限,默认改变 -function Room:changeHero(player, new_general, full, isDeputy, sendLog, maxHpChange, kingdomChange) - local new = Fk.generals[new_general] or Fk.generals["sunce"] or Fk.generals["blank_shibing"] - - kingdomChange = (kingdomChange == nil) and true or kingdomChange - local kingdom = (isDeputy or not kingdomChange) and player.kingdom or new.kingdom - if not isDeputy and kingdomChange then - local allKingdoms = {} - if new.subkingdom then - allKingdoms = { new.kingdom, new.subkingdom } - else - allKingdoms = Fk:getKingdomMap(new.kingdom) - end - if #allKingdoms > 0 then - kingdom = self:askForChoice(player, allKingdoms, "AskForKingdom", "#ChooseInitialKingdom") - end - end - - execGameEvent(GameEvent.ChangeProperty, - { - from = player, - general = not isDeputy and new_general or nil, - deputyGeneral = isDeputy and new_general or nil, - gender = isDeputy and player.gender or new.gender, - kingdom = kingdom, - sendLog = sendLog, - results = {}, - }) - - maxHpChange = (maxHpChange == nil) and true or maxHpChange - if maxHpChange then - self:setPlayerProperty(player, "maxHp", player:getGeneralMaxHp()) - end - if full or player.hp > player.maxHp then - self:setPlayerProperty(player, "hp", player.maxHp) - end -end - ----@param player ServerPlayer @ 要变更势力的玩家 ----@param kingdom string @ 要变更的势力 ----@param sendLog? boolean @ 是否发Log -function Room:changeKingdom(player, kingdom, sendLog) - if kingdom == player.kingdom then return end - sendLog = sendLog or false - - execGameEvent(GameEvent.ChangeProperty, - { - from = player, - kingdom = kingdom, - sendLog = sendLog, - results = {}, - }) -end - --- 房间信息摘要,返回房间的大致信息 --- 用于旁观和重连,但也可用于debug function Room:getSummary(player, observe) @@ -748,6 +639,15 @@ function Room:getSummary(player, observe) } end +function Room:toJsonObject(player) + local o = AbstractRoom.toJsonObject(self) + o.round_count = self:getTag("RoundCount") or 0 + if player then + o.you = player.id + end + return o +end + ------------------------------------------------------------------------ -- 网络通信有关 ------------------------------------------------------------------------ @@ -784,33 +684,6 @@ function Room:doBroadcastNotify(command, jsonData, players) end end ----@param room Room -local function surrenderCheck(room) - if not room.hasSurrendered then return end - local player = table.find(room.players, function(p) - return p.surrendered - end) - if not player then - room.hasSurrendered = false - return - end - room:broadcastProperty(player, "surrendered") - local mode = Fk.game_modes[room.settings.gameMode] - local winner = Pcall(mode.getWinner, mode, player) - if winner ~= "" then - room:gameOver(winner) - end - - -- 以防万一 - player.surrendered = false - room:broadcastProperty(player, "surrendered") - room.hasSurrendered = false -end - -local function setRequestTimer(room) - room.room:setRequestTimer(room.timeout * 1000 + 500) -end - --- 向某个玩家发起一次Request。 ---@param player ServerPlayer @ 发出这个请求的目标玩家 ---@param command string @ 请求的类型 @@ -818,20 +691,15 @@ end ---@param wait? boolean @ 是否要等待答复,默认为true ---@return string @ 收到的答复,如果wait为false的话就返回nil function Room:doRequest(player, command, jsonData, wait) - if wait == nil then wait = true end - self.request_queue = {} - self.race_request_list = nil - player:doRequest(command, jsonData, self.timeout) - - if wait then - setRequestTimer(self) - local ret = player:waitForReply(self.timeout) - player.serverplayer:setBusy(false) - player.serverplayer:setThinking(false) - self.room:destroyRequestTimer() - surrenderCheck(self) - return ret - end + -- fk.qCritical("Room:doRequest is deprecated!") + if wait == true then error("wait can't be true") end + local request = Request:new(command, {player}) + request.send_json = false -- 因为参数已经json.encode过了,该死的兼容性 + request.receive_json = false + request.accept_cancel = true + request:setData(player, jsonData) + request:ask() + return request.result[player.id] end --- 向多名玩家发出请求。 @@ -839,29 +707,16 @@ end ---@param players? ServerPlayer[] @ 发出请求的玩家列表 ---@param jsonData? string @ 请求数据 function Room:doBroadcastRequest(command, players, jsonData) + -- fk.qCritical("Room:doBroadcastRequest is deprecated!") players = players or self.players - self.request_queue = {} - self.race_request_list = nil - setRequestTimer(self) + local request = Request:new(command, players) + request.send_json = false -- 因为参数已经json.encode过了 + request.receive_json = false + request.accept_cancel = true for _, p in ipairs(players) do - p:doRequest(command, jsonData or p.request_data) + request:setData(p, jsonData or p.request_data) end - - local remainTime = self.timeout - local currentTime = os.time() - local elapsed = 0 - for _, p in ipairs(players) do - elapsed = os.time() - currentTime - p:waitForReply(remainTime - elapsed) - end - - for _, p in ipairs(players) do - p.serverplayer:setBusy(false) - p.serverplayer:setThinking(false) - end - - self.room:destroyRequestTimer() - surrenderCheck(self) + request:ask() end --- 向多名玩家发出竞争请求。 @@ -874,69 +729,18 @@ end ---@param jsonData string @ 请求数据 ---@return ServerPlayer? @ 在这次竞争请求中获胜的角色,可能是nil function Room:doRaceRequest(command, players, jsonData) + -- fk.qCritical("Room:doRaceRequest is deprecated!") players = players or self.players - players = table.simpleClone(players) - local player_len = #players - setRequestTimer(self) - -- self:notifyMoveFocus(players, command) - self.request_queue = {} - self.race_request_list = players + local request = Request:new(command, players, 1) + request.send_json = false -- 因为参数已经json.encode过了 + request.receive_json = false for _, p in ipairs(players) do - p:doRequest(command, jsonData or p.request_data) + request:setData(p, jsonData or p.request_data) end - - local remainTime = self.timeout - local currentTime = os.time() - local elapsed = 0 - local winner - local canceled_players = {} - local ret - while true do - elapsed = os.time() - currentTime - if remainTime - elapsed <= 0 then - break - end - for i = #players, 1, -1 do - local p = players[i] - p:waitForReply(0) - if p.reply_ready == true then - winner = p - break - end - - if p.reply_cancel then - table.remove(players, i) - table.insertIfNeed(canceled_players, p) - elseif p.id > 0 then - -- 骗过调度器让他以为自己尚未就绪 - p.request_timeout = remainTime - elapsed - p.serverplayer:setThinking(true) - end - end - if winner then - self:doBroadcastNotify("CancelRequest", "") - ret = winner - break - end - - if player_len == #canceled_players then - break - end - - coroutine.yield("__handleRequest", (remainTime - elapsed) * 1000) - end - - for _, p in ipairs(self.players) do - p.serverplayer:setBusy(false) - p.serverplayer:setThinking(false) - end - - self.room:destroyRequestTimer() - surrenderCheck(self) - return ret + request:ask() + return request:getWinners()[1] end - --- 延迟一段时间。 ---@param ms integer @ 要延迟的毫秒数 function Room:delay(ms) @@ -944,7 +748,7 @@ function Room:delay(ms) self.delay_start = start self.delay_duration = ms self.in_delay = true - self.room:delay(ms) + self.room:delay(math.ceil(ms)) coroutine.yield("__handleRequest", ms) end @@ -1101,7 +905,8 @@ end ---@param player ServerPlayer @ 发动技能的那个玩家 ---@param skill_name string @ 技能名 ---@param skill_type? string | AnimationType @ 技能的动画效果,默认是那个技能的anim_type -function Room:notifySkillInvoked(player, skill_name, skill_type) +---@param tos? table @ 技能目标,填空则不声明 +function Room:notifySkillInvoked(player, skill_name, skill_type, tos) local bigAnim = false if not skill_type then local skill = Fk.skills[skill_name] @@ -1116,11 +921,20 @@ function Room:notifySkillInvoked(player, skill_name, skill_type) if skill_type == "big" then bigAnim = true end - self:sendLog{ - type = "#InvokeSkill", - from = player.id, - arg = skill_name, - } + if tos and #tos > 0 then + self:sendLog{ + type = "#InvokeSkillTo", + from = player.id, + arg = skill_name, + to = tos, + } + else + self:sendLog{ + type = "#InvokeSkill", + from = player.id, + arg = skill_name, + } + end if not bigAnim then self:doAnimate("InvokeSkill", { @@ -1311,8 +1125,9 @@ end ---@param skillName? string @ 技能名 ---@param cancelable? boolean @ 能否点取消 ---@param no_indicate? boolean @ 是否不显示指示线 +---@param targetTipName? string @ 引用的选择目标提示的函数名 ---@return integer[] @ 选择的玩家id列表,可能为空 -function Room:askForChoosePlayers(player, targets, minNum, maxNum, prompt, skillName, cancelable, no_indicate) +function Room:askForChoosePlayers(player, targets, minNum, maxNum, prompt, skillName, cancelable, no_indicate, targetTipName) if maxNum < 1 then return {} end @@ -1324,7 +1139,8 @@ function Room:askForChoosePlayers(player, targets, minNum, maxNum, prompt, skill num = maxNum, min_num = minNum, pattern = "", - skillName = skillName + skillName = skillName, + targetTipName = targetTipName, } local _, ret = self:askForUseActiveSkill(player, "choose_players_skill", prompt or "", cancelable, data, no_indicate) if ret then @@ -1376,6 +1192,11 @@ function Room:askForCard(player, minNum, maxNum, includeEquip, skillName, cancel else if cancelable then return {} end local cards = player:getCardIds("he&") + if type(expand_pile) == "string" then + table.insertTable(cards, player:getPile(expand_pile)) + elseif type(expand_pile) == "table" then + table.insertTable(cards, expand_pile) + end local exp = Exppattern:Parse(pattern) cards = table.filter(cards, function(cid) return exp:match(Fk:getCardById(cid)) @@ -1397,8 +1218,9 @@ end ---@param prompt? string @ 提示信息 ---@param cancelable? boolean @ 能否点取消 ---@param no_indicate? boolean @ 是否不显示指示线 +---@param targetTipName? string @ 引用的选择目标提示的函数名 ---@return integer[], integer -function Room:askForChooseCardAndPlayers(player, targets, minNum, maxNum, pattern, prompt, skillName, cancelable, no_indicate) +function Room:askForChooseCardAndPlayers(player, targets, minNum, maxNum, pattern, prompt, skillName, cancelable, no_indicate, targetTipName) if maxNum < 1 then return {} end @@ -1417,7 +1239,8 @@ function Room:askForChooseCardAndPlayers(player, targets, minNum, maxNum, patter num = maxNum, min_num = minNum, pattern = pattern, - skillName = skillName + skillName = skillName, + targetTipName = targetTipName, } local _, ret = self:askForUseActiveSkill(player, "choose_players_skill", prompt or "", cancelable, data, no_indicate) if ret then @@ -1445,7 +1268,7 @@ end ---@param cancelable? boolean @ 能否点取消 ---@param no_indicate? boolean @ 是否不显示指示线 ---@return integer[], integer[] -function Room:askForChooseCardsAndPlayers(player, minCardNum, maxCardNum, targets, minTargetNum, maxTargetNum, pattern, prompt, skillName, cancelable, no_indicate) +function Room:askForChooseCardsAndPlayers(player, minCardNum, maxCardNum, targets, minTargetNum, maxTargetNum, pattern, prompt, skillName, cancelable, no_indicate, targetTipName) cancelable = (cancelable == nil) and true or cancelable no_indicate = no_indicate or false pattern = pattern or "." @@ -1464,6 +1287,7 @@ function Room:askForChooseCardsAndPlayers(player, minCardNum, maxCardNum, target min_c_num = minCardNum, pattern = pattern, skillName = skillName, + targetTipName = targetTipName, } local _, ret = self:askForUseActiveSkill(player, "ex__choose_skill", prompt or "", cancelable, data, no_indicate) if ret then @@ -2115,8 +1939,9 @@ end ---@return table<"top"|"bottom", integer[]> function Room:askForGuanxing(player, cards, top_limit, bottom_limit, customNotify, noPut, areaNames) -- 这一大堆都是来提前报错的 - top_limit = top_limit or Util.DummyTable - bottom_limit = bottom_limit or Util.DummyTable + local leng = #cards + top_limit = top_limit or { 0, leng } + bottom_limit = bottom_limit or { 0, leng } if #top_limit > 0 then assert(top_limit[1] >= 0 and top_limit[2] >= 0, "limits error: The lower limit should be greater than 0") assert(top_limit[1] <= top_limit[2], "limits error: The upper limit should be less than the lower limit") @@ -2126,40 +1951,40 @@ function Room:askForGuanxing(player, cards, top_limit, bottom_limit, customNotif assert(bottom_limit[1] <= bottom_limit[2], "limits error: The upper limit should be less than the lower limit") end if #top_limit > 0 and #bottom_limit > 0 then - assert(#cards >= top_limit[1] + bottom_limit[1] and #cards <= top_limit[2] + bottom_limit[2], "limits Error: No enough space") + assert(leng >= top_limit[1] + bottom_limit[1] and leng <= top_limit[2] + bottom_limit[2], "limits Error: No enough space") end if areaNames then assert(#areaNames == 2, "areaNames error: Should have 2 elements") + else + areaNames = { "Top", "Bottom" } end local command = "AskForGuanxing" self:notifyMoveFocus(player, customNotify or command) - local max_top = top_limit and top_limit[2] or #cards + local max_top = top_limit[2] local card_map = {} if max_top > 0 then table.insert(card_map, table.slice(cards, 1, max_top + 1)) - else - table.insert(card_map, {}) end - if max_top < #cards then + if max_top < leng then table.insert(card_map, table.slice(cards, max_top + 1)) end local data = { prompt = "", is_free = true, cards = card_map, - min_top_cards = top_limit and top_limit[1] or 0, - max_top_cards = top_limit and top_limit[2] or #cards, - min_bottom_cards = bottom_limit and bottom_limit[1] or 0, - max_bottom_cards = bottom_limit and bottom_limit[2] or #cards, - top_area_name = areaNames and areaNames[1] or "Top", - bottom_area_name = areaNames and areaNames[2] or "Bottom", + min_top_cards = top_limit[1], + max_top_cards = top_limit[2], + min_bottom_cards = bottom_limit[1], + max_bottom_cards = bottom_limit[2], + top_area_name = areaNames[1], + bottom_area_name = areaNames[2], } local result = self:doRequest(player, command, json.encode(data)) local top, bottom if result ~= "" then local d = json.decode(result) - if #top_limit > 0 and top_limit[2] == 0 then + if top_limit[2] == 0 then top = Util.DummyTable bottom = d[1] else @@ -2167,8 +1992,9 @@ function Room:askForGuanxing(player, cards, top_limit, bottom_limit, customNotif bottom = d[2] or Util.DummyTable end else - top = table.random(cards, top_limit and top_limit[2] or #cards) or Util.DummyTable - bottom = table.shuffle(table.filter(cards, function(id) return not table.contains(top, id) end)) or Util.DummyTable + local pos = math.min(top_limit[2], leng - bottom_limit[1]) + top = table.slice(cards, 1, pos + 1) + bottom = table.slice(cards, pos + 1) end if not noPut then @@ -2234,15 +2060,12 @@ function Room:handleUseCardReply(player, data) if skill.interaction then skill.interaction.data = data.interaction_data end if skill:isInstanceOf(ActiveSkill) then self:useSkill(player, skill, function() - if not skill.no_indicate then - self:doIndicate(player.id, targets) - end skill:onUse(self, { from = player.id, cards = selected_cards, tos = targets, }) - end) + end, {tos = targets, cards = selected_cards, cost_data = {}}) return nil elseif skill:isInstanceOf(ViewAsSkill) then Self = player @@ -2284,7 +2107,7 @@ function Room:handleUseCardReply(player, data) local use = {} ---@type CardUseStruct use.from = player.id use.tos = {} - for _, target in ipairs(targets) do + for _, target in ipairs(targets or Util.DummyTable) do table.insert(use.tos, { target }) end if #use.tos == 0 then @@ -2343,10 +2166,10 @@ function Room:askForUseCard(player, card_name, pattern, prompt, cancelable, extr } self.logic:trigger(fk.AskForCardUse, player, askForUseCardData) + local useResult if askForUseCardData.result and type(askForUseCardData.result) == 'table' then - return askForUseCardData.result + useResult = askForUseCardData.result else - local useResult local disabledSkillNames = {} repeat @@ -2354,7 +2177,10 @@ function Room:askForUseCard(player, card_name, pattern, prompt, cancelable, extr local data = {card_name, pattern, prompt, cancelable, extra_data, disabledSkillNames} Fk.currentResponsePattern = pattern + self.logic:trigger(fk.HandleAskForPlayCard, nil, askForUseCardData, true) local result = self:doRequest(player, command, json.encode(data)) + askForUseCardData.afterRequest = true + self.logic:trigger(fk.HandleAskForPlayCard, nil, askForUseCardData, true) Fk.currentResponsePattern = nil if result ~= "" then @@ -2365,9 +2191,12 @@ function Room:askForUseCard(player, card_name, pattern, prompt, cancelable, extr end end until type(useResult) ~= "string" - return useResult + + askForUseCardData.result = useResult end - return nil + + self.logic:trigger(fk.AfterAskForCardUse, player, askForUseCardData) + return useResult end --- 询问一名玩家打出一张牌。 @@ -2396,37 +2225,46 @@ function Room:askForResponse(player, card_name, pattern, prompt, cancelable, ext cardName = card_name, pattern = pattern, extraData = extra_data, + eventData = effectData, } self.logic:trigger(fk.AskForCardResponse, player, eventData) + local responseResult if eventData.result then - return eventData.result + responseResult = eventData.result else - local useResult local disabledSkillNames = {} repeat - useResult = nil + responseResult = nil local data = {card_name, pattern, prompt, cancelable, extra_data, disabledSkillNames} Fk.currentResponsePattern = pattern + eventData.isResponse = true + self.logic:trigger(fk.HandleAskForPlayCard, nil, eventData, true) local result = self:doRequest(player, command, json.encode(data)) + eventData.afterRequest = true + self.logic:trigger(fk.HandleAskForPlayCard, nil, eventData, true) Fk.currentResponsePattern = nil if result ~= "" then - useResult = self:handleUseCardReply(player, result) + responseResult = self:handleUseCardReply(player, result) - if type(useResult) == "string" and useResult ~= "" then - table.insertIfNeed(disabledSkillNames, useResult) + if type(responseResult) == "string" and responseResult ~= "" then + table.insertIfNeed(disabledSkillNames, responseResult) end end - until type(useResult) ~= "string" + until type(responseResult) ~= "string" - if useResult then - return useResult.card + if responseResult then + responseResult = responseResult.card end + + eventData.result = responseResult end - return nil + + self.logic:trigger(fk.AfterAskForCardResponse, player, eventData) + return responseResult end --- 同时询问多名玩家是否使用某一张牌。 @@ -2438,9 +2276,11 @@ end ---@param prompt? string @ 提示信息 ---@param cancelable? boolean @ 能否点取消 ---@param extra_data? any @ 额外信息 +---@param effectData? CardEffectEvent @ 关联的卡牌生效流程 ---@return CardUseStruct? @ 最终决胜出的卡牌使用信息 -function Room:askForNullification(players, card_name, pattern, prompt, cancelable, extra_data) +function Room:askForNullification(players, card_name, pattern, prompt, cancelable, extra_data, effectData) if #players == 0 then + self.logic:trigger(fk.AfterAskForNullification, nil, { eventData = effectData }) return nil end @@ -2462,7 +2302,17 @@ function Room:askForNullification(players, card_name, pattern, prompt, cancelabl local data = {card_name, pattern, prompt, cancelable, extra_data, disabledSkillNames} Fk.currentResponsePattern = pattern + + local eventData = { + cardName = card_name, + pattern = pattern, + extraData = extra_data, + eventData = effectData, + } + self.logic:trigger(fk.HandleAskForPlayCard, nil, eventData, true) local winner = self:doRaceRequest(command, players, json.encode(data)) + eventData.afterRequest = true + self.logic:trigger(fk.HandleAskForPlayCard, nil, eventData, true) if winner then local result = winner.client_reply @@ -2475,6 +2325,11 @@ function Room:askForNullification(players, card_name, pattern, prompt, cancelabl Fk.currentResponsePattern = nil until type(useResult) ~= "string" + local askForNullificationData = { + result = useResult, + eventData = effectData, + } + self.logic:trigger(fk.AfterAskForNullification, nil, askForNullificationData) return useResult end @@ -2504,13 +2359,36 @@ function Room:askForAG(player, id_list, cancelable, reason) end --- 给player发一条消息,在他的窗口中用一系列卡牌填充一个AG。 ----@param player ServerPlayer @ 要通知的玩家 +---@param players ServerPlayer|ServerPlayer[] @ 要通知的玩家 ---@param id_list integer[] | Card[] @ 要填充的卡牌 ----@param disable_ids? integer[] | Card[] @ 未使用 -function Room:fillAG(player, id_list, disable_ids) +---@param disable_ids? integer[] | Card[] @ 未使用 不能选择的牌 +function Room:fillAG(players, id_list, disable_ids) + local record = self:getTag("AGrecord") or {} + local new = true + if players.id ~= nil then + --- FIXEME: 很危险的判断,AG以后肯定要大改了,先这样算了 + if #record > 0 and record[#record][2][1] == id_list[1] then + new = false + table.insert(record[#record][1], players.id) + end + players = { players } + end id_list = Card:getIdList(id_list) -- disable_ids = Card:getIdList(disable_ids) - player:doNotify("FillAG", json.encode{ id_list, disable_ids }) + if new then + --[[ 不用关闭AG,开新AG会覆盖 + if #record > 0 then + for _, pid in ipairs(record[#record][1]) do + self:getPlayerById(pid):doNotify("CloseAG", "") + end + end + --]] + table.insert(record, {table.map(players, Util.IdMapper), id_list, disable_ids, {}}) + end + self:setTag("AGrecord", record) + for _, player in ipairs(players) do + player:doNotify("FillAG", json.encode{ id_list, disable_ids }) + end end --- 告诉一些玩家,AG中的牌被taker取走了。 @@ -2519,6 +2397,12 @@ end ---@param notify_list? ServerPlayer[] @ 要告知的玩家,默认为全员 function Room:takeAG(taker, id, notify_list) self:doBroadcastNotify("TakeAG", json.encode{ taker.id, id }, notify_list) + local record = self:getTag("AGrecord") or {} + if #record > 0 then + local currentRecord = record[#record] + currentRecord[4][tostring(id)] = taker.id + self:setTag("AGrecord", record) + end end --- 关闭player那侧显示的AG。 @@ -2526,8 +2410,31 @@ end --- 若不传参(即player为nil),那么关闭所有玩家的AG。 ---@param player? ServerPlayer @ 要关闭AG的玩家 function Room:closeAG(player) + local record = self:getTag("AGrecord") or {} if player then player:doNotify("CloseAG", "") - else self:doBroadcastNotify("CloseAG", "") end + else + self:doBroadcastNotify("CloseAG", "") + end + if #record > 0 then + local currentRecord = record[#record] + if player then + table.removeOne(currentRecord[1], player.id) + self:setTag("AGrecord", record) + if #currentRecord[1] > 0 then return end + end + table.remove(record, #record) + self:setTag("AGrecord", record) + if #record > 0 then + local newRecord = record[#record] + local players = table.map(newRecord[1], Util.Id2PlayerMapper) + for _, p in ipairs(players) do + p:doNotify("FillAG", json.encode{ newRecord[2], newRecord[3] }) + end + for cid, pid in pairs(newRecord[4]) do + self:doBroadcastNotify("TakeAG", json.encode{ pid, tonumber(cid) }, players) + end + end + end end -- TODO: 重构request机制,不然这个还得手动拿client_reply @@ -2730,724 +2637,6 @@ function Room:askForChooseToMoveCardInBoard(player, prompt, skillName, cancelabl end end ------------------------------------------------------------------------- --- 使用牌 ------------------------------------------------------------------------- - ---- 根据卡牌使用数据,去实际使用这个卡牌。 ----@param cardUseEvent CardUseStruct @ 使用数据 ----@return boolean -function Room:useCard(cardUseEvent) - return execGameEvent(GameEvent.UseCard, cardUseEvent) -end - ----@param room Room ----@param cardUseEvent CardUseStruct ----@param aimEventCollaborators table ----@return boolean -local onAim = function(room, cardUseEvent, aimEventCollaborators) - local eventStages = { fk.TargetSpecifying, fk.TargetConfirming, fk.TargetSpecified, fk.TargetConfirmed } - for _, stage in ipairs(eventStages) do - if (not cardUseEvent.tos) or #cardUseEvent.tos == 0 then - return false - end - - room:sortPlayersByAction(cardUseEvent.tos, true) - local aimGroup = AimGroup:initAimGroup(TargetGroup:getRealTargets(cardUseEvent.tos)) - - local collaboratorsIndex = {} - local firstTarget = true - repeat - local toId = AimGroup:getUndoneOrDoneTargets(aimGroup)[1] - ---@type AimStruct - local aimStruct - local initialEvent = false - collaboratorsIndex[toId] = collaboratorsIndex[toId] or 1 - - if not aimEventCollaborators[toId] or collaboratorsIndex[toId] > #aimEventCollaborators[toId] then - aimStruct = { - from = cardUseEvent.from, - card = cardUseEvent.card, - to = toId, - targetGroup = cardUseEvent.tos, - nullifiedTargets = cardUseEvent.nullifiedTargets or {}, - tos = aimGroup, - firstTarget = firstTarget, - additionalDamage = cardUseEvent.additionalDamage, - additionalRecover = cardUseEvent.additionalRecover, - additionalEffect = cardUseEvent.additionalEffect, - extra_data = cardUseEvent.extra_data, - } - - local index = 1 - for _, targets in ipairs(cardUseEvent.tos) do - if index > collaboratorsIndex[toId] then - break - end - - if #targets > 1 then - for i = 2, #targets do - aimStruct.subTargets = {} - table.insert(aimStruct.subTargets, targets[i]) - end - end - end - - collaboratorsIndex[toId] = 1 - initialEvent = true - else - aimStruct = aimEventCollaborators[toId][collaboratorsIndex[toId]] - aimStruct.from = cardUseEvent.from - aimStruct.card = cardUseEvent.card - aimStruct.tos = aimGroup - aimStruct.targetGroup = cardUseEvent.tos - aimStruct.nullifiedTargets = cardUseEvent.nullifiedTargets or {} - aimStruct.firstTarget = firstTarget - aimStruct.additionalEffect = cardUseEvent.additionalEffect - aimStruct.extra_data = cardUseEvent.extra_data - end - - firstTarget = false - - room.logic:trigger(stage, (stage == fk.TargetSpecifying or stage == fk.TargetSpecified) and room:getPlayerById(aimStruct.from) or room:getPlayerById(aimStruct.to), aimStruct) - - AimGroup:removeDeadTargets(room, aimStruct) - - local aimEventTargetGroup = aimStruct.targetGroup - if aimEventTargetGroup then - room:sortPlayersByAction(aimEventTargetGroup, true) - end - - cardUseEvent.from = aimStruct.from - cardUseEvent.tos = aimEventTargetGroup - cardUseEvent.nullifiedTargets = aimStruct.nullifiedTargets - cardUseEvent.additionalEffect = aimStruct.additionalEffect - cardUseEvent.extra_data = aimStruct.extra_data - - if #AimGroup:getAllTargets(aimStruct.tos) == 0 then - return false - end - - local cancelledTargets = AimGroup:getCancelledTargets(aimStruct.tos) - if #cancelledTargets > 0 then - for _, target in ipairs(cancelledTargets) do - aimEventCollaborators[target] = {} - collaboratorsIndex[target] = 1 - end - end - aimStruct.tos[AimGroup.Cancelled] = {} - - aimEventCollaborators[toId] = aimEventCollaborators[toId] or {} - if room:getPlayerById(toId):isAlive() then - if initialEvent then - table.insert(aimEventCollaborators[toId], aimStruct) - else - aimEventCollaborators[toId][collaboratorsIndex[toId]] = aimStruct - end - - collaboratorsIndex[toId] = collaboratorsIndex[toId] + 1 - end - - AimGroup:setTargetDone(aimStruct.tos, toId) - aimGroup = aimStruct.tos - until #AimGroup:getUndoneOrDoneTargets(aimGroup) == 0 - end - - return true -end - ---- 对卡牌使用数据进行生效 ----@param cardUseEvent CardUseStruct -function Room:doCardUseEffect(cardUseEvent) - ---@type table - local aimEventCollaborators = {} - if cardUseEvent.tos and not onAim(self, cardUseEvent, aimEventCollaborators) then - return - end - - local realCardIds = self:getSubcardsByRule(cardUseEvent.card, { Card.Processing }) - - self.logic:trigger(fk.BeforeCardUseEffect, self:getPlayerById(cardUseEvent.from), cardUseEvent) - -- If using Equip or Delayed trick, move them to the area and return - if cardUseEvent.card.type == Card.TypeEquip then - if #realCardIds == 0 then - return - end - - local target = TargetGroup:getRealTargets(cardUseEvent.tos)[1] - if not (self:getPlayerById(target).dead or table.contains((cardUseEvent.nullifiedTargets or Util.DummyTable), target)) then - local existingEquipId - if cardUseEvent.toPutSlot and cardUseEvent.toPutSlot:startsWith("#EquipmentChoice") then - local index = cardUseEvent.toPutSlot:split(":")[2] - existingEquipId = self:getPlayerById(target):getEquipments(cardUseEvent.card.sub_type)[tonumber(index)] - elseif not self:getPlayerById(target):hasEmptyEquipSlot(cardUseEvent.card.sub_type) then - existingEquipId = self:getPlayerById(target):getEquipment(cardUseEvent.card.sub_type) - end - - if existingEquipId then - self:moveCards( - { - ids = { existingEquipId }, - from = target, - toArea = Card.DiscardPile, - moveReason = fk.ReasonPutIntoDiscardPile, - }, - { - ids = realCardIds, - to = target, - toArea = Card.PlayerEquip, - moveReason = fk.ReasonUse, - } - ) - else - self:moveCards({ - ids = realCardIds, - to = target, - toArea = Card.PlayerEquip, - moveReason = fk.ReasonUse, - }) - end - end - - return - elseif cardUseEvent.card.sub_type == Card.SubtypeDelayedTrick then - if #realCardIds == 0 then - return - end - - local target = TargetGroup:getRealTargets(cardUseEvent.tos)[1] - if not (self:getPlayerById(target).dead or table.contains((cardUseEvent.nullifiedTargets or Util.DummyTable), target)) then - local findSameCard = false - for _, cardId in ipairs(self:getPlayerById(target):getCardIds(Player.Judge)) do - if Fk:getCardById(cardId).trueName == cardUseEvent.card.trueName then - findSameCard = true - end - end - - if not findSameCard then - if cardUseEvent.card:isVirtual() then - self:getPlayerById(target):addVirtualEquip(cardUseEvent.card) - elseif cardUseEvent.card.name ~= Fk:getCardById(cardUseEvent.card.id, true).name then - local card = Fk:cloneCard(cardUseEvent.card.name) - card.skillNames = cardUseEvent.card.skillNames - card:addSubcard(cardUseEvent.card.id) - self:getPlayerById(target):addVirtualEquip(card) - else - self:getPlayerById(target):removeVirtualEquip(cardUseEvent.card.id) - end - - self:moveCards({ - ids = realCardIds, - to = target, - toArea = Card.PlayerJudge, - moveReason = fk.ReasonUse, - }) - - return - end - end - - return - end - - if not cardUseEvent.card.skill then - return - end - - ---@class CardEffectEvent - local cardEffectEvent = { - from = cardUseEvent.from, - tos = cardUseEvent.tos, - card = cardUseEvent.card, - toCard = cardUseEvent.toCard, - responseToEvent = cardUseEvent.responseToEvent, - nullifiedTargets = cardUseEvent.nullifiedTargets, - disresponsiveList = cardUseEvent.disresponsiveList, - unoffsetableList = cardUseEvent.unoffsetableList, - additionalDamage = cardUseEvent.additionalDamage, - additionalRecover = cardUseEvent.additionalRecover, - cardsResponded = cardUseEvent.cardsResponded, - prohibitedCardNames = cardUseEvent.prohibitedCardNames, - extra_data = cardUseEvent.extra_data, - } - - -- If using card to other card (like jink or nullification), simply effect and return - if cardUseEvent.toCard ~= nil then - self:doCardEffect(cardEffectEvent) - - if cardEffectEvent.cardsResponded then - cardUseEvent.cardsResponded = cardUseEvent.cardsResponded or {} - for _, card in ipairs(cardEffectEvent.cardsResponded) do - table.insertIfNeed(cardUseEvent.cardsResponded, card) - end - end - return - end - - for i = 1, (cardUseEvent.additionalEffect or 0) + 1 do - if #TargetGroup:getRealTargets(cardUseEvent.tos) > 0 and cardUseEvent.card.skill.onAction then - cardUseEvent.card.skill:onAction(self, cardUseEvent) - cardEffectEvent.extra_data = cardUseEvent.extra_data - end - - -- Else: do effect to all targets - local collaboratorsIndex = {} - for _, toId in ipairs(TargetGroup:getRealTargets(cardUseEvent.tos)) do - if not table.contains(cardUseEvent.nullifiedTargets, toId) and self:getPlayerById(toId):isAlive() then - if aimEventCollaborators[toId] then - cardEffectEvent.to = toId - collaboratorsIndex[toId] = collaboratorsIndex[toId] or 1 - local curAimEvent = aimEventCollaborators[toId][collaboratorsIndex[toId]] - - cardEffectEvent.subTargets = curAimEvent.subTargets - cardEffectEvent.additionalDamage = curAimEvent.additionalDamage - cardEffectEvent.additionalRecover = curAimEvent.additionalRecover - - if curAimEvent.disresponsiveList then - cardEffectEvent.disresponsiveList = cardEffectEvent.disresponsiveList or {} - - for _, disresponsivePlayer in ipairs(curAimEvent.disresponsiveList) do - if not table.contains(cardEffectEvent.disresponsiveList, disresponsivePlayer) then - table.insert(cardEffectEvent.disresponsiveList, disresponsivePlayer) - end - end - end - - if curAimEvent.unoffsetableList then - cardEffectEvent.unoffsetableList = cardEffectEvent.unoffsetableList or {} - - for _, unoffsetablePlayer in ipairs(curAimEvent.unoffsetableList) do - if not table.contains(cardEffectEvent.unoffsetableList, unoffsetablePlayer) then - table.insert(cardEffectEvent.unoffsetableList, unoffsetablePlayer) - end - end - end - - cardEffectEvent.disresponsive = curAimEvent.disresponsive - cardEffectEvent.unoffsetable = curAimEvent.unoffsetable - cardEffectEvent.fixedResponseTimes = curAimEvent.fixedResponseTimes - cardEffectEvent.fixedAddTimesResponsors = curAimEvent.fixedAddTimesResponsors - - collaboratorsIndex[toId] = collaboratorsIndex[toId] + 1 - - local curCardEffectEvent = table.simpleClone(cardEffectEvent) - self:doCardEffect(curCardEffectEvent) - - if curCardEffectEvent.cardsResponded then - cardUseEvent.cardsResponded = cardUseEvent.cardsResponded or {} - for _, card in ipairs(curCardEffectEvent.cardsResponded) do - table.insertIfNeed(cardUseEvent.cardsResponded, card) - end - end - - if type(curCardEffectEvent.nullifiedTargets) == 'table' then - table.insertTableIfNeed(cardUseEvent.nullifiedTargets, curCardEffectEvent.nullifiedTargets) - end - end - end - end - - if #TargetGroup:getRealTargets(cardUseEvent.tos) > 0 and cardUseEvent.card.skill.onAction then - cardUseEvent.card.skill:onAction(self, cardUseEvent, true) - end - end -end - ---- 对卡牌效果数据进行生效 ----@param cardEffectEvent CardEffectEvent -function Room:doCardEffect(cardEffectEvent) - return execGameEvent(GameEvent.CardEffect, cardEffectEvent) -end - ----@param cardEffectEvent CardEffectEvent -function Room:handleCardEffect(event, cardEffectEvent) - if event == fk.PreCardEffect then - if cardEffectEvent.card.skill:aboutToEffect(self, cardEffectEvent) then return end - if - cardEffectEvent.card.trueName == "slash" and - not (cardEffectEvent.unoffsetable or table.contains(cardEffectEvent.unoffsetableList or Util.DummyTable, cardEffectEvent.to)) - then - local loopTimes = 1 - if cardEffectEvent.fixedResponseTimes then - if type(cardEffectEvent.fixedResponseTimes) == "table" then - loopTimes = cardEffectEvent.fixedResponseTimes["jink"] or 1 - elseif type(cardEffectEvent.fixedResponseTimes) == "number" then - loopTimes = cardEffectEvent.fixedResponseTimes - end - end - Fk.currentResponsePattern = "jink" - - for i = 1, loopTimes do - local to = self:getPlayerById(cardEffectEvent.to) - local prompt = "" - if cardEffectEvent.from then - if loopTimes == 1 then - prompt = "#slash-jink:" .. cardEffectEvent.from - else - prompt = "#slash-jink-multi:" .. cardEffectEvent.from .. "::" .. i .. ":" .. loopTimes - end - end - - local use = self:askForUseCard( - to, - "jink", - nil, - prompt, - true, - nil, - cardEffectEvent - ) - if use then - use.toCard = cardEffectEvent.card - use.responseToEvent = cardEffectEvent - self:useCard(use) - end - - if not cardEffectEvent.isCancellOut then - break - end - - cardEffectEvent.isCancellOut = i == loopTimes - end - elseif - cardEffectEvent.card.type == Card.TypeTrick and - not (cardEffectEvent.disresponsive or cardEffectEvent.unoffsetable) and - not table.contains(cardEffectEvent.prohibitedCardNames or Util.DummyTable, "nullification") - then - local players = {} - Fk.currentResponsePattern = "nullification" - local cardCloned = Fk:cloneCard("nullification") - for _, p in ipairs(self.alive_players) do - if not p:prohibitUse(cardCloned) then - local cards = p:getHandlyIds() - for _, cid in ipairs(cards) do - if - Fk:getCardById(cid).trueName == "nullification" and - not ( - table.contains(cardEffectEvent.disresponsiveList or Util.DummyTable, p.id) or - table.contains(cardEffectEvent.unoffsetableList or Util.DummyTable, p.id) - ) - then - table.insert(players, p) - break - end - end - if not table.contains(players, p) then - Self = p -- for enabledAtResponse - for _, s in ipairs(table.connect(p.player_skills, p._fake_skills)) do - if - s.pattern and - Exppattern:Parse("nullification"):matchExp(s.pattern) and - not (s.enabledAtResponse and not s:enabledAtResponse(p)) and - not ( - table.contains(cardEffectEvent.disresponsiveList or Util.DummyTable, p.id) or - table.contains(cardEffectEvent.unoffsetableList or Util.DummyTable, p.id) - ) - then - table.insert(players, p) - break - end - end - end - end - end - - local prompt = "" - if cardEffectEvent.to then - prompt = "#AskForNullification::" .. cardEffectEvent.to .. ":" .. cardEffectEvent.card.name - elseif cardEffectEvent.from then - prompt = "#AskForNullificationWithoutTo:" .. cardEffectEvent.from .. "::" .. cardEffectEvent.card.name - end - - local extra_data - if #TargetGroup:getRealTargets(cardEffectEvent.tos) > 1 then - local parentUseEvent = self.logic:getCurrentEvent():findParent(GameEvent.UseCard) - if parentUseEvent then - extra_data = { useEventId = parentUseEvent.id, effectTo = cardEffectEvent.to } - end - end - local use = self:askForNullification(players, nil, nil, prompt, true, extra_data) - if use then - use.toCard = cardEffectEvent.card - use.responseToEvent = cardEffectEvent - self:useCard(use) - end - end - Fk.currentResponsePattern = nil - elseif event == fk.CardEffecting then - if cardEffectEvent.card.skill then - execGameEvent(GameEvent.SkillEffect, function () - cardEffectEvent.card.skill:onEffect(self, cardEffectEvent) - end, self:getPlayerById(cardEffectEvent.from), cardEffectEvent.card.skill) - end - end -end - ---- 对“打出牌”进行处理 ----@param cardResponseEvent CardResponseEvent -function Room:responseCard(cardResponseEvent) - return execGameEvent(GameEvent.RespondCard, cardResponseEvent) -end - ----@param card_name string @ 想要视为使用的牌名 ----@param subcards? integer[] @ 子卡,可以留空或者直接nil ----@param from ServerPlayer @ 使用来源 ----@param tos ServerPlayer | ServerPlayer[] @ 目标角色(列表) ----@param skillName? string @ 技能名 ----@param extra? boolean @ 是否不计入次数 ----@return CardUseStruct -function Room:useVirtualCard(card_name, subcards, from, tos, skillName, extra) - local card = Fk:cloneCard(card_name) - card.skillName = skillName - - if from:prohibitUse(card) then return false end - - if tos.class then tos = { tos } end - for i, p in ipairs(tos) do - if from:isProhibited(p, card) then - table.remove(tos, i) - end - end - - if #tos == 0 then return false end - - if subcards then card:addSubcards(Card:getIdList(subcards)) end - - local use = {} ---@type CardUseStruct - use.from = from.id - use.tos = table.map(tos, function(p) return { p.id } end) - use.card = card - use.extraUse = extra - self:useCard(use) - - return use -end - ------------------------------------------------------------------------- --- 移动牌 ------------------------------------------------------------------------- - ---- 传入一系列移牌信息,去实际移动这些牌 ----@vararg CardsMoveInfo ----@return boolean? -function Room:moveCards(...) - return execGameEvent(GameEvent.MoveCards, ...) -end - ---- 让一名玩家获得一张牌 ----@param player integer|ServerPlayer @ 要拿牌的玩家 ----@param card integer|integer[]|Card|Card[] @ 要拿到的卡牌 ----@param unhide? boolean @ 是否明着拿 ----@param reason? CardMoveReason @ 卡牌移动的原因 ----@param proposer? integer @ 移动操作者的id ----@param skill_name? string @ 技能名 ----@param moveMark? table|string @ 移动后自动赋予标记,格式:{标记名(支持-inarea后缀,移出值代表区域后清除), 值} ----@param visiblePlayers? integer|integer[] @ 控制移动对特定角色可见(在moveVisible为false时生效) -function Room:obtainCard(player, card, unhide, reason, proposer, skill_name, moveMark, visiblePlayers) - local pid = type(player) == "number" and player or player.id - self:moveCardTo(card, Card.PlayerHand, player, reason, skill_name, nil, unhide, proposer or pid, moveMark, visiblePlayers) -end - ---- 让玩家摸牌 ----@param player ServerPlayer @ 摸牌的玩家 ----@param num integer @ 摸牌数 ----@param skillName? string @ 技能名 ----@param fromPlace? string @ 摸牌的位置,"top" 或者 "bottom" ----@param moveMark? table|string @ 移动后自动赋予标记,格式:{标记名(支持-inarea后缀,移出值代表区域后清除), 值} ----@return integer[] @ 摸到的牌 -function Room:drawCards(player, num, skillName, fromPlace, moveMark) - local drawData = { - who = player, - num = num, - skillName = skillName, - fromPlace = fromPlace, - } - if self.logic:trigger(fk.BeforeDrawCard, player, drawData) then - return {} - end - - num = drawData.num - fromPlace = drawData.fromPlace - player = drawData.who - - local topCards = self:getNCards(num, fromPlace) - self:moveCards({ - ids = topCards, - to = player.id, - toArea = Card.PlayerHand, - moveReason = fk.ReasonDraw, - proposer = player.id, - skillName = skillName, - moveMark = moveMark, - }) - - return { table.unpack(topCards) } -end - ---- 将一张或多张牌移动到某处 ----@param card integer | integer[] | Card | Card[] @ 要移动的牌 ----@param to_place integer @ 移动的目标位置 ----@param target? ServerPlayer|integer @ 移动的目标角色 ----@param reason? integer @ 移动时使用的移牌原因 ----@param skill_name? string @ 技能名 ----@param special_name? string @ 私人牌堆名 ----@param visible? boolean @ 是否明置 ----@param proposer? integer @ 移动操作者的id ----@param moveMark? table|string @ 移动后自动赋予标记,格式:{标记名(支持-inarea后缀,移出值代表区域后清除), 值} ----@param visiblePlayers? integer|integer[] @ 控制移动对特定角色可见(在moveVisible为false时生效) -function Room:moveCardTo(card, to_place, target, reason, skill_name, special_name, visible, proposer, moveMark, visiblePlayers) - reason = reason or fk.ReasonJustMove - skill_name = skill_name or "" - special_name = special_name or "" - local ids = Card:getIdList(card) - - local to - if table.contains( - {Card.PlayerEquip, Card.PlayerHand, - Card.PlayerJudge, Card.PlayerSpecial}, to_place) then - assert(target) - if type(target) == "number" then - to = target - else - to = target.id - end - end - - local movesSplitedByOwner = {} - for _, cardId in ipairs(ids) do - local moveFound = table.find(movesSplitedByOwner, function(move) - return move.from == self.owner_map[cardId] - end) - - if moveFound then - table.insert(moveFound.ids, cardId) - else - table.insert(movesSplitedByOwner, { - ids = { cardId }, - from = self.owner_map[cardId], - to = to, - toArea = to_place, - moveReason = reason, - skillName = skill_name, - specialName = special_name, - moveVisible = visible, - proposer = proposer, - moveMark = moveMark, - visiblePlayers = visiblePlayers, - }) - end - end - - self:moveCards(table.unpack(movesSplitedByOwner)) -end - ---- 将一些卡牌同时分配给一些角色。 ----@param room Room @ 房间 ----@param list table @ 分配牌和角色的数据表,键为角色id,值为分配给其的牌id数组 ----@param proposer? integer @ 操作者的id。默认为空 ----@param skillName? string @ 技能名。默认为“分配” ----@return table @ 返回成功分配的卡牌 -function Room:doYiji(room, list, proposer, skillName) - skillName = skillName or "distribution_skill" - local moveInfos = {} - local move_ids = {} - for to, cards in pairs(list) do - local toP = room:getPlayerById(to) - local handcards = toP:getCardIds("h") - cards = table.filter(cards, function (id) return not table.contains(handcards, id) end) - if #cards > 0 then - table.insertTable(move_ids, cards) - local moveMap = {} - local noFrom = {} - for _, id in ipairs(cards) do - local from = room.owner_map[id] - if from then - moveMap[from] = moveMap[from] or {} - table.insert(moveMap[from], id) - else - table.insert(noFrom, id) - end - end - for from, _cards in pairs(moveMap) do - table.insert(moveInfos, { - ids = _cards, - moveInfo = table.map(_cards, function(id) - return {cardId = id, fromArea = room:getCardArea(id), fromSpecialName = room:getPlayerById(from):getPileNameOfId(id)} - end), - from = from, - to = to, - toArea = Card.PlayerHand, - moveReason = fk.ReasonGive, - proposer = proposer, - skillName = skillName, - }) - end - if #noFrom > 0 then - table.insert(moveInfos, { - ids = noFrom, - to = to, - toArea = Card.PlayerHand, - moveReason = fk.ReasonGive, - proposer = proposer, - skillName = skillName, - }) - end - end - end - if #moveInfos > 0 then - room:moveCards(table.unpack(moveInfos)) - end - return move_ids -end - ---- 将一张牌移动至某角色的装备区,若不合法则置入弃牌堆。目前没做相同副类别装备同时置入的适配(甘露神典韦) ----@param target ServerPlayer @ 接受牌的角色 ----@param cards integer|integer[] @ 移动的牌 ----@param skillName? string @ 技能名 ----@param convert? boolean @ 是否可以替换装备(默认可以) ----@param proposer? ServerPlayer @ 操作者 -function Room:moveCardIntoEquip(target, cards, skillName, convert, proposer) - convert = (convert == nil) and true or convert - skillName = skillName or "" - cards = type(cards) == "table" and cards or {cards} - local moves = {} - for _, cardId in ipairs(cards) do - local card = Fk:getCardById(cardId) - local fromId = self.owner_map[cardId] - local proposerId = proposer and proposer.id or nil - if target:canMoveCardIntoEquip(cardId, convert) then - if target:hasEmptyEquipSlot(card.sub_type) then - table.insert(moves,{ids = {cardId}, from = fromId, to = target.id, toArea = Card.PlayerEquip, moveReason = fk.ReasonPut,skillName = skillName,proposer = proposerId}) - else - local existingEquip = target:getEquipments(card.sub_type) - local throw = #existingEquip == 1 and existingEquip[1] or - self:askForCardChosen(proposer or target, target, {card_data = { {Util.convertSubtypeAndEquipSlot(card.sub_type),existingEquip} } }, "replaceEquip","#replaceEquip") - table.insert(moves,{ids = {throw}, from = target.id, toArea = Card.DiscardPile, moveReason = fk.ReasonPutIntoDiscardPile, skillName = skillName,proposer = proposerId}) - table.insert(moves,{ids = {cardId}, from = fromId, to = target.id, toArea = Card.PlayerEquip, moveReason = fk.ReasonPut,skillName = skillName,proposer = proposerId}) - end - else - table.insert(moves,{ids = {cardId}, from = fromId, toArea = Card.DiscardPile, moveReason = fk.ReasonPutIntoDiscardPile,skillName = skillName}) - end - end - self:moveCards(table.unpack(moves)) -end ------------------------------------------------------------------------- --- 其他游戏事件 ------------------------------------------------------------------------- - --- 与体力值等有关的事件 - ---- 改变一名玩家的体力。 ----@param player ServerPlayer @ 玩家 ----@param num integer @ 变化量 ----@param reason? string @ 原因 ----@param skillName? string @ 技能名 ----@param damageStruct? DamageStruct @ 伤害数据 ----@return boolean -function Room:changeHp(player, num, reason, skillName, damageStruct) - return execGameEvent(GameEvent.ChangeHp, player, num, reason, skillName, damageStruct) -end - --- 改变玩家的护甲数 ---@param player ServerPlayer ---@param num integer @ 变化量 @@ -3458,265 +2647,6 @@ function Room:changeShield(player, num) self:broadcastProperty(player, "shield") end ---- 令一名玩家失去体力。 ----@param player ServerPlayer @ 玩家 ----@param num integer @ 失去的数量 ----@param skillName? string @ 技能名 ----@return boolean -function Room:loseHp(player, num, skillName) - return execGameEvent(GameEvent.LoseHp, player, num, skillName) -end - ---- 改变一名玩家的体力上限。 ----@param player ServerPlayer @ 玩家 ----@param num integer @ 变化量 ----@return boolean -function Room:changeMaxHp(player, num) - return execGameEvent(GameEvent.ChangeMaxHp, player, num) -end - ---- 根据伤害数据造成伤害。 ----@param damageStruct DamageStruct ----@return boolean -function Room:damage(damageStruct) - return execGameEvent(GameEvent.Damage, damageStruct) -end - ---- 根据回复数据回复体力。 ----@param recoverStruct RecoverStruct ----@return boolean -function Room:recover(recoverStruct) - return execGameEvent(GameEvent.Recover, recoverStruct) -end - ---- 根据濒死数据让人进入濒死。 ----@param dyingStruct DyingStruct -function Room:enterDying(dyingStruct) - return execGameEvent(GameEvent.Dying, dyingStruct) -end - ---- 根据死亡数据杀死角色。 ----@param deathStruct DeathStruct -function Room:killPlayer(deathStruct) - return execGameEvent(GameEvent.Death, deathStruct) -end - --- 与失去/获得技能有关的事件 - ---- 令一名玩家获得/失去技能。 ---- ---- skill_names 是字符串数组或者用管道符号(|)分割的字符串。 ---- ---- 每个skill_name都是要获得的技能的名。如果在skill_name前面加上"-",那就是失去技能。 ----@param player ServerPlayer @ 玩家 ----@param skill_names string[] | string @ 要获得/失去的技能 ----@param source_skill? string | Skill @ 源技能 ----@param no_trigger? boolean @ 是否不触发相关时机 -function Room:handleAddLoseSkills(player, skill_names, source_skill, sendlog, no_trigger) - if type(skill_names) == "string" then - skill_names = skill_names:split("|") - end - - if sendlog == nil then sendlog = true end - - if #skill_names == 0 then return end - local losts = {} ---@type boolean[] - local triggers = {} ---@type Skill[] - local lost_piles = {} ---@type integer[] - for _, skill in ipairs(skill_names) do - if string.sub(skill, 1, 1) == "-" then - local actual_skill = string.sub(skill, 2, #skill) - if player:hasSkill(actual_skill, true, true) then - local lost_skills = player:loseSkill(actual_skill, source_skill) - for _, s in ipairs(lost_skills) do - self:doBroadcastNotify("LoseSkill", json.encode{ - player.id, - s.name - }) - - if sendlog and s.visible then - self:sendLog{ - type = "#LoseSkill", - from = player.id, - arg = s.name - } - end - - table.insert(losts, true) - table.insert(triggers, s) - if s.derived_piles then - for _, pile_name in ipairs(s.derived_piles) do - table.insertTableIfNeed(lost_piles, player:getPile(pile_name)) - end - end - end - end - else - local sk = Fk.skills[skill] - if sk and not player:hasSkill(sk, true, true) then - local got_skills = player:addSkill(sk, source_skill) - - for _, s in ipairs(got_skills) do - -- TODO: limit skill mark - - self:doBroadcastNotify("AddSkill", json.encode{ - player.id, - s.name - }) - - if sendlog and s.visible then - self:sendLog{ - type = "#AcquireSkill", - from = player.id, - arg = s.name - } - end - - table.insert(losts, false) - table.insert(triggers, s) - end - end - end - end - - if (not no_trigger) and #triggers > 0 then - for i = 1, #triggers do - local event = losts[i] and fk.EventLoseSkill or fk.EventAcquireSkill - self.logic:trigger(event, player, triggers[i]) - end - end - - if #lost_piles > 0 then - self:moveCards({ - ids = lost_piles, - from = player.id, - toArea = Card.DiscardPile, - moveReason = fk.ReasonPutIntoDiscardPile, - }) - end -end - --- 判定 - ---- 根据判定数据进行判定。判定的结果直接保存在这个数据中。 ----@param data JudgeStruct -function Room:judge(data) - return execGameEvent(GameEvent.Judge, data) -end - ---- 改判。 ----@param card Card @ 改判的牌 ----@param player ServerPlayer @ 改判的玩家 ----@param judge JudgeStruct @ 要被改判的判定数据 ----@param skillName? string @ 技能名 ----@param exchange? boolean @ 是否要替换原有判定牌(即类似鬼道那样) -function Room:retrial(card, player, judge, skillName, exchange) - if not card then return end - local triggerResponded = self.owner_map[card:getEffectiveId()] == player - local isHandcard = (triggerResponded and self:getCardArea(card:getEffectiveId()) == Card.PlayerHand) - - if triggerResponded then - local resp = {} ---@type CardResponseEvent - resp.from = player.id - resp.card = card - resp.skipDrop = true - self:responseCard(resp) - else - local move1 = {} ---@type CardsMoveInfo - move1.ids = { card:getEffectiveId() } - move1.from = player.id - move1.toArea = Card.Processing - move1.moveReason = fk.ReasonJustMove - move1.skillName = skillName - self:moveCards(move1) - end - - local oldJudge = judge.card - judge.card = card - local rebyre = judge.retrial_by_response - judge.retrial_by_response = player - - self:sendLog{ - type = "#ChangedJudge", - from = player.id, - to = { judge.who.id }, - arg2 = card:toLogString(), - arg = skillName, - } - - Fk:filterCard(judge.card.id, judge.who, judge) - - exchange = exchange and not player.dead - - local move2 = {} ---@type CardsMoveInfo - move2.ids = { oldJudge:getEffectiveId() } - move2.toArea = exchange and Card.PlayerHand or Card.DiscardPile - move2.moveReason = exchange and fk.ReasonJustMove or fk.ReasonJudge - move2.to = exchange and player.id or nil - move2.skillName = skillName - - self:moveCards(move2) -end - ---- 弃置一名角色的牌。 ----@param card_ids integer[]|integer @ 被弃掉的牌 ----@param skillName? string @ 技能名 ----@param who ServerPlayer @ 被弃牌的人 ----@param thrower? ServerPlayer @ 弃别人牌的人 -function Room:throwCard(card_ids, skillName, who, thrower) - if type(card_ids) == "number" then - card_ids = {card_ids} - end - skillName = skillName or "" - thrower = thrower or who - self:moveCards({ - ids = card_ids, - from = who.id, - toArea = Card.DiscardPile, - moveReason = fk.ReasonDiscard, - proposer = thrower.id, - skillName = skillName - }) -end - ---- 重铸一名角色的牌。 ----@param card_ids integer[] @ 被重铸的牌 ----@param who ServerPlayer @ 重铸的角色 ----@param skillName? string @ 技能名,默认为“重铸” ----@return integer[] @ 摸到的牌 -function Room:recastCard(card_ids, who, skillName) - if type(card_ids) == "number" then - card_ids = {card_ids} - end - skillName = skillName or "recast" - self:moveCards({ - ids = card_ids, - from = who.id, - toArea = Card.DiscardPile, - skillName = skillName, - moveReason = fk.ReasonRecast, - proposer = who.id - }) - self:sendFootnote(card_ids, { - type = "##RecastCard", - from = who.id, - }) - self:broadcastPlaySound("./audio/system/recast") - self:sendLog{ - type = skillName == "recast" and "#Recast" or "#RecastBySkill", - from = who.id, - card = card_ids, - arg = skillName, - } - return self:drawCards(who, #card_ids, skillName) -end - ---- 根据拼点信息开始拼点。 ----@param pindianData PindianStruct -function Room:pindian(pindianData) - return execGameEvent(GameEvent.Pindian, pindianData) -end - -- 杂项函数 function Room:adjustSeats() @@ -3776,66 +2706,15 @@ end --- 洗牌。 function Room:shuffleDrawPile() - if #self.draw_pile + #self.discard_pile == 0 then - return - end + local seed = math.random(2 << 32 - 1) + AbstractRoom.shuffleDrawPile(self, seed) - table.insertTable(self.draw_pile, self.discard_pile) - for _, id in ipairs(self.discard_pile) do - self:setCardArea(id, Card.DrawPile, nil) - end - self.discard_pile = {} - table.shuffle(self.draw_pile) - - self:doBroadcastNotify("UpdateDrawPile", #self.draw_pile) + -- self:doBroadcastNotify("UpdateDrawPile", #self.draw_pile) + self:doBroadcastNotify("ShuffleDrawPile", seed) self.logic:trigger(fk.AfterDrawPileShuffle, nil, {}) end ---- 使用技能。先增加技能发动次数,再执行相应的函数。 ----@param player ServerPlayer @ 发动技能的玩家 ----@param skill Skill @ 发动的技能 ----@param effect_cb fun() @ 实际要调用的函数 -function Room:useSkill(player, skill, effect_cb) - player:revealBySkillName(skill.name) - if not skill.mute then - if skill.attached_equip then - local equip = Fk.all_card_types[skill.attached_equip] - local pkgPath = "./packages/" .. equip.package.extensionName - local soundName = pkgPath .. "/audio/card/" .. equip.name - self:broadcastPlaySound(soundName) - self:sendLog{ - type = "#InvokeSkill", - from = player.id, - arg = skill.name, - } - self:setEmotion(player, pkgPath .. "/image/anim/" .. equip.name) - else - player:broadcastSkillInvoke(skill.name) - self:notifySkillInvoked(player, skill.name) - end - end - - if skill:isSwitchSkill() then - local switchSkillName = skill.switchSkillName - self:setPlayerMark( - player, - MarkEnum.SwithSkillPreName .. switchSkillName, - player:getSwitchSkillState(switchSkillName, true) - ) - end - - if effect_cb then - return execGameEvent(GameEvent.SkillEffect, effect_cb, player, skill) - end -end - ----@param player ServerPlayer ----@param sendLog? bool -function Room:revivePlayer(player, sendLog, reason) - return execGameEvent(GameEvent.Revive, player, sendLog, reason) -end - ---@param room Room local function shouldUpdateWinRate(room) if room.settings.enableFreeAssign then @@ -3903,84 +2782,6 @@ function Room:gameOver(winner) end end ----@param card Card ----@param fromAreas? CardArea[] ----@return integer[] -function Room:getSubcardsByRule(card, fromAreas) - if card:isVirtual() and #card.subcards == 0 then - return {} - end - - local cardIds = {} - fromAreas = fromAreas or Util.DummyTable - for _, cardId in ipairs(card:isVirtual() and card.subcards or { card.id }) do - if #fromAreas == 0 or table.contains(fromAreas, self:getCardArea(cardId)) then - table.insert(cardIds, cardId) - end - end - - return cardIds -end - ----@param pattern string ----@param num? number ----@param fromPile? string @ 查找的来源区域,值为drawPile|discardPile|allPiles ----@return integer[] @ id列表 可能空 -function Room:getCardsFromPileByRule(pattern, num, fromPile) - num = num or 1 - local pileToSearch = self.draw_pile - if fromPile == "discardPile" then - pileToSearch = self.discard_pile - elseif fromPile == "allPiles" then - pileToSearch = table.simpleClone(self.draw_pile) - table.insertTable(pileToSearch, self.discard_pile) - end - - if #pileToSearch == 0 then - return {} - end - - local cardPack = {} - if num < 3 then - for i = 1, num do - local randomIndex = math.random(1, #pileToSearch) - local curIndex = randomIndex - repeat - local curCardId = pileToSearch[curIndex] - if Fk:getCardById(curCardId):matchPattern(pattern) and not table.contains(cardPack, curCardId) then - table.insert(cardPack, pileToSearch[curIndex]) - break - end - - curIndex = curIndex + 1 - if curIndex > #pileToSearch then - curIndex = 1 - end - until curIndex == randomIndex - - if #cardPack == 0 then - break - end - end - else - local matchedIds = {} - for _, id in ipairs(pileToSearch) do - if Fk:getCardById(id):matchPattern(pattern) then - table.insert(matchedIds, id) - end - end - - local loopTimes = math.min(num, #matchedIds) - for i = 1, loopTimes do - local randomCardId = matchedIds[math.random(1, #matchedIds)] - table.insert(cardPack, randomCardId) - table.removeOne(matchedIds, randomCardId) - end - end - - return cardPack -end - ---@param flag? string ---@param players? ServerPlayer[] ---@param excludeIds? integer[] @@ -4014,10 +2815,7 @@ end ---@param number? integer @ 点数 ---@return Card function Room:printCard(name, suit, number) - local cd = Fk:cloneCard(name, suit, number) - Fk:_addPrintedCard(cd) - table.insert(self.void, cd.id) - self:setCardArea(cd.id, Card.Void, nil) + local cd = AbstractRoom.printCard(self, name, suit, number) self:doBroadcastNotify("PrintCard", json.encode{ name, suit, number }) return cd end @@ -4188,4 +2986,54 @@ function Room:setPlayerRest(player, roundNum) self:broadcastProperty(player, "rest") end +--- 结束当前回合(不会终止结算) +function Room:endTurn() + self.current._phase_end = true + self:setTag("endTurn", true) +end + +--清理遗留在处理区的卡牌 +---@param cards? integer[] @ 待清理的卡牌。不填则清理处理区所有牌 +---@param skillName? string @ 技能名 +function Room:cleanProcessingArea(cards, skillName) + local throw = cards and table.filter(cards, function(id) return self:getCardArea(id) == Card.Processing end) or self.processing_area + if #throw > 0 then + self:moveCardTo(throw, Card.DiscardPile, nil, fk.ReasonPutIntoDiscardPile, skillName) + end +end + +--- 为角色或牌的表型标记添加值 +---@param sth ServerPlayer|Card @ 更新标记的玩家或卡牌 +---@param mark string @ 标记的名称 +---@param value any @ 要增加的值 +function Room:addTableMark(sth, mark, value) + local t = sth:getTableMark(mark) + table.insert(t, value) + if sth:isInstanceOf(Card) then + self:setCardMark(sth, mark, t) + else + self:setPlayerMark(sth, mark, t) + end +end + +--- 为角色或牌的表型标记移除值 +---@param sth ServerPlayer|Card @ 更新标记的玩家或卡牌 +---@param mark string @ 标记的名称 +---@param value any @ 要移除的值 +function Room:removeTableMark(sth, mark, value) + local t = sth:getTableMark(mark) + table.removeOne(t, value) + if sth:isInstanceOf(Card) then + self:setCardMark(sth, mark, #t > 0 and t or 0) + else + self:setPlayerMark(sth, mark, #t > 0 and t or 0) + end +end + +function Room:__index(k) + if k == "room_settings" then + return self.settings + end +end + return Room diff --git a/lua/server/serverplayer.lua b/lua/server/serverplayer.lua index 5e35ae52..4fad62c8 100644 --- a/lua/server/serverplayer.lua +++ b/lua/server/serverplayer.lua @@ -14,7 +14,6 @@ ---@field public skipped_phases Phase[] ---@field public phase_state table[] ---@field public phase_index integer ----@field public role_shown boolean ---@field private _fake_skills Skill[] ---@field private _manually_fake_skills Skill[] ---@field public prelighted_skills Skill[] @@ -32,11 +31,12 @@ function ServerPlayer:initialize(_self) self.room = nil -- Below are for doBroadcastRequest + -- 但是几乎全部被船新request杀了 self.request_data = "" - self.client_reply = "" + --self.client_reply = "" self.default_reply = "" - self.reply_ready = false - self.reply_cancel = false + --self.reply_ready = false + --self.reply_cancel = false self.phases = {} self.skipped_phases = {} self.phase_state = {} @@ -74,92 +74,14 @@ function ServerPlayer:doNotify(command, jsonData) end end ---- Send a request to client, and allow client to reply within *timeout* seconds. ---- ---- *timeout* must not be negative. If nil, room.timeout is used. ----@param command string ----@param jsonData string ----@param timeout? integer -function ServerPlayer:doRequest(command, jsonData, timeout) - self.client_reply = "" - self.reply_ready = false - self.reply_cancel = false - - if self.serverplayer:busy() then - self.room.request_queue[self.serverplayer] = self.room.request_queue[self.serverplayer] or {} - table.insert(self.room.request_queue[self.serverplayer], { self.id, command, jsonData, timeout }) - return - end - - self.room.request_self[self.serverplayer:getId()] = self.id - - if not table.contains(self._observers, self.serverplayer) then - self.serverplayer:doNotify("StartChangeSelf", tostring(self.id)) - end - - timeout = timeout or self.room.timeout - self.serverplayer:setBusy(true) - self.ai_data = { - command = command, - jsonData = jsonData, - } - self.serverplayer:doRequest(command, jsonData, timeout) -end - -local function _waitForReply(player, timeout) - local result - local start = os.getms() - local state = player.serverplayer:getState() - player.request_timeout = timeout - player.request_start = start - if state ~= fk.Player_Online then - if player.room.hasSurrendered then - return "__cancel" - end - - if state ~= fk.Player_Robot then - player.room:checkNoHuman() - player.room:delay(500) - return "__cancel" - end - -- Let AI make reply. First handle request - -- coroutine.yield("__handleRequest", 0) - - player.room:checkNoHuman() - player.ai:readRequestData() - local reply = player.ai:makeReply() - if reply == "" then reply = "__cancel" end - return reply - end - while true do - player.serverplayer:setThinking(true) - result = player.serverplayer:waitForReply(0) - if result ~= "__notready" then - player._timewaste_count = 0 - player.serverplayer:setThinking(false) - return result - end - local rest = timeout * 1000 - (os.getms() - start) / 1000 - if timeout and rest <= 0 then - if timeout >= 15 then - player._timewaste_count = player._timewaste_count + 1 - end - player.serverplayer:setThinking(false) - - if player._timewaste_count >= 3 then - player._timewaste_count = 0 - player.serverplayer:emitKick() - end - - return "" - end - - if player.room.hasSurrendered then - player.serverplayer:setThinking(false) - return "" - end - - coroutine.yield("__handleRequest", rest) +-- FIXME: 基本都改成新写法后删了这个兼容玩意 +function ServerPlayer:__index(k) + local request = self.room.last_request + if not request then return nil end + if k == "client_reply" then + return request.result[self.id] + elseif k == "reply_ready" then + return request.result[self.id] and request.result[self.id] ~= "" end end @@ -173,128 +95,27 @@ function ServerPlayer:chat(msg) }) end ---- Wait for at most *timeout* seconds for reply from client. ---- ---- If *timeout* is negative or **nil**, the function will wait forever until get reply. ----@param timeout integer @ seconds to wait ----@return string @ JSON data -function ServerPlayer:waitForReply(timeout) - local result = _waitForReply(self, timeout) - local sid = self.serverplayer:getId() - local id = self.id - if self.room.request_self[sid] ~= id then - result = "" - end - - self.request_data = "" - self.client_reply = result - if result == "__cancel" then - result = "" - self.reply_cancel = true - self.serverplayer:setBusy(false) - self.serverplayer:setThinking(false) - end - if result ~= "" then - self.reply_ready = true - self.serverplayer:setBusy(false) - self.serverplayer:setThinking(false) - end - - -- FIXME: 一控多求无懈 - local queue = self.room.request_queue[self.serverplayer] - if queue and #queue > 0 and not self.serverplayer:busy() then - local i, c, j, t = table.unpack(table.remove(queue, 1)) - self.room:getPlayerById(i):doRequest(c, j, t) - end - - return result -end - -local function assign(t1, t2, k) - t1[k] = t2[k] -end - --- 获取摘要信息。供重连/旁观使用 --- 根据参数,返回一个大表保存自己的信息,客户端自行分析 ----@param player ServerPlayer ----@param observe? boolean -function ServerPlayer:getSummary(player, observe) - local room = self.room - if not room.game_started then - local ret = { p = {} } - -- If game does not starts, that mean we are entering room that - -- all players are choosing their generals. - -- Note that when we are in this function, the main thread must be - -- calling delay() or waiting for reply. - if self.role_shown then - -- room:notifyProperty(player, self, "role") - ret.p.general = self.general - ret.p.deputyGeneral = self.deputyGeneral - ret.p.role = self.role - end - return ret - end - - local properties = {} - - assign(properties, self, "general") - assign(properties, self, "deputyGeneral") - assign(properties, self, "maxHp") - assign(properties, self, "hp") - assign(properties, self, "shield") - assign(properties, self, "gender") - assign(properties, self, "kingdom") - - if self.dead then - assign(properties, self, "dead") - assign(properties, self, self.rest > 0 and "rest" or "role") - else - assign(properties, self, "seat") - assign(properties, self, "phase") - end - - if not self.faceup then - assign(properties, self, "faceup") - end - - if self.chained then - assign(properties, self, "chained") - end - - if self.role_shown then - assign(properties, self, "role") - end - - if #self.sealedSlots > 0 then - assign(properties, self, "sealedSlots") - end - +function ServerPlayer:toJsonObject() + local o = Player.toJsonObject(self) local sp = self._splayer - - return { - -- data for Setup/AddPlayer - d = { - self.id, - sp:getScreenName(), - sp:getAvatar(), - false, - sp:getTotalGameTime(), - }, - p = properties, - ch = self.cardUsedHistory, - sh = self.skillUsedHistory, - m = self.mark, - s = table.map(self.player_skills, Util.NameMapper), - c = self.player_cards, - sc = self.special_cards, + o.setup_data = { + self.id, + sp:getScreenName(), + sp:getAvatar(), + false, + sp:getTotalGameTime(), } + return o end +-- 似乎没有必要 +-- function ServerPlayer:loadJsonObject() end + function ServerPlayer:reconnect() local room = self.room self.serverplayer:setState(fk.Player_Online) - local summary = room:getSummary(self, false) + local summary = room:toJsonObject(self) self:doNotify("Reconnect", json.encode(summary)) room:notifyProperty(self, self, "role") self:doNotify("RoomOwner", json.encode{ room.room:getOwner():getId() }) @@ -331,6 +152,7 @@ function ServerPlayer:turnOver() self.room.logic:trigger(fk.TurnedOver, self) end +---@param cards integer|integer[]|Card|Card[] function ServerPlayer:showCards(cards) cards = Card:getIdList(cards) for _, id in ipairs(cards) do @@ -355,12 +177,7 @@ 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 @@ -427,7 +244,7 @@ function ServerPlayer:gainAnExtraPhase(phase, delay) room:sendLog{ type = "#GainAnExtraPhase", from = self.id, - arg = phase_name_table[phase], + arg = Util.PhaseStrMapper(phase), } GameEvent.Phase:create(self, self.phase):exec() @@ -441,7 +258,7 @@ function ServerPlayer:gainAnExtraPhase(phase, delay) room:sendLog{ type = "#PhaseSkipped", from = self.id, - arg = phase_name_table[phase], + arg = Util.PhaseStrMapper(phase), } end @@ -479,7 +296,7 @@ function ServerPlayer:play(phase_table) end for i = 1, #phases do - if self.dead then + if self.dead or room:getTag("endTurn") or phases[i] == nil then self:changePhase(self.phase, Player.NotActive) break end @@ -514,7 +331,7 @@ function ServerPlayer:play(phase_table) room:sendLog{ type = "#PhaseSkipped", from = self.id, - arg = phase_name_table[self.phase], + arg = Util.PhaseStrMapper(self.phase), } end end @@ -540,10 +357,17 @@ end --- 当进行到出牌阶段空闲点时,结束出牌阶段。 function ServerPlayer:endPlayPhase() - self._play_phase_end = true + if self.phase == Player.Play then + self._phase_end = true + end -- TODO: send log end +--- 结束当前阶段。 +function ServerPlayer:endCurrentPhase() + self._phase_end = true +end + --- 获得一个额外回合 ---@param delay? boolean ---@param skillName? string @@ -1014,7 +838,7 @@ function ServerPlayer:addBuddy(other) other = self.room:getPlayerById(other) end Player.addBuddy(self, other) - self:doNotify("AddBuddy", json.encode{ other.id, other.player_cards[Player.Hand] }) + self.room:doBroadcastNotify("AddBuddy", json.encode{ self.id, other.id }) end function ServerPlayer:removeBuddy(other) @@ -1022,7 +846,7 @@ function ServerPlayer:removeBuddy(other) other = self.room:getPlayerById(other) end Player.removeBuddy(self, other) - self:doNotify("RmBuddy", tostring(other.id)) + self.room:doBroadcastNotify("RmBuddy", json.encode{ self.id, other.id }) end return ServerPlayer diff --git a/lua/server/system_enum.lua b/lua/server/system_enum.lua index 79743909..cb9d3bc3 100644 --- a/lua/server/system_enum.lua +++ b/lua/server/system_enum.lua @@ -82,6 +82,7 @@ fk.IceDamage = 4 ---@field public skillName? string @ 造成本次伤害的技能名 ---@field public beginnerOfTheDamage? boolean @ 是否是本次铁索传导的起点 ---@field public by_user? boolean @ 是否由卡牌直接生效造成的伤害 +---@field public chain_table? ServerPlayer[] @ 铁索连环表 --- RecoverStruct 描述和回复体力有关的数据。 ---@class RecoverStruct diff --git a/lua/ui_emu/base.lua b/lua/ui_emu/base.lua new file mode 100644 index 00000000..47fe7c31 --- /dev/null +++ b/lua/ui_emu/base.lua @@ -0,0 +1,152 @@ +-- 模拟一套UI操作,并在具体子类中实现相应操作逻辑。分为UI组件和UI场景两种类。 +-- 在客户端与Qml直接同步,在服务端中用于AI。 + +-- 模拟UI组件。最基本的属性为enabled,表示是否可以进行交互。 +-- 注意在编写逻辑时不要直接修改Item的属性。用scene:update。 +---@class Item: Object +---@field public parent Scene +---@field public enabled boolean +---@field public id string | integer +local Item = class("Item") + +---@parant scene Scene +function Item:initialize(scene, id) + self.parent = scene + self.enabled = false + self.id = id +end + +function Item:toData() + return { + enabled = self.enabled, + id = self.id, + } +end + +---@return boolean 是否发生改变 +function Item:setData(newData) + local changed + for k, v in pairs(newData) do + changed = changed or (self[k] ~= v) + self[k] = v + end + return changed +end + +---@class SelectableItem: Item +---@field public selected boolean +local SelectableItem = Item:subclass("SelectableItem") + +---@parant scene Scene +function SelectableItem:initialize(scene, id) + Item.initialize(self, scene, id) + self.selected = false +end + +function SelectableItem:toData() + local ret = Item.toData(self) + ret.selected = self.selected + return ret +end + +-- 最基本的“交互”,对应到UI中就是一次点击。 +-- 在派生类中视情况可能要为其传入参数表示修改后的值。 +function Item:interact() end + +--[[ + 模拟UI场景。用途是容纳所有模拟UI组件,并与实际的UI进行信息交换。 + + 在实际针对Scene与Handler进行开发时,Scene只需要创建Item并管理就行了, + 与逻辑相关的代码都在RequestHandler及其子类中编写,然而直接负责管理各个 + UI组件的是Scene。以下是注意事项: + + 1. 使用scene:update方法来更新Item的属性: + ------------------------------- + + [QML] cardItem.enabled = true; + [Lua] scene:update("CardItem", cid, { enabled = true }) + + 这样做是为了在后续操作中能成功的将此处作出的修改传达给QML。因为没有类似QML的 + 属性绑定机制,因此要另外调用update方法来记录相关的属性变动。 + + 2. 使用Scene提供的方法来访问元素 + --------------------------------- + + 例如RoomScene中已经创建了表达卡牌和技能的Item,因此在Handler的逻辑编写中, + 应当避免再去使用getCards或者getSkills这样获取原始属性的函数,而是直接访问Item + 例如: +--]] +---@class Scene: Object +---@field public parent RequestHandler +---@field public scene_name string +---@field public items { [string]: { [string|integer]: Item } } +local Scene = class("Scene") + +function Scene:initialize(parent) + self.parent = parent + self.items = {} +end + +---@param item Item +function Scene:addItem(item, ui_data) + local key = item.class.name + self.items[key] = self.items[key] or {} + self.items[key][item.id] = item + local changeData = self.parent.change + if changeData then + local k = "_new" + changeData[k] = changeData[k] or {} + table.insert(changeData[k], { + type = key, + data = item:toData(), + ui_data = ui_data, + }) + end +end + +function Scene:removeItem(elemType, id, ui_data) + local tab = self.items[elemType] + if type(tab) ~= "table" then return end + if not (tab and tab[id]) then return end + tab[id] = nil + local changeData = self.parent.change + if changeData then + local k = "_delete" + changeData[k] = changeData[k] or {} + table.insert(changeData[k], { + type = elemType, + id = id, + ui_data = ui_data, + }) + end +end + +function Scene:getAllItems(elemType) + return self.items[elemType] or Util.DummyTable +end + +-- 模拟一次UI交互,修改相关item的属性即可 +-- 同时修改自己parent的changeData +function Scene:update(elemType, id, newData) + local item = self.items[elemType][id] + local changed = item:setData(newData) + local changeData = self.parent.change + if changed and changeData then + changeData[elemType] = changeData[elemType] or {} + table.insert(changeData[elemType], item:toData()) + end +end + +-- 一般由RequestHandler或者其他上层部分调用 +-- 调用者需要维护changeData,确保传给UI的数据最少 +function Scene:notifyUI() + if not ClientInstance then return nil end + self.parent.change["_type"] = self.scene_name + ClientInstance:notifyUI("UpdateRequestUI", self.parent.change) +end + +return { + Item = Item, + SelectableItem = SelectableItem, + Scene = Scene, +} diff --git a/lua/ui_emu/choosecardbox.lua b/lua/ui_emu/choosecardbox.lua new file mode 100644 index 00000000..e88e0b25 --- /dev/null +++ b/lua/ui_emu/choosecardbox.lua @@ -0,0 +1,34 @@ +local PopupBox = require 'ui_emu.popupbox' +local common = require 'ui_emu.common' +local CardItem = common.CardItem +-- 过河拆桥、顺手牵羊使用的选卡包 + +---@class ChooseCardBox: PopupBox +local ChooseCardBox = PopupBox:subclass("ChooseCardBox") + +function ChooseCardBox:initialize(player) + self.room = Fk:currentRoom() + self.player = player +end + +-- 打开qml框后的初始化,对应request打开qml框的操作 +---@param data any @ 数据 +function ChooseCardBox:setup(data) + for _, cid in ipairs(data.cards) do + self:addItem(CardItem:new(self, cid)) + end +end + +-- 父场景将UI应有的变化传至此处 +-- 需要实现各种合法性检验,决定需要变更状态的UI,并最终将变更反馈给真实的界面 +---@param elemType string @ 元素类型 +---@param id any @ 元素ID +---@param action any @ 动作 +---@param data any @ 数据 +---@return { [string]: Item[] } +function ChooseCardBox:update(elemType, id, action, data) + -- 返回自己的变化 + return self.change +end + +return PopupBox diff --git a/lua/ui_emu/common.lua b/lua/ui_emu/common.lua new file mode 100644 index 00000000..0e569663 --- /dev/null +++ b/lua/ui_emu/common.lua @@ -0,0 +1,29 @@ +local base = require 'ui_emu.base' +local SelectableItem = base.SelectableItem + +---@class CardItem: SelectableItem +local CardItem = SelectableItem:subclass("CardItem") + +---@class Photo: SelectableItem +---@field public state string +local Photo = SelectableItem:subclass("Photo") + +function Photo:initialize(scene, id) + SelectableItem.initialize(self, scene, id) + self.state = "normal" +end + +function Photo:toData() + local ret = SelectableItem.toData(self) + ret.state = self.state + return ret +end + +---@class SkillButton: SelectableItem +local SkillButton = SelectableItem:subclass("SkillButton") + +return { + CardItem = CardItem, + Photo = Photo, + SkillButton = SkillButton, +} diff --git a/lua/ui_emu/control.lua b/lua/ui_emu/control.lua new file mode 100644 index 00000000..d55ef47e --- /dev/null +++ b/lua/ui_emu/control.lua @@ -0,0 +1,11 @@ +-- 对应QtQuick.Controls里面的组件 或者相对应的 +-- 以后可能还有更多需要模拟的组件吧 +local base = require 'ui_emu.base' +local Item = base.Item + +---@class Button: SelectableItem +local Button = Item:subclass("Button") + +return { + Button = Button, +} diff --git a/lua/ui_emu/interaction.lua b/lua/ui_emu/interaction.lua new file mode 100644 index 00000000..1e4b51ad --- /dev/null +++ b/lua/ui_emu/interaction.lua @@ -0,0 +1,23 @@ +local Item = (require 'ui_emu.base').Item + +-- 用来表明interaction的Item,格式大约可比照ui-util.lua中的表定义 +---@class Interaction: Item +---@field public spec any 弹出的东西 +---@field public skill_name string 技能名 +---@field public data any skill.interaction.data +local Interaction = Item:subclass("Interaction") + +function Interaction:initialize(scene, id, spec) + Item.initialize(self, scene, id) + self.spec = spec + self.enabled = true +end + +function Interaction:toData() + local ret = Item.toData(self) + ret.spec = self.spec + ret.skill_name = self.skill_name + return ret +end + +return Interaction diff --git a/lua/ui_emu/okscene.lua b/lua/ui_emu/okscene.lua new file mode 100644 index 00000000..093c0059 --- /dev/null +++ b/lua/ui_emu/okscene.lua @@ -0,0 +1,18 @@ +local base = require 'ui_emu.base' +local control = require 'ui_emu.control' +local Scene = base.Scene +local Button = control.Button + +---@class OKScene: Scene +local OKScene = Scene:subclass("OKScene") +OKScene.scene_name = "Room" + +---@param parent RequestHandler +function OKScene:initialize(parent) + Scene.initialize(self, parent) + + self:addItem(Button:new(self, "OK")) + self:addItem(Button:new(self, "Cancel")) +end + +return OKScene diff --git a/lua/ui_emu/popupbox.lua b/lua/ui_emu/popupbox.lua new file mode 100644 index 00000000..79993ba1 --- /dev/null +++ b/lua/ui_emu/popupbox.lua @@ -0,0 +1,42 @@ +local base = require 'ui_emu.base' +local control = require 'ui_emu.control' +local Scene = base.Scene + +-- 一种模拟具体qml框的处理机构 +-- 具体是什么东西全靠继承子类处理 + +-- 理论上来说,这就是一个小scene +-- 会向其父场景传输其应有的变化 +-- 同理,UI改变也由父场景传输至这里 + +---@class PopupBox: Scene +local PopupBox = Scene:subclass("PopupBox") + +-- 打开qml框后的初始化,对应request打开qml框的操作 +function PopupBox:initialize(parent, data) + Scene.initialize(self, parent) + self.data = data + self.change = {} +end + +-- 模拟一次UI交互,修改相关item的属性即可 +-- 同时修改自己parent的changeData +function PopupBox:update(elemType, id, newData) + local item = self.items[elemType][id] + local changed = item:setData(newData) + local changeData = self.change + if changed and changeData then + changeData[elemType] = changeData[elemType] or {} + table.insert(changeData[elemType], item:toData()) + end +end + +-- 由父RequestHandler调用,用以将本qml变化传至父RequestHandler +-- 调用者需要维护changeData,确保传给UI的数据最少 +function PopupBox:notifyUI() + if not ClientInstance then return nil end + self.parent.change["_type"] = self.class.name + ClientInstance:notifyUI("UpdateRequestUI", self.parent.change) +end + +return PopupBox diff --git a/lua/ui_emu/poxi_box.lua b/lua/ui_emu/poxi_box.lua new file mode 100644 index 00000000..e69de29b diff --git a/lua/ui_emu/roomscene.lua b/lua/ui_emu/roomscene.lua new file mode 100644 index 00000000..e3f05068 --- /dev/null +++ b/lua/ui_emu/roomscene.lua @@ -0,0 +1,86 @@ +local common = require 'ui_emu.common' +local OKScene = require 'ui_emu.okscene' +local CardItem = common.CardItem +local Photo = common.Photo +local SkillButton = common.SkillButton + +---@class RoomScene: OKScene +local RoomScene = OKScene:subclass("RoomScene") +RoomScene.scene_name = "Room" + +---@param parent RequestHandler +function RoomScene:initialize(parent) + OKScene.initialize(self, parent) + local player = parent.player + + for _, p in ipairs(parent.room.alive_players) do + self:addItem(Photo:new(self, p.id)) + end + for _, cid in ipairs(player:getCardIds("h")) do + self:addItem(CardItem:new(self, cid)) + end + for _, skill in ipairs(player:getAllSkills()) do + if skill:isInstanceOf(ActiveSkill) or skill:isInstanceOf(ViewAsSkill) then + self:addItem(SkillButton:new(self, skill.name)) + end + end +end + +function RoomScene:unselectOtherCards(cid) + local dat = { selected = false } + for id, _ in pairs(self:getAllItems("CardItem")) do + if id ~= cid then + self:update("CardItem", id, dat) + end + end +end +function RoomScene:unselectOtherTargets(pid) + local dat = { selected = false } + for id, _ in pairs(self:getAllItems("Photo")) do + if id ~= pid then + self:update("Photo", id, dat) + end + end +end +RoomScene.unselectAllCards = RoomScene.unselectOtherCards +RoomScene.unselectAllTargets = RoomScene.unselectOtherTargets + +-- 若所有角色都不可选则将state设为normal; 反之只要有可选的就设candidate +-- 这样更美观 +function RoomScene:updateTargetEnability(pid, enabled) + local photoTab = self.items["Photo"] + local photo = photoTab[pid] + self:update("Photo", pid, { enabled = not not enabled }) + if enabled then + if photo.state == "normal" then + for id, _ in pairs(photoTab) do + self:update("Photo", id, { state = "candidate" }) + end + end + else + local allDisabled = true + for _, v in pairs(photoTab) do + if v.enabled then + allDisabled = false + break + end + end + if allDisabled then + for id, _ in pairs(photoTab) do + self:update("Photo", id, { state = "normal" }) + end + end + end +end + +function RoomScene:disableAllTargets() + for id, _ in pairs(self.items["Photo"]) do + self:update("Photo", id, { + state = "normal", + selected = false, + enabled = false, + }) + end +end + +return RoomScene diff --git a/lua/ui_emu/specialskills.lua b/lua/ui_emu/specialskills.lua new file mode 100644 index 00000000..750dd189 --- /dev/null +++ b/lua/ui_emu/specialskills.lua @@ -0,0 +1,21 @@ +local Item = (require 'ui_emu.base').Item + +-- 用来表明SpecialSkills的Item,本质是一个单选框 +---@class SpecialSkills: Item +---@field public skills string[] 技能名 +---@field public data any orig_text +local SpecialSkills = Item:subclass("SpecialSkills") + +function SpecialSkills:initialize(scene, id) + Item.initialize(self, scene, id) + self.skills = {} + self.enabled = true +end + +function SpecialSkills:toData() + local ret = Item.toData(self) + ret.skills = self.skills + return ret +end + +return SpecialSkills diff --git a/packages/maneuvering/init.lua b/packages/maneuvering/init.lua index 7f4698f9..54ea7fd7 100644 --- a/packages/maneuvering/init.lua +++ b/packages/maneuvering/init.lua @@ -4,6 +4,9 @@ local extension = Package:new("maneuvering", Package.CardPack) local slash = Fk:cloneCard("slash") +Fk:addDamageNature(fk.FireDamage, "fire_damage") +Fk:addDamageNature(fk.ThunderDamage, "thunder_damage") + local thunderSlashSkill = fk.CreateActiveSkill{ name = "thunder__slash_skill", prompt = function(self, selected_cards) @@ -462,10 +465,21 @@ local silverLion = fk.CreateArmor{ } extension:addCard(silverLion) +local hualiuSkill = fk.CreateDistanceSkill{ + name = "#hualiu_skill", + attached_equip = "hualiu", + correct_func = function(self, from, to) + if to:hasSkill(self) then + return 1 + end + end, +} +Fk:addSkill(hualiuSkill) local huaLiu = fk.CreateDefensiveRide{ name = "hualiu", suit = Card.Diamond, number = 13, + equip_skill = hualiuSkill, } extension:addCards({ @@ -549,6 +563,8 @@ Fk:loadTranslationTable{ [":hualiu"] = "装备牌·坐骑
坐骑技能:其他角色与你的距离+1。", } -Fk:loadTranslationTable(require 'packages/maneuvering/i18n/en_US', 'en_US') +local pkgprefix = "packages/" +if UsingNewCore then pkgprefix = "packages/freekill-core/" end +Fk:loadTranslationTable(require(pkgprefix .. 'maneuvering/i18n/en_US'), 'en_US') return extension diff --git a/packages/standard/aux_skills.lua b/packages/standard/aux_skills.lua index 93d7a849..c57369eb 100644 --- a/packages/standard/aux_skills.lua +++ b/packages/standard/aux_skills.lua @@ -87,6 +87,13 @@ local choosePlayersSkill = fk.CreateActiveSkill{ return table.contains(self.targets, to_select) end end, + target_tip = function(self, to_select, selected, selected_cards, card, selectable, extra_data) + if self.targetTipName then + local targetTip = Fk.target_tips[self.targetTipName] + assert(targetTip) + return targetTip.target_tip(self, to_select, selected, selected_cards, card, selectable, extra_data) + end + end, card_num = function(self) return self.pattern ~= "" and 1 or 0 end, min_target_num = function(self) return self.min_num end, max_target_num = function(self) return self.num end, @@ -104,9 +111,6 @@ local exChooseSkill = fk.CreateActiveSkill{ local checkpoint = true local card = Fk:getCardById(to_select) - if not self.include_equip then - checkpoint = checkpoint and (Fk:currentRoom():getCardArea(to_select) ~= Player.Equip) - end if self.pattern and self.pattern ~= "" then checkpoint = checkpoint and (Exppattern:Parse(self.pattern):match(card)) @@ -119,6 +123,13 @@ local exChooseSkill = fk.CreateActiveSkill{ return table.contains(self.targets, to_select) end end, + target_tip = function(self, to_select, selected, selected_cards, card, selectable, extra_data) + if self.targetTipName then + local targetTip = Fk.target_tips[self.targetTipName] + assert(targetTip) + return targetTip.target_tip(self, to_select, selected, selected_cards, card, selectable, extra_data) + end + end, min_target_num = function(self) return self.min_t_num end, max_target_num = function(self) return self.max_t_num end, min_card_num = function(self) return self.min_c_num end, diff --git a/packages/standard/i18n/en_US.lua b/packages/standard/i18n/en_US.lua index 514843d9..64121e6f 100644 --- a/packages/standard/i18n/en_US.lua +++ b/packages/standard/i18n/en_US.lua @@ -152,8 +152,8 @@ Fk:loadTranslationTable({ ["biyue"] = "Envious by Moon", [":biyue"] = "In your Finish Phase, you can draw 1 card.", - ["fastchat_m"] = "快捷短语", - ["fastchat_f"] = "快捷短语", + ["fastchat_m"] = "quick chats", + ["fastchat_f"] = "quick chats", ["$fastchat_m1"] = "能不能快一点啊,兵贵神速啊。", ["$fastchat_m2"] = "主公,别开枪,自己人!", @@ -171,6 +171,13 @@ Fk:loadTranslationTable({ ["$fastchat_m14"] = "哥们,给力点行吗?", ["$fastchat_m15"] = "哥哥,交个朋友吧。", ["$fastchat_m16"] = "妹子,交个朋友吧。", + ["$fastchat_m17"] = "我从未见过如此厚颜无耻之人!", + ["$fastchat_m18"] = "你随便杀,闪不了算我输。", + ["$fastchat_m19"] = "这波,不亏。", + ["$fastchat_m20"] = "请收下我的膝盖。", + ["$fastchat_m21"] = "你咋不上天呢?", + ["$fastchat_m22"] = "放开我的队友,冲我来。", + ["$fastchat_m23"] = "见证奇迹的时刻到了。", ["$fastchat_f1"] = "能不能快一点啊,兵贵神速啊。", ["$fastchat_f2"] = "主公,别开枪,自己人!", ["$fastchat_f3"] = "小内再不跳,后面还怎么玩啊?", @@ -187,6 +194,13 @@ Fk:loadTranslationTable({ ["$fastchat_f14"] = "哥们,给力点行吗?", ["$fastchat_f15"] = "哥,交个朋友吧。", ["$fastchat_f16"] = "妹子,交个朋友吧。", + ["$fastchat_f17"] = "我从未见过如此厚颜无耻之人!", + ["$fastchat_f18"] = "你随便杀,闪不了算我输。", + ["$fastchat_f19"] = "这波,不亏。", + ["$fastchat_f20"] = "请收下我的膝盖。", + ["$fastchat_f21"] = "你咋不上天呢?", + ["$fastchat_f22"] = "放开我的队友,冲我来。", + ["$fastchat_f23"] = "见证奇迹的时刻到了。", ["aaa_role_mode"] = "Role mode", [":aaa_role_mode"] = [[ diff --git a/packages/standard/i18n/init.lua b/packages/standard/i18n/init.lua index 35299a1c..acddf6d1 100644 --- a/packages/standard/i18n/init.lua +++ b/packages/standard/i18n/init.lua @@ -1,4 +1,6 @@ -- SPDX-License-Identifier: GPL-3.0-or-later -dofile "packages/standard/i18n/zh_CN.lua" -dofile "packages/standard/i18n/en_US.lua" +local pkgprefix = "packages/" +if UsingNewCore then pkgprefix = "packages/freekill-core/" end +dofile(pkgprefix .. "standard/i18n/zh_CN.lua") +dofile(pkgprefix .. "standard/i18n/en_US.lua") diff --git a/packages/standard/i18n/zh_CN.lua b/packages/standard/i18n/zh_CN.lua index 708c4e14..eab672cd 100644 --- a/packages/standard/i18n/zh_CN.lua +++ b/packages/standard/i18n/zh_CN.lua @@ -359,9 +359,7 @@ Fk:loadTranslationTable{ ___ -(原文地址: https://sgs.52pk.com/zl/201205/5299813.shtml ) - -你即将开始学习一款集角色扮演、战斗、伪装等要素于一体的多人卡牌游戏。它能让你通过扮演耳熟能详的三国角色,在颠覆性的历史舞台中,演义一段扑朔迷离并充满刺激的较量。你将会充分体验到与玩家博弈的乐趣,它将是你聚会休闲的最佳伙伴,它就是――三国杀。 +你即将开始学习一款集角色扮演、战斗、伪装等要素于一体的多人卡牌游戏。它能让你通过扮演耳熟能详的三国角色,在颠覆性的历史舞台中,演义一段扑朔迷离并充满刺激的较量。你将会充分体验到与玩家博弈的乐趣,它将是你聚会休闲的最佳伙伴,它就是——三国杀。 ___ @@ -518,6 +516,8 @@ ___ 2、所有的反贼和内奸都已死亡:主公和忠臣(不论死活)都获胜。 +(原文地址: https://sgs.52pk.com/zl/201205/5299813.shtml ) + ]========================================], } diff --git a/packages/standard/init.lua b/packages/standard/init.lua index 8daa6022..c9b4bd96 100644 --- a/packages/standard/init.lua +++ b/packages/standard/init.lua @@ -2,9 +2,12 @@ local extension = Package:new("standard") extension.metadata = require "packages.standard.metadata" -dofile "packages/standard/game_rule.lua" -dofile "packages/standard/aux_skills.lua" -dofile "packages/standard/aux_poxi.lua" + +local pkgprefix = "packages/" +if UsingNewCore then pkgprefix = "packages/freekill-core/" end +dofile(pkgprefix .. "standard/game_rule.lua") +dofile(pkgprefix .. "standard/aux_skills.lua") +dofile(pkgprefix .. "standard/aux_poxi.lua") Fk:appendKingdomMap("god", {"wei", "shu", "wu", "qun"}) @@ -162,15 +165,15 @@ local tuxi = fk.CreateTriggerSkill{ local result = room:askForChoosePlayers(player, targets, 1, 2, "#tuxi-ask", self.name) if #result > 0 then - self.cost_data = result + room:sortPlayersByAction(result) + self.cost_data = {tos = result} return true end end, on_use = function(self, event, target, player, data) local room = player.room - room:sortPlayersByAction(self.cost_data) - for _, id in ipairs(self.cost_data) do - if player.dead then return end + for _, id in ipairs(self.cost_data.tos) do + if player.dead then break end local p = room:getPlayerById(id) if not p.dead and not p:isKongcheng() then local c = room:askForCardChosen(player, p, "h", self.name) @@ -1368,6 +1371,6 @@ Fk:loadTranslationTable{ } -- load translations of this package -dofile "packages/standard/i18n/init.lua" +dofile(pkgprefix .. "standard/i18n/init.lua") return extension diff --git a/packages/standard_cards/i18n/init.lua b/packages/standard_cards/i18n/init.lua index d0d0cfa3..03d5168f 100644 --- a/packages/standard_cards/i18n/init.lua +++ b/packages/standard_cards/i18n/init.lua @@ -1,4 +1,6 @@ -- SPDX-License-Identifier: GPL-3.0-or-later -dofile "packages/standard_cards/i18n/zh_CN.lua" -dofile "packages/standard_cards/i18n/en_US.lua" +local pkgprefix = "packages/" +if UsingNewCore then pkgprefix = "packages/freekill-core/" end +dofile(pkgprefix .. "standard_cards/i18n/zh_CN.lua") +dofile(pkgprefix .. "standard_cards/i18n/en_US.lua") diff --git a/packages/standard_cards/init.lua b/packages/standard_cards/init.lua index ef594209..89713f09 100644 --- a/packages/standard_cards/init.lua +++ b/packages/standard_cards/init.lua @@ -305,7 +305,7 @@ local duelSkill = fk.CreateActiveSkill{ local cardResponded for i = 1, loopTimes do - cardResponded = room:askForResponse(currentResponser, 'slash', nil, nil, false, nil, effect) + cardResponded = room:askForResponse(currentResponser, 'slash', nil, nil, true, nil, effect) if cardResponded then room:responseCard({ from = currentResponser.id, @@ -495,7 +495,7 @@ local savageAssaultSkill = fk.CreateActiveSkill{ return user ~= to_select end, on_effect = function(self, room, effect) - local cardResponded = room:askForResponse(room:getPlayerById(effect.to), 'slash', nil, nil, false, nil, effect) + local cardResponded = room:askForResponse(room:getPlayerById(effect.to), 'slash', nil, nil, true, nil, effect) if cardResponded then room:responseCard({ @@ -539,7 +539,7 @@ local archeryAttackSkill = fk.CreateActiveSkill{ return user ~= to_select end, on_effect = function(self, room, effect) - local cardResponded = room:askForResponse(room:getPlayerById(effect.to), 'jink', nil, nil, false, nil, effect) + local cardResponded = room:askForResponse(room:getPlayerById(effect.to), 'jink', nil, nil, true, nil, effect) if cardResponded then room:responseCard({ @@ -631,6 +631,7 @@ local amazingGraceSkill = fk.CreateActiveSkill{ use.extra_data = use.extra_data or {} use.extra_data.AGFilled = toDisplay + use.extra_data.AGResult = {} else if use.extra_data and use.extra_data.AGFilled then table.forEach(room.players, function(p) @@ -661,7 +662,8 @@ local amazingGraceSkill = fk.CreateActiveSkill{ local chosen = room:askForAG(to, effect.extra_data.AGFilled, false, self.name) room:takeAG(to, chosen, room.players) - room:obtainCard(effect.to, chosen, true, fk.ReasonPrey) + table.insert(effect.extra_data.AGResult, {effect.to, chosen}) + room:moveCardTo(chosen, Card.PlayerHand, effect.to, fk.ReasonPrey, self.name, nil, true, effect.to) table.removeOne(effect.extra_data.AGFilled, chosen) end } @@ -705,7 +707,8 @@ local lightningSkill = fk.CreateActiveSkill{ to = to, damage = 3, card = effect.card, - damageType = fk.ThunderDamage, + -- damageType = fk.ThunderDamage, + damageType = Fk:getDamageNature(fk.ThunderDamage) and fk.ThunderDamage or fk.NormalDamage, skillName = self.name, } @@ -1048,17 +1051,20 @@ local axeSkill = fk.CreateTriggerSkill{ attached_equip = "axe", events = {fk.CardEffectCancelledOut}, can_trigger = function(self, event, target, player, data) - return player:hasSkill(self) and data.from == player.id and data.card.trueName == "slash" and not player.room:getPlayerById(data.to).dead + return player:hasSkill(self) and data.from == player.id and data.card.trueName == "slash" and + not player.room:getPlayerById(data.to).dead end, on_cost = function(self, event, target, player, data) local room = player.room - local pattern - if player:getEquipment(Card.SubtypeWeapon) then - pattern = ".|.|.|.|.|.|^"..tostring(player:getEquipment(Card.SubtypeWeapon)) - else - pattern = "." + local cards = {} + for _, id in ipairs(player:getCardIds("he")) do + if not player:prohibitDiscard(id) and + not (table.contains(player:getEquipments(Card.SubtypeWeapon), id) and Fk:getCardById(id).name == "axe") then + table.insert(cards, id) + end end - local cards = room:askForDiscard(player, 2, 2, true, self.name, true, pattern, "#axe-invoke::"..data.to, true) + cards = room:askForDiscard(player, 2, 2, true, self.name, true, ".|.|.|.|.|.|"..table.concat(cards, ","), + "#axe-invoke::"..data.to, true) if #cards > 0 then self.cost_data = cards return true @@ -1248,84 +1254,134 @@ extension:addCards({ niohShield, }) -local horseSkill = fk.CreateDistanceSkill{ - name = "horse_skill", - global = true, +local diluSkill = fk.CreateDistanceSkill{ + name = "#dilu_skill", + attached_equip = "dilu", correct_func = function(self, from, to) - local ret = 0 - if from:getEquipment(Card.SubtypeOffensiveRide) then - ret = ret - 1 + if to:hasSkill(self) then + return 1 end - if to:getEquipment(Card.SubtypeDefensiveRide) then - ret = ret + 1 - end - return ret end, } -if not Fk.skills["horse_skill"] then - Fk:addSkill(horseSkill) -end - +Fk:addSkill(diluSkill) local diLu = fk.CreateDefensiveRide{ name = "dilu", suit = Card.Club, number = 5, + equip_skill = diluSkill, } extension:addCards({ diLu, }) +local jueyingSkill = fk.CreateDistanceSkill{ + name = "#jueying_skill", + attached_equip = "jueying", + correct_func = function(self, from, to) + if to:hasSkill(self) then + return 1 + end + end, +} +Fk:addSkill(jueyingSkill) local jueYing = fk.CreateDefensiveRide{ name = "jueying", suit = Card.Spade, number = 5, + equip_skill = jueyingSkill, } extension:addCards({ jueYing, }) +local zhuahuangfeidianSkill = fk.CreateDistanceSkill{ + name = "#zhuahuangfeidian_skill", + attached_equip = "zhuahuangfeidian", + correct_func = function(self, from, to) + if to:hasSkill(self) then + return 1 + end + end, +} +Fk:addSkill(zhuahuangfeidianSkill) local zhuaHuangFeiDian = fk.CreateDefensiveRide{ name = "zhuahuangfeidian", suit = Card.Heart, number = 13, + equip_skill = zhuahuangfeidianSkill, } extension:addCards({ zhuaHuangFeiDian, }) +local chituSkill = fk.CreateDistanceSkill{ + name = "#chitu_skill", + attached_equip = "chitu", + correct_func = function(self, from, to) + if from:hasSkill(self) then + return -1 + end + end, +} +Fk:addSkill(chituSkill) local chiTu = fk.CreateOffensiveRide{ name = "chitu", suit = Card.Heart, number = 5, + equip_skill = chituSkill, } extension:addCards({ chiTu, }) +local dayuanSkill = fk.CreateDistanceSkill{ + name = "#dayuan_skill", + attached_equip = "dayuan", + correct_func = function(self, from, to) + if from:hasSkill(self) then + return -1 + end + end, +} +Fk:addSkill(dayuanSkill) local daYuan = fk.CreateOffensiveRide{ name = "dayuan", suit = Card.Spade, number = 13, + equip_skill = dayuanSkill, } extension:addCards({ daYuan, }) +local zixingSkill = fk.CreateDistanceSkill{ + name = "#zixing_skill", + attached_equip = "zixing", + correct_func = function(self, from, to) + if from:hasSkill(self) then + return -1 + end + end, +} +Fk:addSkill(zixingSkill) local ziXing = fk.CreateOffensiveRide{ name = "zixing", suit = Card.Diamond, number = 13, + equip_skill = zixingSkill, } extension:addCards({ ziXing, }) -dofile "packages/standard_cards/i18n/init.lua" +local pkgprefix = "packages/" +if UsingNewCore then pkgprefix = "packages/freekill-core/" end +dofile(pkgprefix .. "standard_cards/i18n/init.lua") return extension diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a1361a5e..93b6158a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,5 +1,21 @@ # SPDX-License-Identifier: GPL-3.0-or-later +file(GLOB SWIG_FILES "${PROJECT_SOURCE_DIR}/src/swig/*.i") +if (DEFINED FK_SERVER_ONLY) + set(SWIG_SOURCE ${PROJECT_SOURCE_DIR}/src/swig/freekill-nogui.i) +else () + set(SWIG_SOURCE ${PROJECT_SOURCE_DIR}/src/swig/freekill.i) +endif () + +add_custom_command( + OUTPUT ${PROJECT_SOURCE_DIR}/src/swig/freekill-wrap.cxx + DEPENDS ${SWIG_FILES} + COMMENT "Generating freekill-wrap.cxx" + COMMAND swig -c++ -lua -Wall -o + ${PROJECT_SOURCE_DIR}/src/swig/freekill-wrap.cxx + ${SWIG_SOURCE} +) + set(freekill_SRCS # "main.cpp" "freekill.cpp" @@ -21,6 +37,8 @@ set(freekill_SRCS "ui/qmlbackend.cpp" "swig/freekill-wrap.cxx" ) +set_source_files_properties( + "swig/freekill-wrap.cxx" PROPERTIES GENERATED TRUE) if (NOT DEFINED FK_SERVER_ONLY) list(APPEND freekill_SRCS diff --git a/src/main.cpp b/src/main.cpp index 89525db4..56c10fac 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // 为了写测试而特意给程序本身单独分出一个main.cpp 顺便包含项目文档(这样真的好吗) -#include "freekill.h" +int freekill_main(int argc, char **argv); int main(int argc, char **argv) { return freekill_main(argc, argv); }