diff --git a/CHANGELOG.md b/CHANGELOG.md index 10189e81..9618d27c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # ChangeLog -## v0.4.21 & v0.4.22 +## v0.4.21 & v0.4.22 & v0.4.23 - 修复了确认键亮起时取消键不可用的bug - lua端的ob属性根本没同步,同步一下 diff --git a/CMakeLists.txt b/CMakeLists.txt index c4b8cda1..e2a7b9ca 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.22) -project(FreeKill VERSION 0.4.22) +project(FreeKill VERSION 0.4.23) add_definitions(-DFK_VERSION=\"${CMAKE_PROJECT_VERSION}\") find_package(Qt6 REQUIRED COMPONENTS diff --git a/Fk/Cheat/PlayerDetail.qml b/Fk/Cheat/PlayerDetail.qml index d58b2276..5f2eec23 100644 --- a/Fk/Cheat/PlayerDetail.qml +++ b/Fk/Cheat/PlayerDetail.qml @@ -153,6 +153,10 @@ Flickable { selectByMouse: false wrapMode: TextEdit.WordWrap textFormat: TextEdit.RichText + property var savedtext: [] + function clearSavedText() { + savedtext = []; + } onLinkActivated: (link) => { if (link === "back") { text = savedtext.pop(); @@ -180,6 +184,7 @@ Flickable { screenName.text = ""; playerGameData.text = ""; skillDesc.text = ""; + skillDesc.clearSavedText(); const id = extra_data.photo.playerid; if (id === 0) return; diff --git a/Fk/Pages/GeneralsOverview.qml b/Fk/Pages/GeneralsOverview.qml index 33c899e8..a89d472e 100644 --- a/Fk/Pages/GeneralsOverview.qml +++ b/Fk/Pages/GeneralsOverview.qml @@ -5,7 +5,7 @@ import QtQuick.Layouts import QtQuick.Controls import Fk import Fk.RoomElement -import "RoomLogic.js" as RoomLogic +// import "RoomLogic.js" as RoomLogic Item { id: root @@ -390,12 +390,28 @@ Item { } onClicked: { - callbacks["LogEvent"]({ - type: "PlaySkillSound", - name: name, - general: specific ? detailGeneralCard.name : null, // 分化特别和一般 - i: idx, - }); + const skill = name; + const general = specific ? detailGeneralCard.name : null; + let extension; + let path; + let dat; + + // try main general + if (general) { + dat = lcall("GetGeneralData", general); + extension = dat.extension; + path = "./packages/" + extension + "/audio/skill/" + skill + "_" + general; + if (Backend.exists(path + ".mp3") || Backend.exists(path + "1.mp3")) { + Backend.playSound(path, idx); + return; + } + } + + // finally normal skill + dat = lcall("GetSkillData", skill); + extension = dat.extension; + path = "./packages/" + extension + "/audio/skill/" + skill; + Backend.playSound(path, idx); } onPressAndHold: { diff --git a/Fk/Pages/Room.qml b/Fk/Pages/Room.qml index 13fec8cf..5649f957 100644 --- a/Fk/Pages/Room.qml +++ b/Fk/Pages/Room.qml @@ -341,6 +341,7 @@ Item { } lcall("FinishRequestUI"); + applyChange({}); } } }, @@ -1338,7 +1339,12 @@ Item { photo.state = pdata.state; photo.selectable = pdata.enabled; photo.selected = pdata.selected; - }) + }); + for (let i = 0; i < photoModel.count; i++) { + const item = photos.itemAt(i); + item.targetTip = lcall("GetTargetTip", item.playerid); + } + const buttons = uiUpdate["Button"]; if (buttons) { okCancel.visible = true; diff --git a/Fk/Pages/RoomLogic.js b/Fk/Pages/RoomLogic.js index af7198ad..842a7efc 100644 --- a/Fk/Pages/RoomLogic.js +++ b/Fk/Pages/RoomLogic.js @@ -768,26 +768,17 @@ callbacks["MoveFocus"] = (data) => { cancelAllFocus(); const focuses = data[0]; const command = data[1]; + const timeout = data[2] ?? (config.roomTimeout * 1000); let item, model; for (let i = 0; i < playerNum; i++) { model = photoModel.get(i); if (focuses.indexOf(model.id) != -1) { item = photos.itemAt(i); + item.progressBar.duration = timeout; item.progressBar.visible = true; item.progressTip = luatr(command) + luatr(" thinking..."); - - /* - if (command === "PlayCard") { - item.playing = true; - } - } else { - item = photos.itemAt(i); - if (command === "PlayCard") { - item.playing = false; - } - */ } } } @@ -1241,10 +1232,6 @@ callbacks["AskForResponseCard"] = (data) => { roomScene.okCancel.visible = true; } -callbacks["WaitForNullification"] = () => { - roomScene.state = "notactive"; -} - callbacks["SetPlayerMark"] = (data) => { const player = getPhoto(data[0]); const mark = data[1]; @@ -1469,6 +1456,10 @@ callbacks["UpdateMiniGame"] = (data) => { } } +callbacks["EmptyRequest"] = (data) => { + roomScene.activate(); +} + callbacks["UpdateLimitSkill"] = (data) => { const id = data[0]; const skill = data[1]; diff --git a/Fk/RoomElement/HandcardArea.qml b/Fk/RoomElement/HandcardArea.qml index 38ea1d46..b45022dd 100644 --- a/Fk/RoomElement/HandcardArea.qml +++ b/Fk/RoomElement/HandcardArea.qml @@ -162,6 +162,13 @@ Item { break; } } - }) + }); + for (let i = 0; i < cards.length; i++) { + const card = cards[i]; + if (!card.selectable) { + const reason = lcall("GetCardProhibitReason", card.cid); + card.prohibitReason = reason; + } + } } } diff --git a/Fk/RoomElement/Photo.qml b/Fk/RoomElement/Photo.qml index 60167edb..9ca57a74 100644 --- a/Fk/RoomElement/Photo.qml +++ b/Fk/RoomElement/Photo.qml @@ -627,13 +627,14 @@ Item { anchors.bottomMargin: -4 from: 0.0 to: 100.0 + property int duration: config.roomTimeout * 1000 visible: false NumberAnimation on value { running: progressBar.visible from: 100.0 to: 0.0 - duration: config.roomTimeout * 1000 + duration: progressBar.duration onFinished: { progressBar.visible = false; @@ -665,6 +666,59 @@ Item { visible: root.state === "candidate" && selectable } + RowLayout { + anchors.centerIn: parent + spacing: 5 + + Repeater { + model: root.targetTip + + Item { + // Layout.alignment: Qt.AlignHCenter + width: modelData.type === "normal" ? 40 : 24 + + GlowText { + anchors.centerIn: parent + visible: modelData.type === "normal" + text: Util.processPrompt(modelData.content) + font.family: fontLi2.name + color: "#FEFE84" + font.pixelSize: { + if (text.length <= 3) return 36; + else return 28; + } + //font.bold: true + glow.color: "black" + glow.spread: 0.3 + glow.radius: 5 + lineHeight: 0.85 + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WrapAnywhere + width: font.pixelSize + 4 + } + + Text { + anchors.centerIn: parent + visible: modelData.type === "warning" + font.family: fontLibian.name + font.pixelSize: 24 + opacity: 0.9 + horizontalAlignment: Text.AlignHCenter + lineHeight: 24 + lineHeightMode: Text.FixedHeight + //color: "#EAC28A" + color: "snow" + width: 24 + wrapMode: Text.WrapAnywhere + style: Text.Outline + //styleColor: "#83231F" + styleColor: "red" + text: Util.processPrompt(modelData.content) + } + } + } + } + InvisibleCardArea { id: handcardAreaItem anchors.centerIn: parent @@ -739,53 +793,6 @@ Item { } } - RowLayout { - anchors.centerIn: parent - spacing: 5 - - Repeater { - model: root.targetTip - - Item { - Layout.alignment: Qt.AlignHCenter - width: 30 - - GlowText { - anchors.centerIn: parent - visible: modelData.type === "normal" - text: Util.processPrompt(modelData.content) - font.family: fontLibian.name - color: "#F7F589" - font.pixelSize: 30 - font.bold: true - glow.color: "black" - glow.spread: 0.3 - glow.radius: 5 - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WrapAnywhere - width: 30 - } - - Text { - anchors.centerIn: parent - visible: modelData.type === "warning" - font.family: fontLibian.name - font.pixelSize: 24 - opacity: 0.9 - horizontalAlignment: Text.AlignHCenter - lineHeight: 24 - lineHeightMode: Text.FixedHeight - color: "#EAC28A" - width: 24 - wrapMode: Text.WrapAnywhere - style: Text.Outline - styleColor: "#83231F" - text: Util.processPrompt(modelData.content) - } - } - } - } - Rectangle { color: "#CC2E2C27" radius: 6 diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 8fc3ff70..73d53069 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -3,8 +3,8 @@ + android:versionCode="423" + android:versionName="0.4.23"> diff --git a/lua/client/client.lua b/lua/client/client.lua index 97cf1284..3ebb00c1 100644 --- a/lua/client/client.lua +++ b/lua/client/client.lua @@ -213,18 +213,21 @@ fk.client_callback["SetCardFootnote"] = function(data) ClientInstance:setCardNote(data[1], data[2]); end -local function setup(id, name, avatar) +local function setup(id, name, avatar, msec) local self = fk.Self self:setId(id) self:setScreenName(name) self:setAvatar(avatar) Self = ClientPlayer:new(fk.Self) + if msec then + fk.ClientInstance:setupServerLag(msec) + end end fk.client_callback["Setup"] = function(data) -- jsonData: [ int id, string screenName, string avatar ] - local id, name, avatar = data[1], data[2], data[3] - setup(id, name, avatar) + local id, name, avatar, msec = data[1], data[2], data[3], data[4] + setup(id, name, avatar, msec) end fk.client_callback["EnterRoom"] = function(_data) @@ -243,7 +246,7 @@ fk.client_callback["EnterRoom"] = function(_data) local data = _data[3] ClientInstance.enter_room_data = json.encode(_data); - ClientInstance.room_settings = data + ClientInstance.settings = data table.insertTableIfNeed( data.disabledPack, Fk.game_mode_disabled[data.gameMode] @@ -857,13 +860,9 @@ fk.client_callback["AskForUseActiveSkill"] = function(data) local skill = Fk.skills[data[1]] local extra_data = data[4] 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] + + local h = Fk.request_handlers["AskForUseActiveSkill"]:new(Self, data) h.change = {} h:setup() h.scene:notifyUI() @@ -873,12 +872,7 @@ 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] + local h = Fk.request_handlers["AskForUseCard"]:new(Self, data) h.change = {} h:setup() h.scene:notifyUI() @@ -888,12 +882,7 @@ 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] + local h = Fk.request_handlers["AskForResponseCard"]:new(Self, data) h.change = {} h:setup() h.scene:notifyUI() @@ -1100,7 +1089,7 @@ fk.client_callback["GameOver"] = function(jsonData) c.record[2] = table.concat({ c.record[2], Self.player:getScreenName(), - c.room_settings.gameMode, + c.settings.gameMode, Self.general, Self.role, jsonData, @@ -1118,7 +1107,7 @@ fk.client_callback["EnterLobby"] = function(jsonData) c.record[2] = table.concat({ c.record[2], Self.player:getScreenName(), - c.room_settings.gameMode, + c.settings.gameMode, Self.general, Self.role, "", diff --git a/lua/client/client_util.lua b/lua/client/client_util.lua index 9431a357..cab293f8 100644 --- a/lua/client/client_util.lua +++ b/lua/client/client_util.lua @@ -335,49 +335,6 @@ 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 @@ -457,10 +414,25 @@ function GetSkillData(skill_name) end function GetSkillStatus(skill_name) + local player = Self 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 + local locked = not skill:isEffectable(player) + if not locked then + for mark, value in pairs(player.mark) do + if mark == MarkEnum.InvalidSkills then + if table.contains(value, skill_name) then + locked = true + break + end + elseif mark:startsWith(MarkEnum.InvalidSkills .. "-") and table.contains(value, skill_name) then + for _, suffix in ipairs(MarkEnum.TempMarkSuffix) do + if mark:find(suffix, 1, true) then + locked = true + break + end + end + end + end end return { locked = locked, ---@type boolean @@ -543,46 +515,6 @@ 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 @@ -746,7 +678,7 @@ end function ResetClientLua() local _data = ClientInstance.enter_room_data; - local data = ClientInstance.room_settings + local data = ClientInstance.settings Self = ClientPlayer:new(fk.Self) ClientInstance = Client:new() -- clear old client data ClientInstance.players = {Self} @@ -754,7 +686,7 @@ function ResetClientLua() ClientInstance.discard_pile = {} ClientInstance.enter_room_data = _data; - ClientInstance.room_settings = data + ClientInstance.settings = data ClientInstance.disabled_packs = data.disabledPack ClientInstance.disabled_generals = data.disabledGenerals @@ -766,7 +698,7 @@ function ResetAddPlayer(j) end function GetRoomConfig() - return ClientInstance.room_settings + return ClientInstance.settings end function GetPlayerGameData(pid) @@ -813,7 +745,7 @@ function SetReplayingShowCards(o) end function CheckSurrenderAvailable(playedTime) - local curMode = ClientInstance.room_settings.gameMode + local curMode = ClientInstance.settings.gameMode return Fk.game_modes[curMode]:surrenderFunc(playedTime) end @@ -822,17 +754,38 @@ function SaveRecord() c.client:saveRecord(json.encode(c.record), c.record[2]) end -function GetCardProhibitReason(cid, method, pattern) +function GetCardProhibitReason(cid) local card = Fk:getCardById(cid) if not card then return "" end + local handler = ClientInstance.current_request_handler + if (not handler) or (not handler:isInstanceOf(Fk.request_handlers["AskForUseActiveSkill"])) then return "" end + local method, pattern = "", handler.pattern or "." + + if handler.class.name == "ReqPlayCard" then method = "play" + elseif handler.class.name == "ReqResponseCard" then method = "response" + elseif handler.class.name == "ReqUseCard" then method = "use" + elseif handler.skill_name == "discard_skill" then method = "discard" + end + if method == "play" and not card.skill:canUse(Self, card) then return "" end if method ~= "play" and not card:matchPattern(pattern) then return "" end if method == "play" then method = "use" end + local fn_table = { + use = "prohibitUse", + response = "prohibitResponse", + discard = "prohibitDiscard", + } + local str_table = { + use = "method_use", + response = "method_response_play", + discard = "method_discard", + } + local status_skills = Fk:currentRoom().status_skills[ProhibitSkill] or Util.DummyTable local s for _, skill in ipairs(status_skills) do - local fn = method == "use" and skill.prohibitUse or skill.prohibitResponse + local fn = skill[fn_table[method]] if fn(skill, Self, card) then s = skill break @@ -844,14 +797,70 @@ function GetCardProhibitReason(cid, method, pattern) local skillName = s.name local ret = Fk:translate(skillName) if ret ~= skillName then - return ret .. Fk:translate("prohibit") .. Fk:translate(method == "use" and "method_use" or "method_response_play") + return ret .. Fk:translate("prohibit") .. Fk:translate(str_table[method]) elseif skillName:endsWith("_prohibit") and skillName:startsWith("#") then - return Fk:translate(skillName:sub(2, -10)) .. Fk:translate("prohibit") .. Fk:translate(method == "use" and "method_use" or "method_response_play") + return Fk:translate(skillName:sub(2, -10)) .. Fk:translate("prohibit") .. Fk:translate(str_table[method]) else return ret end end +function GetTargetTip(pid) + local handler = ClientInstance.current_request_handler --[[@as ReqPlayCard ]] + if (not handler) or (not handler:isInstanceOf(Fk.request_handlers["AskForUseActiveSkill"])) then return "" end + + local to_select = pid + local selected = handler.selected_targets + local selected_cards = handler.pendings + local card = handler.selected_card --[[@as Card?]] + local skill = Fk.skills[handler.skill_name] + local photo = handler.scene.items["Photo"][pid] --[[@as Photo]] + local selectable = photo.enabled + local extra_data = handler.extra_data + + local ret = {} + + if skill then + if skill:isInstanceOf(ActiveSkill) then + local tip = skill:targetTip(to_select, selected, selected_cards, nil, selectable) + if type(tip) == "string" then + table.insert(ret, { content = tip, type = "normal" }) + elseif type(tip) == "table" then + table.insertTable(ret, tip) + end + elseif skill:isInstanceOf(ViewAsSkill) then + card = skill:viewAs(selected_cards) + end + end + + if card then + local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or Util.DummyTable + for _, sk in ipairs(status_skills) do + ret = ret or {} + if #ret > 4 then + return ret + end + + local tip = sk:getTargetTip(Self, to_select, selected, selected_cards, card, selectable, extra_data) + if type(tip) == "string" then + table.insert(ret, { content = tip, 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 = tip, type = "normal" }) + elseif type(tip) == "table" then + table.insertTable(ret, tip) + end + end + + return ret +end + function CanSortHandcards(pid) return ClientInstance:getPlayerById(pid):getMark(MarkEnum.SortProhibited) == 0 end @@ -940,7 +949,10 @@ function RevertSelection() h.scene:notifyUI() end +local requestUIUpdating = false function UpdateRequestUI(elemType, id, action, data) + if requestUIUpdating then return end + requestUIUpdating = true local h = ClientInstance.current_request_handler h.change = {} local finish = h:update(elemType, id, action, data) @@ -949,12 +961,14 @@ function UpdateRequestUI(elemType, id, action, data) else h:_finish() end + requestUIUpdating = false end function FinishRequestUI() local h = ClientInstance.current_request_handler if h then h:_finish() + ClientInstance.current_request_handler = nil end end diff --git a/lua/client/i18n/en_US.lua b/lua/client/i18n/en_US.lua index 766a39ad..a7a54c35 100644 --- a/lua/client/i18n/en_US.lua +++ b/lua/client/i18n/en_US.lua @@ -236,7 +236,7 @@ Fk:loadTranslationTable({ ["#replaceEquip"] = "Please Choose a Equip Card to be replaced", ["#askForPindian"] = "%arg: please choose a hand card for point fight", ["#StartPindianReason"] = "%from started point fight (%arg)", - ["#ShowPindianCard"] = "The point fight card of %from is %card", + ["#ShowPindianCard"] = "The point fight card of %from is %arg", ["#ShowPindianResult"] = "%from %arg the point fight between %from and %to", ["pindianwin"] = "won", ["pindiannotwin"] = "lost", diff --git a/lua/client/i18n/zh_CN.lua b/lua/client/i18n/zh_CN.lua index a2d0b7ca..8b4cc862 100644 --- a/lua/client/i18n/zh_CN.lua +++ b/lua/client/i18n/zh_CN.lua @@ -205,6 +205,7 @@ FreeKill使用的是libgit2的C API,与此同时使用Git完成拓展包的下 ["Cancel"] = "取消", ["End"] = "结束", -- ["Quit"] = "退出", + ["All"] = "全部", ["BanGeneral"] = "禁将", ["ResumeGeneral"] = "解禁", ["Enable"] = "启用", @@ -293,7 +294,7 @@ FreeKill使用的是libgit2的C API,与此同时使用Git完成拓展包的下 ["#replaceEquip"] = "选择一张装备牌替换之", ["#askForPindian"] = "%arg:请选择一张手牌作为拼点牌", ["#StartPindianReason"] = "%from 由于 %arg 而发起拼点", - ["#ShowPindianCard"] = "%from 的拼点牌是 %card", + ["#ShowPindianCard"] = "%from 的拼点牌是 %arg", ["#ShowPindianResult"] = "%from 在 %from 和 %to 之间的拼点中 %arg", ["pindianwin"] = "赢", ["pindiannotwin"] = "没赢", diff --git a/lua/core/debug.lua b/lua/core/debug.lua index be097798..dc02fdee 100644 --- a/lua/core/debug.lua +++ b/lua/core/debug.lua @@ -23,7 +23,7 @@ function Traceback() end local msgh = function(err) - fk.qCritical(err .. "\n" .. debug.traceback(nil, 2)) + fk.qCritical(tostring(err) .. "\n" .. debug.traceback(nil, 2)) end function Pcall(f, ...) @@ -36,3 +36,10 @@ end function p(v) print(inspect(v)) end function pt(t) for k, v in pairs(t) do print(k, v) end end + +local _verbose = false +function verbose(fmt, ...) + if not _verbose then return end + local str = fmt:format(...) + fk.qInfo(str) +end diff --git a/lua/core/engine.lua b/lua/core/engine.lua index 19c1aa71..26913528 100644 --- a/lua/core/engine.lua +++ b/lua/core/engine.lua @@ -19,10 +19,12 @@ ---@field public same_generals table @ 所有同名武将组合 ---@field public lords string[] @ 所有主公武将,用于常备主公 ---@field public all_card_types table @ 所有的卡牌类型以及一张样板牌 +---@field public all_card_names string[] @ 有序的所有的卡牌牌名,顺序:基本牌(杀置顶),普通锦囊,延时锦囊,按副类别排序的装备 ---@field public cards Card[] @ 所有卡牌 ---@field public translations table> @ 翻译表 ---@field public game_modes table @ 所有游戏模式 ---@field public game_mode_disabled table @ 游戏模式禁用的包 +---@field public main_mode_list table @ 主模式检索表 ---@field public currentResponsePattern string @ 要求用牌的种类(如要求用特定花色的桃···) ---@field public currentResponseReason string @ 要求用牌的原因(如濒死,被特定牌指定,使用特定技能···) ---@field public filtered_cards table @ 被锁视技影响的卡牌 @@ -66,10 +68,12 @@ function Engine:initialize() self.same_generals = {} self.lords = {} -- lordName[] self.all_card_types = {} + self.all_card_names = {} self.cards = {} -- Card[] self.translations = {} -- srcText --> translated self.game_modes = {} self.game_mode_disabled = {} + self.main_mode_list = {} self.kingdoms = {} self.kingdom_map = {} self.damage_nature = { [fk.NormalDamage] = { "normal_damage", false } } @@ -82,6 +86,7 @@ function Engine:initialize() self:loadPackages() self:setLords() + self:loadCardNames() self:loadDisabled() self:loadRequestHandlers() self:addSkills(AuxSkills) @@ -265,9 +270,28 @@ function Engine:loadDisabled() for mode_name, game_mode in pairs(self.game_modes) do local disabled_packages = {} for name, pkg in pairs(self.packages) do - if table.contains(game_mode.blacklist or Util.DummyTable, name) or - (game_mode.whitelist and not table.contains(game_mode.whitelist, name)) or - table.contains(pkg.game_modes_blacklist or Util.DummyTable, mode_name) or + --- GameMode对Package筛选 + if type(game_mode.whitelist) == "function" then + if not game_mode:whitelist(pkg) then + table.insert(disabled_packages, name) + end + elseif type(game_mode.whitelist) == "table" then + if not table.contains(game_mode.whitelist, name) then + table.insert(disabled_packages, name) + end + end + if type(game_mode.blacklist) == "function" then + if game_mode:blacklist(pkg) then + table.insert(disabled_packages, name) + end + elseif type(game_mode.blacklist) == "table" then + if table.contains(game_mode.blacklist, name) then + table.insert(disabled_packages, name) + end + end + + --- Package对GameMode筛选 + if table.contains(pkg.game_modes_blacklist or Util.DummyTable, mode_name) or (pkg.game_modes_whitelist and not table.contains(pkg.game_modes_whitelist, mode_name)) then table.insert(disabled_packages, name) end @@ -387,6 +411,7 @@ function Engine:setLords() for _, skill in ipairs(skills) do if skill.lordSkill then table.insert(self.lords, general.name) + break end end end @@ -498,6 +523,7 @@ function Engine:addCard(card) if self.all_card_types[card.name] == nil then self.skills[card.skill.name] = card.skill self.all_card_types[card.name] = card + table.insert(self.all_card_names, card.name) end end @@ -524,6 +550,23 @@ function Engine:cloneCard(name, suit, number) return ret end +--- 为所有加载的卡牌牌名排序 +function Engine:loadCardNames() + local slash, basic, commonTrick, other = {}, {}, {}, {} + for _, name in ipairs(self.all_card_names) do + local card = self.all_card_types[name] + if card.type == Card.TypeBasic then + table.insert(card.trueName == "slash" and slash or basic, name) + elseif card:isCommonTrick() then + table.insert(commonTrick, name) + else + table.insert(other, name) + end + end + table.sort(other, function(a, b) return self.all_card_types[a].sub_type < self.all_card_types[b].sub_type end) + self.all_card_names = table.connect(slash, basic, commonTrick, other) +end + --- 向Engine中添加一系列游戏模式。 ---@param game_modes GameMode[] @ 要添加的游戏模式列表 function Engine:addGameModes(game_modes) diff --git a/lua/core/game_mode.lua b/lua/core/game_mode.lua index 8229cf65..1ab7b063 100644 --- a/lua/core/game_mode.lua +++ b/lua/core/game_mode.lua @@ -1,6 +1,22 @@ -- SPDX-License-Identifier: GPL-3.0-or-later --- 呃 起码要做出以下几种吧: +--- GameMode用来描述一个游戏模式。 +--- +--- 可以参考欢乐斗地主。 +--- +---@class GameMode: Object +---@field public name string @ 游戏模式名 +---@field public minPlayer integer @ 最小玩家数 +---@field public maxPlayer integer @ 最大玩家数 +---@field public rule? TriggerSkill @ 规则(通过技能完成,通常用来为特定角色及特定时机提供触发事件) +---@field public logic? fun(): GameLogic @ 逻辑(通过function完成,通常用来初始化、分配身份及座次) +---@field public whitelist? string[] | fun(self: GameMode, pkg: Package): bool @ 白名单 +---@field public blacklist? string[] | fun(self: GameMode, pkg: Package): bool @ 黑名单 +---@field public config_template? GameModeConfigEntry[] 游戏模式的配置页面,如此一个数组 +---@field public main_mode? string @ 主模式名(用于判断此模式是否为某模式的衍生) +local GameMode = class("GameMode") + +-- 呃 起码要做出以下几种吧:(class必须放顶层否则文档那个东西不识别 辣鸡 -- Switch:那个开关组件 [boolean model=nil] -- RadioButton:那个多选一,圆圈里面打点组件 [string model={ label, value }[]] -- ComboBox: 那个多选一,下拉一个菜单并选择一个的组件 [string model同上] @@ -16,21 +32,6 @@ ---@field public model any @ 这种delegate需要的model:参见注释 ---@field public default? any @ 默认值 cfg的value ---- GameMode用来描述一个游戏模式。 ---- ---- 可以参考欢乐斗地主。 ---- ----@class GameMode: Object ----@field public name string @ 游戏模式名 ----@field public minPlayer integer @ 最小玩家数 ----@field public maxPlayer integer @ 最大玩家数 ----@field public rule? TriggerSkill @ 规则(通过技能完成,通常用来为特定角色及特定时机提供触发事件) ----@field public logic? fun(): GameLogic @ 逻辑(通过function完成,通常用来初始化、分配身份及座次) ----@field public whitelist? string[] @ 白名单 ----@field public blacklist? string[] @ 黑名单 ----@field public config_template? GameModeConfigEntry[] 游戏模式的配置页面,如此一个数组 -local GameMode = class("GameMode") - --- 构造函数,不可随意调用。 ---@param name string @ 游戏模式名 ---@param min integer @ 最小玩家数 @@ -100,14 +101,17 @@ function GameMode:getAdjustedProperty (player) return list end ---- 向游戏模式中添加拓展包过滤。 ----@param whitelist string[] @ 白名单 ----@param blacklist string[] @ 黑名单 -function GameMode:addPackageFilter(whitelist, blacklist) - assert(type(whitelist) == "table") - assert(type(blacklist) == "table") - table.insertTable(self.whitelist, whitelist) - table.insertTable(self.blacklist, blacklist) + +-- 执行死亡奖惩 +---@param victim ServerPlayer @ 死亡角色 +---@param killer? ServerPlayer @ 击杀者 +function GameMode:deathRewardAndPunish (victim, killer) + if not killer or killer.dead then return end + if victim.role == "rebel" then + killer:drawCards(3, "kill") + elseif victim.role == "loyalist" and killer.role == "lord" then + killer:throwAllCards("he") + end end return GameMode diff --git a/lua/core/package.lua b/lua/core/package.lua index be6f4af7..04b1f73a 100644 --- a/lua/core/package.lua +++ b/lua/core/package.lua @@ -85,21 +85,4 @@ function Package:addGameMode(game_mode) table.insert(self.game_modes, game_mode) end ---- 向拓展包中设置游戏模式过滤。 ----@param whitelist string[] @ 白名单 ----@param blacklist string[] @ 黑名单 -function Package:setGameModeFilter(whitelist, blacklist) - self.game_modes_whitelist = whitelist - self.game_modes_blacklist = blacklist -end - ---- 向拓展包中添加游戏模式过滤。 ----@param whitelist string[] @ 白名单 ----@param blacklist string[] @ 黑名单 -function Package:addGameModeFilter(whitelist, blacklist) - assert(type(whitelist) == "table") - assert(type(blacklist) == "table") - table.insertTable(self.game_modes_whitelist, whitelist) - table.insertTable(self.game_modes_blacklist, blacklist) -end return Package diff --git a/lua/core/player.lua b/lua/core/player.lua index c2428ecc..c789ce5b 100644 --- a/lua/core/player.lua +++ b/lua/core/player.lua @@ -1194,6 +1194,25 @@ function Player:isBuddy(other) return self.id == id or table.contains(self.buddy_list, id) end +local function defaultCardVisible(self, cardId, area, owner, falsy) + local public_areas = {Card.DiscardPile, Card.Processing, Card.Void, Card.PlayerEquip, Card.PlayerJudge} + local player_areas = {Card.PlayerHand, Card.PlayerSpecial} + + if area == Card.DrawPile then return false + elseif table.contains(public_areas, area) then return not falsy + elseif table.contains(player_areas, area) then + if area == Card.PlayerSpecial then + local specialName = owner:getPileNameOfId(cardId) + if not specialName:startsWith("$") then + return true + end + end + return owner == self or self:isBuddy(owner) + else + return false + end +end + --- Player是否可看到某card --- @param cardId integer ---@param move? CardsMoveStruct @@ -1203,11 +1222,15 @@ function Player:cardVisible(cardId, move) if room.replaying and room.replaying_show then return true end local falsy = false -- 当难以决定时是否要选择暗置? + local oldarea, oldowner if move then - if table.find(move.moveInfo, function(info) return info.cardId == cardId end) then + ---@type MoveInfo + local info = table.find(move.moveInfo, function(info) return info.cardId == cardId end) + if info then + oldarea = info.fromArea + oldowner = move.from and room:getPlayerById(move.from) if move.moveVisible then return true end if move.moveVisible == false then falsy = true end - -- specialVisible还要控制这个pile对他人是否应该可见,但是不在这里生效 if move.specialVisible then return true end if (type(move.visiblePlayers) == "number" and move.visiblePlayers == self.id) or @@ -1218,11 +1241,9 @@ function Player:cardVisible(cardId, move) end local area = room:getCardArea(cardId) + local owner = room:getCardOwner(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 and not room.replaying then return table.contains(public_areas, area) end local status_skills = Fk:currentRoom().status_skills[VisibilitySkill] or Util.DummyTable @@ -1233,16 +1254,14 @@ function Player:cardVisible(cardId, move) end end - if area == Card.DrawPile then return false - elseif table.contains(public_areas, area) then return not falsy - elseif move and area == Card.PlayerSpecial and not move.specialName:startsWith("$") then - return not falsy - elseif table.contains(player_areas, area) then - local to = room:getCardOwner(cardId) - return to == self or self:isBuddy(to) - else - return false + if defaultCardVisible(self, cardId, area, owner, falsy) then + return true + elseif oldarea then + -- 尽可能让牌可见 + return defaultCardVisible(self, cardId, oldarea, oldowner, falsy) end + + return false end --- Player是否可看到某target的身份 @@ -1328,7 +1347,7 @@ function Player:loadJsonObject(o) 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 _, ids in pairs(o.special_cards) do for _, id in ipairs(ids) do room:setCardArea(id, Card.PlayerSpecial, pid) end diff --git a/lua/core/request_handler.lua b/lua/core/request_handler.lua index d065d53a..97142b8c 100644 --- a/lua/core/request_handler.lua +++ b/lua/core/request_handler.lua @@ -37,7 +37,7 @@ ---@field public change { [string]: Item[] } 将会传递给UI的更新数据 local RequestHandler = class("RequestHandler") -function RequestHandler:initialize(player) +function RequestHandler:initialize(player, data) self.room = Fk:currentRoom() self.player = player -- finish只在Client执行 用于保证UI执行了某些必须执行的善后 diff --git a/lua/core/request_type/active_skill.lua b/lua/core/request_type/active_skill.lua index b6dd4da3..d3eebd4e 100644 --- a/lua/core/request_type/active_skill.lua +++ b/lua/core/request_type/active_skill.lua @@ -25,11 +25,18 @@ local CardItem = (require 'ui_emu.common').CardItem ---@field public expanded_piles { [string]: integer[] } 用于展开/收起 local ReqActiveSkill = RequestHandler:subclass("ReqActiveSkill") -function ReqActiveSkill:initialize(player) +function ReqActiveSkill:initialize(player, data) RequestHandler.initialize(self, player) self.scene = RoomScene:new(self) self.expanded_piles = {} + + if data then + self.skill_name = data[1] + self.prompt = data[2] + self.cancelable = data[3] + self.extra_data = data[4] + end end function ReqActiveSkill:setup(ignoreInteraction) @@ -55,6 +62,7 @@ function ReqActiveSkill:setup(ignoreInteraction) self:updateUnselectedTargets() self:updateButtons() + self:updatePrompt() end function ReqActiveSkill:finish() @@ -73,6 +81,15 @@ function ReqActiveSkill:setSkillPrompt(skill, cid) end end +function ReqActiveSkill:updatePrompt() + local skill = Fk.skills[self.skill_name] + if skill then + self:setSkillPrompt(skill) + 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 @@ -80,7 +97,7 @@ function ReqActiveSkill:setupInteraction() if not interaction then return end - skill.interaction.data = interaction.default_choice or nil -- FIXME + skill.interaction.data = interaction.default or interaction.default_choice or nil -- FIXME -- 假设只有1个interaction (其实目前就是这样) local i = Interaction:new(self.scene, "1", interaction) i.skill_name = self.skill_name @@ -291,11 +308,19 @@ function ReqActiveSkill:doOKButton() if self.selected_card then reply.special_skill = self.skill_name end - ClientInstance:notifyUI("ReplyToServer", json.encode(reply)) + if ClientInstance then + ClientInstance:notifyUI("ReplyToServer", json.encode(reply)) + else + return reply + end end function ReqActiveSkill:doCancelButton() - ClientInstance:notifyUI("ReplyToServer", "__cancel") + if ClientInstance then + ClientInstance:notifyUI("ReplyToServer", "__cancel") + else + return "__cancel" + end end -- 对点击卡牌的处理。data中包含selected属性,可能是选中或者取消选中,分开考虑。 @@ -365,6 +390,7 @@ function ReqActiveSkill:update(elemType, id, action, data) elseif elemType == "Interaction" then self:updateInteraction(data) end + self:updatePrompt() end return ReqActiveSkill diff --git a/lua/core/request_type/play_card.lua b/lua/core/request_type/play_card.lua index 48c67617..812ba0d5 100644 --- a/lua/core/request_type/play_card.lua +++ b/lua/core/request_type/play_card.lua @@ -19,7 +19,6 @@ end function ReqPlayCard:setup() ReqUseCard.setup(self) - self:setPrompt(self.original_prompt) self.scene:update("Button", "End", { enabled = true }) end @@ -85,14 +84,22 @@ 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 + local card = self.selected_card + if self.skill_name then + local skill = Fk.skills[self.skill_name] + if skill:isInstanceOf(ActiveSkill) then + return ReqActiveSkill.feasible(self) + else -- viewasskill + card = skill:viewAs(self.pendings) + end + end if card then local skill = card.skill ---@type ActiveSkill ret = skill:feasible(self.selected_targets, { card.id }, player, card) + if ret then + ret = skill:canUse(player, card, self.extra_data) and not player:prohibitUse(card) + end end return ret end @@ -107,11 +114,11 @@ function ReqPlayCard:selectSpecialUse(data) 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()) + -- 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) + -- self:setSkillPrompt(Fk.skills[data], self.pendings) end self:initiateTargets() end @@ -131,7 +138,11 @@ end function ReqPlayCard:doEndButton() self.scene:update("SpecialSkills", "1", { skills = {} }) self.scene:notifyUI() - ClientInstance:notifyUI("ReplyToServer", "") + if ClientInstance then + ClientInstance:notifyUI("ReplyToServer", "") + else + return "" + end end function ReqPlayCard:selectCard(cid, data) @@ -146,7 +157,7 @@ function ReqPlayCard:selectCard(cid, data) self.skill_name = nil self.selected_card = Fk:getCardById(cid) scene:unselectOtherCards(cid) - self:setSkillPrompt(self.selected_card.skill, self.selected_card:getEffectiveId()) + -- 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) diff --git a/lua/core/request_type/response_card.lua b/lua/core/request_type/response_card.lua index 0b4f0390..779bdfd6 100644 --- a/lua/core/request_type/response_card.lua +++ b/lua/core/request_type/response_card.lua @@ -17,6 +17,19 @@ local ReqActiveSkill = require 'core.request_type.active_skill' ---@field public original_prompt string 最开始的提示信息;这种涉及技能按钮的需要这样一下 local ReqResponseCard = ReqActiveSkill:subclass("ReqResponseCard") +function ReqResponseCard:initialize(player, data) + ReqActiveSkill.initialize(self, player) + + if data then + -- self.skill_name = data[1] (skill_name是给选中的视为技用的) + self.pattern = data[2] + self.prompt = data[3] + self.cancelable = data[4] + self.extra_data = data[5] + self.disabledSkillNames = data[6] + end +end + function ReqResponseCard:setup() if not self.original_prompt then self.original_prompt = self.prompt or "" @@ -25,6 +38,7 @@ function ReqResponseCard:setup() ReqActiveSkill.setup(self) self.selected_card = nil self:updateSkillButtons() + self:updatePrompt() end -- FIXME: 关于&牌堆的可使用打出瞎jb写了点 来个懂哥优化一下 @@ -41,8 +55,12 @@ end function ReqResponseCard:skillButtonValidity(name) local player = self.player local skill = Fk.skills[name] - return skill:isInstanceOf(ViewAsSkill) and skill:enabledAtResponse(player, true) - and skill.pattern and Exppattern:Parse(self.pattern):matchExp(skill.pattern) + return + skill:isInstanceOf(ViewAsSkill) and + skill:enabledAtResponse(player, true) and + skill.pattern and + Exppattern:Parse(self.pattern):matchExp(skill.pattern) and + not table.contains(self.disabledSkillNames or {}, name) end function ReqResponseCard:cardValidity(cid) @@ -86,7 +104,11 @@ function ReqResponseCard:doOKButton() card = self.selected_card:getEffectiveId(), targets = self.selected_targets, } - ClientInstance:notifyUI("ReplyToServer", json.encode(reply)) + if ClientInstance then + ClientInstance:notifyUI("ReplyToServer", json.encode(reply)) + else + return reply + end end function ReqResponseCard:doCancelButton() @@ -109,9 +131,10 @@ function ReqResponseCard:selectSkill(skill, data) end self.skill_name = skill self.selected_card = nil - self:setSkillPrompt(skill) ReqActiveSkill.setup(self) + + -- self:setSkillPrompt(Fk.skills[skill]) else self.skill_name = nil self.prompt = self.original_prompt diff --git a/lua/core/request_type/use_card.lua b/lua/core/request_type/use_card.lua index 62a9e838..6bbee28b 100644 --- a/lua/core/request_type/use_card.lua +++ b/lua/core/request_type/use_card.lua @@ -4,11 +4,27 @@ local ReqResponseCard = require 'core.request_type.response_card' ---@class ReqUseCard: ReqResponseCard local ReqUseCard = ReqResponseCard:subclass("ReqUseCard") +function ReqUseCard:updatePrompt() + if self.skill_name then + return ReqActiveSkill.updatePrompt(self) + end + local card = self.selected_card + if card and card.skill then + self:setSkillPrompt(card.skill, self.selected_card.id) + else + self:setPrompt(self.original_prompt or "") + end +end + function ReqUseCard:skillButtonValidity(name) local player = self.player local skill = Fk.skills[name] - return skill:isInstanceOf(ViewAsSkill) and skill:enabledAtResponse(player, false) - and skill.pattern and Exppattern:Parse(self.pattern):matchExp(skill.pattern) + return + skill:isInstanceOf(ViewAsSkill) and + skill:enabledAtResponse(player, false) and + skill.pattern and + Exppattern:Parse(self.pattern):matchExp(skill.pattern) and + not table.contains(self.disabledSkillNames or {}, name) end function ReqUseCard:cardValidity(cid) @@ -45,7 +61,7 @@ function ReqUseCard:targetValidity(pid) -- 若include中全都没选,且target不在include中则不可选择 if table.every(data.include_targets, function(id) return not table.contains(self.selected_targets, id) - end) and not table.contains(data.must_targets, pid) then + end) and not table.contains(data.include_targets, pid) then ret = false end end @@ -108,7 +124,7 @@ function ReqUseCard:selectTarget(playerid, data) self.selected_targets = {} for _, pid in ipairs(previous_targets) do local ret - ret = not player:isProhibited(pid, card) and skill and + ret = not player:isProhibited(self.room:getPlayerById(pid), card) and skill and skill:targetFilter(pid, self.selected_targets, { card.id }, card, data.extra_data) -- 从头开始写目标 diff --git a/lua/core/room/abstract_room.lua b/lua/core/room/abstract_room.lua index 02ad01ad..3d948432 100644 --- a/lua/core/room/abstract_room.lua +++ b/lua/core/room/abstract_room.lua @@ -89,4 +89,11 @@ function AbstractRoom:loadJsonObject(o) end end +-- 判断当前模式是否为某类模式 +---@param mode string @ 需要判定的模式类型 +---@return boolean +function AbstractRoom:isGameMode(mode) + return table.contains(Fk.main_mode_list[mode] or {}, self.settings.gameMode) +end + return AbstractRoom diff --git a/lua/core/skill.lua b/lua/core/skill.lua index cc81fe25..1b6d7e82 100644 --- a/lua/core/skill.lua +++ b/lua/core/skill.lua @@ -156,4 +156,16 @@ function Skill:getTimes() return ret end +-- 获得此技能时,触发此函数 +---@param player ServerPlayer +---@param is_start bool +function Skill:onAcquire(player, is_start) +end + +-- 失去此技能时,触发此函数 +---@param player ServerPlayer +---@param is_death bool +function Skill:onLose(player, is_death) +end + return Skill diff --git a/lua/core/skill_type/trigger.lua b/lua/core/skill_type/trigger.lua index 6c94820b..04984bd9 100644 --- a/lua/core/skill_type/trigger.lua +++ b/lua/core/skill_type/trigger.lua @@ -57,15 +57,18 @@ end -- DO NOT modify this function function TriggerSkill:doCost(event, target, player, data) local start_time = os.getms() + local room = player.room + room.current_cost_skill = self local ret = self:cost(event, target, player, data) local end_time = os.getms() local room = player.room -- 对于那种cost直接返回true的锁定技,如果是预亮技,那么还是询问一下好 - if ret and player:isFakeSkill(self) and end_time - start_time < 10000 and + if ret and player:isFakeSkill(self) and end_time - start_time < 1000 and (self.main_skill and self.main_skill or self).visible then ret = room:askForSkillInvoke(player, self.name) end + room.current_cost_skill = nil local cost_data_bak = self.cost_data room.logic:trigger(fk.BeforeTriggerSkillUse, player, { skill = self, willUse = ret }) diff --git a/lua/core/skill_type/usable_skill.lua b/lua/core/skill_type/usable_skill.lua index 58e544ff..0c48510a 100644 --- a/lua/core/skill_type/usable_skill.lua +++ b/lua/core/skill_type/usable_skill.lua @@ -92,4 +92,25 @@ function UsableSkill:withinTimesLimit(player, scope, card, card_name, to) -- end))) end +-- 失去此技能时,触发此函数 +---@param player ServerPlayer +---@param is_death bool +function UsableSkill:onLose(player, is_death) + local lost_piles = {} + if self.derived_piles then + for _, pile_name in ipairs(self.derived_piles) do + table.insertTableIfNeed(lost_piles, player:getPile(pile_name)) + end + end + + if #lost_piles > 0 then + player.room:moveCards({ + ids = lost_piles, + from = player.id, + toArea = Card.DiscardPile, + moveReason = fk.ReasonPutIntoDiscardPile, + }) + end +end + return UsableSkill diff --git a/lua/core/util.lua b/lua/core/util.lua index 77ffd362..6ef869c3 100644 --- a/lua/core/util.lua +++ b/lua/core/util.lua @@ -98,15 +98,15 @@ end ---@param val_func? fun(e: T): integer @ 计算权值的函数,对int[]可不写 ---@param reverse? boolean @ 是否反排?反排的话优先返回权值小的元素 function fk.sorted_pairs(t, val_func, reverse) - val_func = val_func or function(e) return e end local t2 = table.simpleClone(t) -- 克隆一次表,用作迭代器上值 + local t_vals = table.map(t2, val_func or function(e) return e end) local iter = function() local max_idx, max, max_val = -1, nil, nil for i, v in ipairs(t2) do if not max then - max_idx, max, max_val = i, v, val_func(v) + max_idx, max, max_val = i, v, t_vals[i] else - local val = val_func(v) + local val = t_vals[i] local checked = val > max_val if reverse then checked = not checked end if checked then @@ -116,6 +116,7 @@ function fk.sorted_pairs(t, val_func, reverse) end if max_idx == -1 then return nil, nil end table.remove(t2, max_idx) + table.remove(t_vals, max_idx) return -1, max, max_val end return iter, nil, 1 @@ -729,4 +730,15 @@ function AimGroup:getCancelledTargets(aimGroup) return aimGroup[AimGroup.Cancelled] end +---@param target ServerPlayer +---@param data AimStruct +---@return boolean +function AimGroup:isOnlyTarget(target, data) + if data.tos == nil then return false end + local tos = AimGroup:getAllTargets(data.tos) + return table.contains(tos, target.id) and not table.find(target.room.alive_players, function (p) + return p ~= target and table.contains(tos, p.id) + end) +end + return { TargetGroup, AimGroup, Util } diff --git a/lua/fk_ex.lua b/lua/fk_ex.lua index f53a5bdc..954cf1df 100644 --- a/lua/fk_ex.lua +++ b/lua/fk_ex.lua @@ -45,6 +45,16 @@ local function readCommonSpecToSkill(skill, spec) assert(type(spec.relate_to_place) == "string") skill.relate_to_place = spec.relate_to_place end + + if spec.on_acquire then + assert(type(spec.on_acquire) == "function") + skill.onAcquire = spec.on_acquire + end + + if spec.on_lose then + assert(type(spec.on_lose) == "function") + skill.onLose = spec.on_lose + end end local function readUsableSpecToSkill(skill, spec) @@ -88,6 +98,8 @@ end ---@field public max_round_use_time? integer ---@field public max_game_use_time? integer ---@field public times? integer | fun(self: UsableSkill): integer +---@field public on_acquire? fun(self: UsableSkill, player: ServerPlayer, is_start: boolean) +---@field public on_lose? fun(self: UsableSkill, player: ServerPlayer, is_death: boolean) ---@class StatusSkillSpec: StatusSkill @@ -191,6 +203,7 @@ end ---@field public mod_target_filter? fun(self: ActiveSkill, to_select: integer, selected: integer[], user: integer, card: Card, distance_limited: boolean): boolean? ---@field public prompt? string|fun(self: ActiveSkill, selected_cards: integer[], selected_targets: integer[]): string ---@field public interaction any +---@field public target_tip? fun(self: ActiveSkill, to_select: integer, selected: integer[], selected_cards: integer[], card: Card, selectable: boolean, extra_data: any): string|TargetTipDataSpec? ---@param spec ActiveSkillSpec ---@return ActiveSkill @@ -635,7 +648,14 @@ function fk.CreateTreasure(spec) return card end ----@param spec GameMode +---@class GameModeSpec: GameMode +---@field public winner_getter? fun(self: GameMode, victim: ServerPlayer): string +---@field public surrender_func? fun(self: GameMode, playedTime: number): string +---@field public is_counted? fun(self: GameMode, room: Room): boolean +---@field public get_adjusted? fun(self: GameMode, player: ServerPlayer): table +---@field public reward_punish? fun(self: GameMode, victim: ServerPlayer, killer: ServerPlayer) + +---@param spec GameModeSpec ---@return GameMode function fk.CreateGameMode(spec) assert(type(spec.name) == "string") @@ -646,6 +666,9 @@ function fk.CreateGameMode(spec) ret.blacklist = spec.blacklist ret.rule = spec.rule ret.logic = spec.logic + ret.main_mode = spec.main_mode or spec.name + Fk.main_mode_list[ret.main_mode] = Fk.main_mode_list[ret.main_mode] or {} + table.insert(Fk.main_mode_list[ret.main_mode], ret.name) if spec.winner_getter then assert(type(spec.winner_getter) == "function") @@ -663,6 +686,10 @@ function fk.CreateGameMode(spec) assert(type(spec.get_adjusted) == "function") ret.getAdjustedProperty = spec.get_adjusted end + if spec.reward_punish then + assert(type(spec.reward_punish) == "function") + ret.deathRewardAndPunish = spec.reward_punish + end return ret end diff --git a/lua/freekill.lua b/lua/freekill.lua index 0ed8b43d..b4a09948 100644 --- a/lua/freekill.lua +++ b/lua/freekill.lua @@ -79,17 +79,18 @@ end Config = loadConf() -- 禁用各种危险的函数,尽可能让Lua执行安全的代码。 -local _os = { +os = { time = os.time, date = os.date, clock = os.clock, difftime = os.difftime, getms = os.getms, } -os = _os -io = nil +io = { + lines = io.lines +} package = nil -load = nil +-- load = nil loadfile = nil local _dofile = dofile dofile = function(f) diff --git a/lua/lib/debugger.lua b/lua/lib/debugger.lua index 8a9b0470..41c33aae 100644 --- a/lua/lib/debugger.lua +++ b/lua/lib/debugger.lua @@ -245,7 +245,9 @@ local function where(info, context_lines) local filename = info.source:match("@(.*)") if filename then if UsingNewCore and (filename:startsWith("./lua/") or filename:startsWith("lua/")) then - filename = "./packages/freekill-core/" .. filename + if not FileIO.pwd():endsWith("packages/freekill-core") then + filename = "./packages/freekill-core/" .. filename + end end pcall(function() for line in io.lines(filename) do table.insert(source, line) end end) elseif info.source then diff --git a/lua/lsp/lib.lua b/lua/lsp/lib.lua index 5540212c..0b20c4be 100644 --- a/lua/lsp/lib.lua +++ b/lua/lsp/lib.lua @@ -24,7 +24,10 @@ function Object:initialize(...) end ---@return T function Object:new(...)end +---@generic T +---@param self T ---@param name string +---@return T function Object:subclass(name)end ---@param class class|Object diff --git a/lua/server/ai/ai.lua b/lua/server/ai/ai.lua index 844c3851..1c6a3ad9 100644 --- a/lua/server/ai/ai.lua +++ b/lua/server/ai/ai.lua @@ -7,39 +7,222 @@ ---@field public room Room ---@field public player ServerPlayer ---@field public command string ----@field public jsonData string ----@field public cb_table table +---@field public data any +---@field public handler ReqActiveSkill 可能空,但是没打问号(免得一堆警告) local AI = class("AI") function AI:initialize(player) + ---@diagnostic disable-next-line self.room = RoomInstance self.player = player - local cb_t = {} - -- default strategy: print command and data, then return "" - setmetatable(cb_t, { - __index = function() - return function() - print(self.command, self.jsonData) - return "" - end - end, - }) - self.cb_table = cb_t end -function AI:readRequestData() - self.command = self.player.ai_data.command - self.jsonData = self.player.ai_data.jsonData +function AI:__tostring() + return string.format("%s: %s", self.class.name, tostring(self.player)) +end + +-- activeSkill, responseCard, useCard, playCard 四巨头专属 +function AI:isInDashboard() + if not (self.handler and self.handler:isInstanceOf(Fk.request_handlers["AskForUseActiveSkill"])) then + fk.qWarning("请检查是否在AI中调用了专属于dashboard操作的一系列函数") + fk.qWarning(debug.traceback()) + return false + end + return true +end + +function AI:getPrompt() + local handler = self.handler + if not handler then return "" end + return handler.prompt +end + +--- 返回当前手牌区域内(包含展开的pile)中所有可选且未选中的卡牌 返回ids +---@param pattern string? 可以带一个过滤条件 +---@return integer[] +function AI:getEnabledCards(pattern) + if not self:isInDashboard() then return Util.DummyTable end + + local ret = {} + for cid, item in pairs(self.handler.scene:getAllItems("CardItem")) do + if item.enabled and not item.selected then + if (not pattern) or Exppattern:Parse(pattern):match(Fk:getCardById(cid)) then + table.insert(ret, cid) + end + end + end + return ret +end + +--- 返回当前所有可选并且还未选中的角色,包括自己 +---@return ServerPlayer[] +function AI:getEnabledTargets() + if not self:isInDashboard() then return Util.DummyTable end + local room = self.room + + local ret = {} + for pid, item in pairs(self.handler.scene:getAllItems("Photo")) do + if item.enabled and not item.selected then + table.insert(ret, room:getPlayerById(pid)) + end + end + return ret +end + +function AI:hasEnabledTarget() + if not self:isInDashboard() then return false end + local room = self.room + + for _, item in pairs(self.handler.scene:getAllItems("Photo")) do + if item.enabled and not item.selected then + return true + end + end + return false +end + +--- 获取技能面板中所有可以按下的技能按钮 +---@return string[] + +--- 获取技能面板中所有可以按下的技能按钮 +---@return string[] +function AI:getEnabledSkills() + if not self:isInDashboard() then return Util.DummyTable end + + local ret = {} + for name, item in pairs(self.handler.scene:getAllItems("SkillButton")) do + if item.enabled and not item.selected then + table.insert(ret, name) + end + end + return ret +end + +---@return integer[] +function AI:getSelectedCards() + if not self:isInDashboard() then return Util.DummyTable end + return self.handler.pendings +end + +---@return Card? +function AI:getSelectedCard() + if not self:isInDashboard() then return Util.DummyTable end + local handler = self.handler + if handler.selected_card then return handler.selected_card end + if not handler.skill_name then return end + local skill = Fk.skills[handler.skill_name] + if not skill:isInstanceOf(ViewAsSkill) then return end + return skill:viewAs(handler.pendings) +end + +---@return ServerPlayer[] +function AI:getSelectedTargets() + if not self:isInDashboard() then return Util.DummyTable end + return table.map(self.handler.selected_targets, function(pid) + return self.room:getPlayerById(pid) + end) +end + +function AI:getSelectedSkill() + if not self:isInDashboard() then return nil end + return self.handler.skill_name +end + +function AI:selectCard(cid, selected) + if not self:isInDashboard() then return end + verbose("%s选择卡牌%d(%s)", selected and "" or "取消", cid, tostring(Fk:getCardById(cid))) + self.handler:update("CardItem", cid, "click", { selected = selected }) +end + +---@param player ServerPlayer +function AI:selectTarget(player, selected) + if not self:isInDashboard() then return end + verbose("%s选择角色%s", selected and "" or "取消", tostring(player)) + self.handler:update("Photo", player.id, "click", { selected = selected }) +end + +function AI:selectSkill(skill_name, selected) + if not self:isInDashboard() then return end + verbose("%s选择技能%s", selected and "" or "取消", skill_name) + self.handler:update("SkillButton", skill_name, "click", { selected = selected }) +end + +function AI:unSelectAllCards() + for _, id in ipairs(self:getSelectedCards()) do + self:selectCard(id, false) + end +end + +function AI:unSelectAllTargets() + for _, p in ipairs(self:getSelectedTargets()) do + self:selectTarget(p, false) + end +end + +function AI:unSelectSkill() + local skill = self:getSelectedSkill() + if not skill then return end + self:selectSkill(skill, false) +end + +function AI:unSelectAll() + self:unSelectSkill() + self:unSelectAllCards() + self:unSelectAllTargets() +end + +function AI:okButtonEnabled() + if not self:isInDashboard() then return false end + return self.handler:feasible() +end + +function AI:isDeadend() + if not self:isInDashboard() then return true end + return (not self:okButtonEnabled()) and #self:getEnabledCards() == 0 + and #self:getEnabledTargets() == 0 +end + +function AI:doOKButton() + if not self:isInDashboard() then return end + if not self:okButtonEnabled() then return "" end + return self.handler:doOKButton() +end + +---@return Skill? +function AI:currentSkill() + local room = self.room + if room.current_cost_skill then return room.current_cost_skill end + local sname = room.logic:getCurrentSkillName() + if sname then + return Fk.skills[sname] + end end function AI:makeReply() Self = self.player - -- local start = os.getms() - local ret = self.cb_table[self.command] and self.cb_table[self.command](self, self.jsonData) or "__cancel" - if ret == "" then ret = "__cancel" end - -- local to_delay = 500 - (os.getms() - start) / 1000 - -- print(to_delay) - -- self.room:delay(to_delay) + local now = os.getms() + local fn = self["handle" .. self.command] + local is_active = self.command == "AskForUseActiveSkill" + if is_active then + local skill = Fk.skills[self.data[1]] + skill._extra_data = self.data[4] + end + local ret = "__cancel" + if fn then + local handler_class = Fk.request_handlers[self.command] + if handler_class then + self.handler = handler_class:new(self.player, self.data) + self.handler:setup() + end + ret = fn(self, self.data) + end + if ret == nil or ret == "" then ret = "__cancel" end + self.handler = nil + if is_active then + local skill = Fk.skills[self.data[1]] + skill._extra_data = Util.DummyTable + end + verbose("%s 在%.2fms后得出结果:%s", self.command, (os.getms() - now) / 1000, json.encode(ret)) return ret end diff --git a/lua/server/ai/init.lua b/lua/server/ai/init.lua index 07e56993..c214c30f 100644 --- a/lua/server/ai/init.lua +++ b/lua/server/ai/init.lua @@ -2,20 +2,29 @@ AI = require "server.ai.ai" TrustAI = require "server.ai.trust_ai" -RandomAI = require "server.ai.random_ai" +-- RandomAI = require "server.ai.random_ai" ---[[ 在release版暂时不启动。 +---[[ 在release版暂时不启动。 SmartAI = require "server.ai.smart_ai" ---[[ 调试中,暂且不加载额外的AI。 -- load ai module from packages -local directories = FileIO.ls("packages") -require "packages.standard.ai" -require "packages.standard_cards.ai" -require "packages.maneuvering.ai" +local directories +if UsingNewCore then + directories = FileIO.ls("..") + require "standard_cards.ai" + require "standard.ai" + -- require "maneuvering.ai" +else + directories = FileIO.ls("packages") + require "packages.standard.ai" + require "packages.standard_cards.ai" + require "packages.maneuvering.ai" +end table.removeOne(directories, "standard") table.removeOne(directories, "standard_cards") table.removeOne(directories, "maneuvering") +--[[ local _disable_packs = json.decode(fk.GetDisabledPacks()) for _, dir in ipairs(directories) do diff --git a/lua/server/ai/logic.lua b/lua/server/ai/logic.lua new file mode 100644 index 00000000..5ce67405 --- /dev/null +++ b/lua/server/ai/logic.lua @@ -0,0 +1,421 @@ +--- 用于对标room和room.logic的、专用于计算某一轮操作的收益的类。 +--- +--- 里面提供的方法和room尽可能完全一致,以便自动分析与手工编写简易预测流程。 +---@class AIGameLogic: Object +---@field public ai SmartAI +---@field public player ServerPlayer +---@field public benefit integer +local AIGameLogic = class("AIGameLogic") + +---@param ai SmartAI +function AIGameLogic:initialize(ai, base_benefit) + self.benefit = base_benefit or 0 + self.ai = ai + self.player = ai.player + self.logic = self -- 用于处理room.logic 这样真的好么。。 + + self.owner_map = ai.room.owner_map + self.card_place = ai.room.card_place +end + +function AIGameLogic:__index(_) + return function() return Util.DummyTable end +end + +function AIGameLogic:getPlayerById(id) + return self.ai.room:getPlayerById(id) +end + +function AIGameLogic:trigger(event, target, data) + local ai = self.ai + local logic = ai.room.logic + local skills = logic.skill_table[event] or Util.DummyTable + local _target = ai.room.current -- for iteration + local player = _target + local exit + + repeat + for _, skill in ipairs(skills) do + local skill_ai = fk.ai_trigger_skills[skill.name] + if skill_ai then + exit = skill_ai:getCorrect(self, event, target, player, data) + if exit then break end + end + end + if exit then break end + player = player.next + until player == _target + + return exit +end + +--- 血条、翻面、铁索等等的收益论(瞎jb乱填版) +function AIGameLogic:setPlayerProperty(player, key, value) + local orig = player[key] + local benefit = 0 + if key == "hp" then + benefit = (value - orig) * 200 + elseif key == "shield" then + benefit = (value - orig) * 150 + elseif key == "chained" then + benefit = value and -80 or 80 + elseif key == "faceup" then + if value and not orig then + benefit = 330 + elseif orig and not value then + benefit = -330 + end + end + if self.ai:isEnemy(player) then benefit = -benefit end + self.benefit = self.benefit + benefit +end + +--- 牌差收益论(瞎jb乱填版): +--- 根据moveInfo判断玩家是拿牌还是掉牌进而暴力算收益 +---@param data CardsMoveStruct +---@param info MoveInfo +function AIGameLogic:applyMoveInfo(data, info) + local benefit = 0 + + if data.from then + if info.fromArea == Player.Hand then + benefit = -90 + elseif info.fromArea == Player.Equip then + benefit = -110 + elseif info.fromArea == Player.Judge then + benefit = 180 + elseif info.fromArea == Player.Special then + benefit = -60 + end + + local from = data.from and self:getPlayerById(data.from) + if from and self.ai:isEnemy(from) then benefit = -benefit end + self.benefit = self.benefit + benefit + benefit = 0 + end + + if data.to then + if data.toArea == Player.Hand then + benefit = 90 + elseif data.toArea == Player.Equip then + benefit = 110 + elseif data.toArea == Player.Judge then + benefit = -180 + elseif data.toArea == Player.Special then + benefit = 60 + end + + local to = data.to and self:getPlayerById(data.to) + if to and self.ai:isEnemy(to) then benefit = -benefit end + self.benefit = self.benefit + benefit + end +end + +--- 阉割版GameEvent: 专用于AI进行简单的收益推理。 +--- +--- 事件首先需要定义自己对于某某玩家的基础收益值,例如伤害事件对目标造成-200的 +--- 收益。事件还要定义自己包含的触发时机列表,根据时机列表考虑相关技能对本次 +--- 事件的收益修正,最终获得真正的收益值。 +--- +--- 事件用于即将选卡/选目标时,或者触发技AI思考自己对某事件影响时构造并计算收益, +--- 因此很容易发生事件嵌套现象。为防止AI思考过久,必须对事件嵌套层数加以限制, +--- 比如限制最多思考到两层嵌套;毕竟没算力只能让AI蠢点了 +---@class AIGameEvent: Object +---@field public ai SmartAI +---@field public logic AIGameLogic +---@field public player ServerPlayer +---@field public data any +local AIGameEvent = class("AIGameEvent") + +---@param ai_logic AIGameLogic +function AIGameEvent:initialize(ai_logic, ...) + self.room = ai_logic + self.logic = ai_logic + self.ai = ai_logic.ai + self.player = self.ai.player + self.data = { ... } +end + +-- 真正的收益计算函数:子类重写这个 +function AIGameEvent:exec() +end + +local _depth = 0 + +-- 用做API的收益计算函数,不要重写 +function AIGameEvent:getBenefit() + local ret = true + _depth = _depth + 1 + if _depth <= 30 then + ret = self:exec() + end + _depth = _depth - 1 + return ret +end + +-- hp.lua + +local ChangeHp = AIGameEvent:subclass("AIGameEvent.ChangeHp") +fk.ai_events.ChangeHp = ChangeHp +function ChangeHp:exec() + local logic = self.logic + local player, num, reason, skillName, damageStruct = table.unpack(self.data) + + ---@type HpChangedData + local data = { + num = num, + reason = reason, + skillName = skillName, + damageEvent = damageStruct, + } + + if logic:trigger(fk.BeforeHpChanged, player, data) then + return true + end + + logic:setPlayerProperty(player, "hp", math.min(player.hp + data.num, player.maxHp)) + logic:trigger(fk.HpChanged, player, data) +end + +function AIGameLogic:changeHp(player, num, reason, skillName, damageStruct) + return not ChangeHp:new(self, player, num, reason, skillName, damageStruct):getBenefit() +end + +local Damage = AIGameEvent:subclass("AIGameEvent.Damage") +fk.ai_events.Damage = Damage +function Damage:exec() + local logic = self.logic + local damageStruct = table.unpack(self.data) + if (not damageStruct.chain) and (not damageStruct.chain_table) and Fk:canChain(damageStruct.damageType) then + damageStruct.chain_table = table.filter(self.ai.room:getOtherPlayers(damageStruct.to), function(p) + return p.chained + end) + end + + local stages = {} + if not damageStruct.isVirtualDMG then + stages = { + { fk.PreDamage, "from"}, + { fk.DamageCaused, "from" }, + { fk.DamageInflicted, "to" }, + } + end + + for _, struct in ipairs(stages) do + local event, player = table.unpack(struct) + if logic:trigger(event, damageStruct[player], damageStruct) then + return true + end + if damageStruct.damage < 1 then return true end + end + + if not damageStruct.isVirtualDMG then + ChangeHp:new(logic, damageStruct.to, -damageStruct.damage, + "damage", damageStruct.skillName, damageStruct):getBenefit() + end + + logic:trigger(fk.Damage, damageStruct.from, damageStruct) + logic:trigger(fk.Damaged, damageStruct.to, damageStruct) + logic:trigger(fk.DamageFinished, damageStruct.to, damageStruct) + + if damageStruct.chain_table and #damageStruct.chain_table > 0 then + for _, p in ipairs(damageStruct.chain_table) do + local dmg = { + from = damageStruct.from, + to = p, + damage = damageStruct.damage, + damageType = damageStruct.damageType, + card = damageStruct.card, + skillName = damageStruct.skillName, + chain = true, + } + + Damage:new(logic, dmg):getBenefit() + end + end +end + +function AIGameLogic:damage(damageStruct) + return not Damage:new(self, damageStruct):getBenefit() +end + +local LoseHp = AIGameEvent:subclass("AIGameEvent.LoseHp") +fk.ai_events.LoseHp = LoseHp +LoseHp.exec = AIParser.parseEventFunc(GameEvent.LoseHp.main) + +function AIGameLogic:loseHp(player, num, skillName) + return not LoseHp:new(self, player, num, skillName):getBenefit() +end + +local Recover = AIGameEvent:subclass("AIGameEvent.Recover") +fk.ai_events.Recover = Recover +Recover.exec = AIParser.parseEventFunc(GameEvent.Recover.main) + +function AIGameLogic:recover(recoverStruct) + return not Recover:new(self, recoverStruct):getBenefit() +end + +-- skill.lua + +local SkillEffect = AIGameEvent:subclass("AIGameEvent.SkillEffect") +fk.ai_events.SkillEffect = SkillEffect +function SkillEffect:exec() + local logic = self.logic + local effect_cb, player, skill, skill_data = table.unpack(self.data) + local main_skill = skill.main_skill and skill.main_skill or skill + + logic:trigger(fk.SkillEffect, player, main_skill) + effect_cb() + logic:trigger(fk.AfterSkillEffect, player, main_skill) +end + +function AIGameLogic:useSkill(player, skill, effect_cb, skill_data) + return not SkillEffect:new(self, effect_cb, player, skill, skill_data or Util.DummyTable):getBenefit() +end + +-- movecard.lua +local MoveCards = AIGameEvent:subclass("AIGameEvent.MoveCards") +fk.ai_events.MoveCards = MoveCards +function MoveCards:exec() + local args = self.data + local logic = self.logic + local cardsMoveStructs = {} + + for _, cardsMoveInfo in ipairs(args) do + if #cardsMoveInfo.ids > 0 then + ---@type MoveInfo[] + local infos = {} + for _, id in ipairs(cardsMoveInfo.ids) do + table.insert(infos, { + cardId = id, + fromArea = cardsMoveInfo.fromArea or self.ai.room:getCardArea(id), + fromSpecialName = cardsMoveInfo.from and logic:getPlayerById(cardsMoveInfo.from):getPileNameOfId(id), + }) + end + + cardsMoveInfo.moveInfo = infos + table.insert(cardsMoveStructs, cardsMoveInfo) + end + end + + if logic:trigger(fk.BeforeCardsMove, nil, cardsMoveStructs) then + return true + end + + for _, data in ipairs(cardsMoveStructs) do + for _, info in ipairs(data.moveInfo) do + logic:applyMoveInfo(data, info) + end + end + + logic:trigger(fk.AfterCardsMove, nil, cardsMoveStructs) +end + +function AIGameLogic:getNCards(num, from) + local cardIds = {} + for _ = 1, num do + table.insert(cardIds, 1) + end + return cardIds +end + +function AIGameLogic:moveCards(...) + return not MoveCards:new(self, ...):getBenefit() +end +AIGameLogic.moveCardTo = GameEventWrappers.moveCardTo +AIGameLogic.obtainCard = GameEventWrappers.obtainCard +AIGameLogic.drawCards = GameEventWrappers.drawCards +AIGameLogic.throwCard = GameEventWrappers.throwCard +function AIGameLogic: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 + }) + return self:drawCards(who, #card_ids, skillName) +end + +-- usecard.lua + +local UseCard = AIGameEvent:subclass("AIGameEvent.UseCard") +fk.ai_events.UseCard = UseCard +function UseCard:exec() + local ai = self.ai + local room = ai.room + local logic = self.logic + local cardUseEvent = table.unpack(self.data) + + if logic:trigger(fk.PreCardUse, room:getPlayerById(cardUseEvent.from), cardUseEvent) then + return true + end + logic:moveCardTo(cardUseEvent.card, Card.Processing, nil, fk.ReasonUse) + + for _, event in ipairs({ fk.AfterCardUseDeclared, fk.AfterCardTargetDeclared, fk.CardUsing }) do + if not cardUseEvent.toCard and #TargetGroup:getRealTargets(cardUseEvent.tos) == 0 then + break + end + + logic:trigger(event, room:getPlayerById(cardUseEvent.from), cardUseEvent) + if event == fk.CardUsing then + logic:doCardUseEffect(cardUseEvent) + end + end + + logic:trigger(fk.CardUseFinished, room:getPlayerById(cardUseEvent.from), cardUseEvent) + logic:moveCards{ + fromArea = Card.Processing, + toArea = Card.DiscardPile, + ids = Card:getIdList(cardUseEvent.card), + moveReason = fk.ReasonUse, + } +end + +function AIGameLogic:useCard(cardUseEvent) + return not UseCard:new(self, cardUseEvent):getBenefit() +end + +function AIGameLogic:deadPlayerFilter(playerIds) + local newPlayerIds = {} + for _, playerId in ipairs(playerIds) do + if self:getPlayerById(playerId):isAlive() then + table.insert(newPlayerIds, playerId) + end + end + + return newPlayerIds +end + +AIGameLogic.doCardUseEffect = GameEventWrappers.doCardUseEffect + +local CardEffect = AIGameEvent:subclass("AIGameEvent.CardEffect") +fk.ai_events.CardEffect = CardEffect +CardEffect.exec = AIParser.parseEventFunc(GameEvent.CardEffect.main) + +function AIGameLogic:doCardEffect(cardEffectEvent) + return not CardEffect:new(self, cardEffectEvent):getBenefit() +end + +function AIGameLogic:handleCardEffect(event, cardEffectEvent) + -- 不考虑闪与无懈 100%生效 + -- 闪和无懈早该重构重构了 + if event == fk.CardEffecting then + if cardEffectEvent.card.skill then + SkillEffect:new(self, function() + local skill = cardEffectEvent.card.skill + local ai = fk.ai_skills[skill.name] + if ai then + ai:onEffect(self, cardEffectEvent) + end + end, self:getPlayerById(cardEffectEvent.from), cardEffectEvent.card.skill):getBenefit() + end + end +end + +return AIGameLogic, AIGameEvent diff --git a/lua/server/ai/mcts.lua b/lua/server/ai/mcts.lua deleted file mode 100644 index 956f524b..00000000 --- a/lua/server/ai/mcts.lua +++ /dev/null @@ -1,7 +0,0 @@ --- SPDX-License-Identifier: GPL-3.0-or-later - -local MonteCarlo = class("MonteCarlo") - - - -return MonteCarlo diff --git a/lua/server/ai/parser.lua b/lua/server/ai/parser.lua new file mode 100644 index 00000000..7ad2d0d5 --- /dev/null +++ b/lua/server/ai/parser.lua @@ -0,0 +1,58 @@ +--- 用于从on_use/on_effect等函数自动生成AI推理用的模拟流程 + +---@class AIParser +local AIParser = {} + +---@type table 文件名-lines +local loaded_files = {} + +local function getLines(filename) + if loaded_files[filename] then return loaded_files[filename] end + if UsingNewCore then + if filename:startsWith("./lua") then + filename = "./packages/freekill-core/" .. filename + end + FileIO.cd("../..") + end + + local t = {} + for line in io.lines(filename) do + table.insert(t, line) + end + loaded_files[filename] = t + + if UsingNewCore then + FileIO.cd("packages/freekill-core") + end + return t +end + +local function getFunctionSource(fn) + local info = debug.getinfo(fn, "S") + local lines = getLines(info.short_src) + return table.slice(lines, info.linedefined, info.lastlinedefined + 1) +end + +-- 最简单替换:breakEvent改成return +function AIParser.parseEventFunc(fn) + local sources = getFunctionSource(fn) + local parsed = {} + for i, line in ipairs(sources) do + if i == 1 then + table.insert(parsed, "return function(self)") + else + if line:find(":breakEvent%(") then + line = "return true" + end + table.insert(parsed, line) + end + end + return load(table.concat(parsed, '\n'))() +end + +function AIParser.parseEventWrapper(wrapperFn) + local sources = getFunctionSource(wrapperFn) + print(table.concat(sources, "\n")) +end + +return AIParser diff --git a/lua/server/ai/random_ai.lua b/lua/server/ai/random_ai.lua deleted file mode 100644 index 2ef243a8..00000000 --- a/lua/server/ai/random_ai.lua +++ /dev/null @@ -1,405 +0,0 @@ --- SPDX-License-Identifier: GPL-3.0-or-later - ----@class RandomAI: AI -local RandomAI = AI:subclass("RandomAI") - ----@param self RandomAI ----@param skill ActiveSkill ----@param card? 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 - - if self.command == "PlayCard" and (not skill:canUse(player, card) or (card and player:prohibitUse(card))) then - return "" - end - - 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 = {} - - -- 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 - - 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, - } - return ret - end - return "" -end - - ----@param skill ViewAsSkill ----@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 - cancelable = cancelable or (cancelable == nil) - extra_data = extra_data or Util.DummyTable - if not skill then return "" end - - if not pattern then - precondition = skill:enabledAtPlay(player) - if not precondition then return "" end - 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 - end - for _, n in ipairs(cnames) do - local c = Fk:cloneCard(n) - precondition = c.skill:canUse(player, c, extra_data) - if precondition then break end - end - else - precondition = skill:enabledAtResponse(player, cardResponsing) and Exppattern:Parse(pattern):matchExp(skill.pattern) - 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 - 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)) - 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 - - 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 -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] - for k, v in pairs(extra_data) do - skill[k] = v - end - if skill:isInstanceOf(ViewAsSkill) then - return self:useVSSkill(skill, nil, cancelable, extra_data) - end - 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) - 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) - local player = self.player - local data = json.decode(jsonData) - local card_name = data[1] - local pattern = data[2] or card_name - local prompt = data[3] - local cancelable = data[4] - local extra_data = data[5] or Util.DummyTable - - 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 = 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) - cards = table.filter(cards, function(c) - return exp:match(c) and not player:prohibitResponse(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 - 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("h&"), Util.Id2CardMapper) - local actives = table.filter(self.player:getAllSkills(), function(s) - return s:isInstanceOf(ActiveSkill) - end) - local vss = table.filter(self.player:getAllSkills(), function(s) - return s:isInstanceOf(ViewAsSkill) - end) - table.insertTable(cards, actives) - table.insertTable(cards, vss) - - while #cards > 0 do - 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 - end - elseif sth:isInstanceOf(ActiveSkill) then - local active = sth - if math.random() > 0.30 then - local ret = RandomAI.useActiveSkill(self, active, nil) - if ret ~= "" then return ret end - end - else - local vs = sth - if math.random() > 0.20 then - local ret = self:useVSSkill(vs) - if ret ~= "" then return ret end - end - end - end - - return "" -end - --- FIXME: for smart ai -RandomAI.cb_table = random_cb - -function RandomAI:initialize(player) - AI.initialize(self, player) - self.cb_table = random_cb -end - -return RandomAI diff --git a/lua/server/ai/skill.lua b/lua/server/ai/skill.lua new file mode 100644 index 00000000..47cb76a6 --- /dev/null +++ b/lua/server/ai/skill.lua @@ -0,0 +1,82 @@ +--- 关于某个技能如何在AI中处理。 +--- +--- 相关方法分为三类,分别是如何搜索、如何计算收益、如何进行收益推理 +--- +--- 所谓搜索,就是如何确定下一步该选择哪张卡牌/哪名角色等。 +--- 默认情况下,AI选择收益最高的选项作为下一步,如果遇到了死胡同就返回考虑下一种。 +--- 所谓死胡同就是什么都不能点击,也不能点确定的状态,必须取消某些的选择。 +--- +--- 所谓的收益计算就是估算这个选项在这个技能的语境下,他大概会带来多少收益。 +--- 限于算力,我们不能编写太复杂的收益计算。默认情况下,收益可以通过推理完成, +--- 而推理的步骤需要Modder向AI给出提示。 +--- +--- 所谓的给出提示就是上面的“如何进行收益推理”。拓展可以针对点击某张卡牌或者 +--- 点击某个角色,告诉AI这么点击了可能会发生某种事件。AI根据事件以及游戏内包含的 +--- 其他技能进行计算,得出收益值。若不想让它这样计算,也可以在上一步直接指定 +--- 固定的收益值。 +--- +--- 所谓的“可能发生某种事件”大致类似GameEvent,但是内部功能大幅简化了(因为 +--- 只是用于简单的推理)。详见同文件夹下event.lua内容。 +---@class SkillAI: Object +---@field public skill ActiveSkill +local SkillAI = class("SkillAI") + +--- 收益估计 +---@param ai SmartAI +---@return integer? +function SkillAI:getEstimatedBenefit(ai) end + +--- 要返回一个结果,以及收益值 +---@param ai SmartAI +---@return any?, integer? +function SkillAI:think(ai) end + +---@param skill string +function SkillAI:initialize(skill) + self.skill = Fk.skills[skill] +end + +-- 搜索类方法:怎么走下一步? + +---@param ai SmartAI +function SkillAI:chooseInteraction(ai) end + +---@param ai SmartAI +function SkillAI:chooseCards(ai) end + +---@param ai SmartAI +---@return any, integer? +function SkillAI:chooseTargets(ai) end + +-- 流程模拟类方法:为了让AIGameLogic开心 + +--- 对触发技生效的模拟 +---@param logic AIGameLogic +---@param event Event @ TriggerEvent +---@param target ServerPlayer @ Player who triggered this event +---@param player ServerPlayer @ Player who is operating +---@param data any @ useful data of the event +function SkillAI:onTriggerUse(logic, event, target, player, data) end + +--- 对主动技生效/卡牌被使用时的模拟 +---@param logic AIGameLogic +---@param event CardUseStruct | SkillEffectEvent +function SkillAI:onUse(logic, event) end + +--- 对卡牌生效的模拟 +---@param logic AIGameLogic +---@param cardEffectEvent CardEffectEvent | SkillEffectEvent +function SkillAI:onEffect(logic, cardEffectEvent) end + +--- 最后效仿一下fk_ex故事 +---@class SkillAISpec +---@field estimated_benefit? integer|fun(self: SkillAI, ai: SmartAI): integer? +---@field think? fun(self: SkillAI, ai: SmartAI): any?, integer? +---@field choose_interaction? fun(self: SkillAI, ai: SmartAI): boolean? +---@field choose_cards? fun(self: SkillAI, ai: SmartAI): boolean? +---@field choose_targets? fun(self: SkillAI, ai: SmartAI): any, integer? +---@field on_trigger_use? fun(self: SkillAI, logic: AIGameLogic, event: Event, target: ServerPlayer?, player: ServerPlayer, data: any) +---@field on_use? fun(self: SkillAI, logic: AIGameLogic, effect: SkillEffectEvent | CardEffectEvent) +---@field on_effect? fun(self: SkillAI, logic: AIGameLogic, effect: SkillEffectEvent | CardEffectEvent) + +return SkillAI diff --git a/lua/server/ai/smart_ai.lua b/lua/server/ai/smart_ai.lua index b1020034..82fd6c21 100644 --- a/lua/server/ai/smart_ai.lua +++ b/lua/server/ai/smart_ai.lua @@ -1,41 +1,32 @@ -- SPDX-License-Identifier: GPL-3.0-or-later --[[ - 关于SmartAI: 一款参考神杀基本AI架构的AI体系。 - 该文件加载了AI常用的种种表以及实用函数等,并提供了可供拓展自定义AI逻辑的接口。 - AI的核心在于编程实现对各种交互的回应(或者说应付各种room:askForXXX), - 所以本文件的直接目的是编写出合适的函数充实smart_cb表以实现合理的答复, - 但为了实现这个目的就还要去额外实现敌友判断、收益计算等等功能。 - 为了便于各个拓展快速编写AI,还要封装一些AI判断时常用的函数。 +一套基于收益论和简易收益预测的AI框架 - 本文件包含以下内容: - 1. 基本策略代码:定义各种全局表,以及smart_cb表 - 2. 敌我相关代码:关于如何判断敌我以及更新意向值等 - 3. 十分常用的各种函数(?) - - -- TODO: 优化底层逻辑,防止AI每次操作之前都要json.decode一下。 - -- TODO: 更加详细的文档 --]] ----@class SmartAI: AI +---@class SmartAI: TrustAI ---@field private _memory table @ AI底层的空间换时间机制 ---@field public friends ServerPlayer[] @ 队友 ---@field public enemies ServerPlayer[] @ 敌人 -local SmartAI = AI:subclass("SmartAI") +local SmartAI = TrustAI:subclass("SmartAI") -- 哦,我懒得写出闪之类的,不得不继承一下,饶了我吧 ----@type table -local smart_cb = {} +AIParser = require 'lua.server.ai.parser' +SkillAI = require "lua.server.ai.skill" +TriggerSkillAI = require "lua.server.ai.trigger_skill" + +---@type table +fk.ai_events = {} +AIGameLogic, AIGameEvent = require "lua.server.ai.logic" function SmartAI:initialize(player) - AI.initialize(self, player) - self.cb_table = smart_cb - self.player = player + TrustAI.initialize(self, player) end function SmartAI:makeReply() - self._memory = {} - return AI.makeReply(self) + self._memory = setmetatable({}, { __mode = "k" }) + return TrustAI.makeReply(self) end function SmartAI:__index(k) @@ -56,201 +47,214 @@ function SmartAI:__index(k) return ret end --- AI框架中常用的模式化函数。 --- 先从表中选函数,若无则调用默认的。点点点是参数 -function SmartAI:callFromTable(func_table, default_func, key, ...) - local f = func_table[key] - if type(f) == "function" then - return f(...) - elseif type(default_func) == "function" then - return default_func(...) - else - return nil - end -end - -- 面板相关交互:对应操控手牌区、技能面板、直接选择目标的交互 -- 对应UI中的"responding"状态和"playing"状态 -- AI代码需要像实际操作UI那样完成以下几个任务: --- * 点击技能按钮(出牌阶段或者打算使用ViewAsSkill) --- * 技能如果带有interaction,则选择interaction --- * 如果需要的话点选手牌 +-- * 点击技能按钮,完成interaction与子卡选择;或者直接点可用手牌 -- * 选择目标 -- * 点确定 --- 这些步骤归结起来,就是让AI想办法返回如下定义的UseReply --- 或者返回nil表示点取消 --=================================================== ----@class UseReply ----@field card? integer|string @ string情况下是json.encode后 ----@field targets? integer[] ----@field special_skill string @ 出牌阶段空闲点使用实体卡特有 ----@field interaction_data any @ 因技能而异,一般都是nil +-- 考虑为triggerSkill设置收益修正函数 ----@param card integer|table ----@param targets? integer[] ----@param special_skill? string ----@param interaction_data? any -function SmartAI:buildUseReply(card, targets, special_skill, interaction_data) - if type(card) == "table" then card = json.encode(card) end - return { - card = card, - targets = targets or {}, - special_skill = special_skill, - interaction_data = interaction_data, +--@field ask_use_card? fun(skill: ActiveSkill, ai: SmartAI): any +--@field ask_response? fun(skill: ActiveSkill, ai: SmartAI): any + +---@type table +fk.ai_skills = {} + +---@param key string +---@param spec SkillAISpec +---@param inherit? string +function SmartAI.static:setSkillAI(key, spec, inherit) + if not fk.ai_skills[key] then + fk.ai_skills[key] = SkillAI:new(key) + end + local ai = fk.ai_skills[key] + local qsgs_wisdom_map = { + estimated_benefit = "getEstimatedBenefit", + think = "think", + choose_interaction = "chooseInteraction", + choose_cards = "chooseCards", + choose_targets = "chooseTargets", + + on_trigger_use = "onTriggerUse", + on_use = "onUse", + on_effect = "onEffect", } + if inherit then + local ai2 = fk.ai_skills[inherit] + for _, k in pairs(qsgs_wisdom_map) do + ai[k] = ai2[k] + end + end + for k, v in pairs(spec) do + local key2 = qsgs_wisdom_map[k] + if key2 then ai[key2] = type(v) == "function" and v or function() return v end end + end end --- AskForUseActiveSkill: 询问发动主动技/视为技 --- * 此处 UseReply.card 必定由 json.encode 而来 --- * 且原型为 { skill = skillName, subcards = integer[] } ----------------------------------------------------------- - ----@type table -fk.ai_active_skill = {} - -smart_cb["AskForUseActiveSkill"] = function(self, jsonData) - local data = json.decode(jsonData) - local skillName, prompt, cancelable, extra_data = table.unpack(data) - - local skill = Fk.skills[skillName] - skill._extra_data = extra_data - - local ret = self:callFromTable(fk.ai_active_skill, nil, skillName, - self, prompt, cancelable, extra_data) - - if ret then return json.encode(ret) end - if cancelable then return "" end - return RandomAI.cb_table["AskForUseActiveSkill"](self, jsonData) +--- 将spec中的键值保存到这个技能的ai中 +---@param key string +---@param spec SkillAISpec 表 +---@param inherit? string 可以直接复用某个技能已有的函数 自然spec中更加优先 +---@diagnostic disable-next-line +function SmartAI:setSkillAI(key, spec, inherit) + error("This is a static method. Please use SmartAI:setSkillAI(...)") end --- AskForUseCard: 询问使用卡牌 --- 判断函数一样返回UseReply,此时卡牌可能是integer或者string --- 为string的话肯定是由ViewAsSkill转化而来 --- 真的要考虑ViewAsSkill吗,害怕 ---------------------------------------------------------- - --- 使用牌相关——同时见于主动使用和响应式使用。 ---- 键是prompt的第一项或者牌名,优先prompt,其次name,实在不行trueName。 ----@type table -fk.ai_use_card = setmetatable({}, { - __index = function(_, k) - -- FIXME: 感觉不妥 - local c = Fk.all_card_types[k] - if not c then return nil end - if c.type == Card.TypeEquip then - return function(self, pattern, prompt, cancelable, extra_data) - local slashes = self:getCards(k, "use", extra_data) - if #slashes == 0 then return nil end - - return self:buildUseReply(slashes[1].id) +SmartAI:setSkillAI("__card_skill", { + choose_targets = function(self, ai) + local targets = ai:getEnabledTargets() + local logic = AIGameLogic:new(ai) + local val_func = function(p) + logic.benefit = 0 + logic:useCard({ + from = ai.player.id, + tos = { { p.id } }, + card = ai:getSelectedCard(), + }) + verbose("目前状况下,对%s的预测收益为%d", tostring(p), logic.benefit) + return logic.benefit + end + for _, p, val in fk.sorted_pairs(targets, val_func) do + if val > 0 then + ai:selectTarget(p, true) + return ai:doOKButton(), val + else + break end end end, + + think = function(self, ai) + local skill_name = self.skill.name + local pattern = skill_name:sub(1, #skill_name - 6) + local cards = ai:getEnabledCards(pattern) + cards = table.random(cards, math.min(#cards, 5)) --[[@as integer[] ]] + -- local cid = table.random(cards) + + local best_ret, best_val = nil, -100000 + for _, cid in ipairs(cards) do + ai:selectCard(cid, true) + local ret, val = self:chooseTargets(ai) + val = val or -100000 + if not best_ret or (best_val < val) then + best_ret, best_val = ret, val + end + ai:unSelectAll() + end + + return best_ret, best_val + end, }) -local defauld_use_card = function(self, pattern, _, cancelable, exdata) - if cancelable then return nil end - local cards = self:getCards(pattern, "use", exdata) - if #cards == 0 then return nil end - - -- TODO: 目标 - return self:buildUseReply(cards[1].id) +function SmartAI.static:setCardSkillAI(key, spec) + SmartAI:setSkillAI(key, spec, "__card_skill") end ---- 请求使用,先试图使用prompt,再试图使用card_name,最后交给随机AI -smart_cb["AskForUseCard"] = function(self, jsonData) - local data = json.decode(jsonData) - local card_name, pattern, prompt, cancelable, extra_data = table.unpack(data) +-- 等价于SmartAI:setCardSkillAI(key, spec, "__card_skill") +---@param key string +---@param spec SkillAISpec 表 +function SmartAI:setCardSkillAI(key, spec) + error("This is a static method. Please use SmartAI:setCardSkillAI(...)") +end - local prompt_prefix = prompt:split(":")[1] - local key - if fk.ai_use_card[prompt_prefix] then - key = prompt_prefix - elseif fk.ai_use_card[card_name] then - key = card_name - else - local tmp = card_name:split("__") - key = tmp[#tmp] +---@type table +fk.ai_trigger_skills = {} + +---@param spec TriggerSkillAISpec +function SmartAI.static:setTriggerSkillAI(key, spec) + if not fk.ai_trigger_skills[key] then + fk.ai_trigger_skills[key] = TriggerSkillAI:new(key) end - local ret = self:callFromTable(fk.ai_use_card, defauld_use_card, key, - self, pattern, prompt, cancelable, extra_data) - - if ret then return json.encode(ret) end - if cancelable then return "" end - return RandomAI.cb_table["AskForUseCard"](self, jsonData) -end - --- AskForResponseCard: 询问打出卡牌 --- 注意事项同前 -------------------------------------- - --- 一样的牌名或者prompt做键优先prompt ----@type table -fk.ai_response_card = {} - -local defauld_response_card = function(self, pattern, _, cancelable) - if cancelable then return nil end - local cards = self:getCards(pattern, "response") - if #cards == 0 then return nil end - return self:buildUseReply(cards[1].id) -end - --- 同前 -smart_cb["AskForResponseCard"] = function(self, jsonData) - local data = json.decode(jsonData) - local card_name, pattern, prompt, cancelable, extra_data = table.unpack(data) - - local prompt_prefix = prompt:split(":")[1] - local key - if fk.ai_response_card[prompt_prefix] then - key = prompt_prefix - elseif fk.ai_response_card[card_name] then - key = card_name - else - local tmp = card_name:split("__") - key = tmp[#tmp] + local ai = fk.ai_trigger_skills[key] + if spec.correct_func then + ai.getCorrect = spec.correct_func end - local ret = self:callFromTable(fk.ai_response_card, defauld_response_card, key, - self, pattern, prompt, cancelable, extra_data) - - if ret then return json.encode(ret) end - if cancelable then return "" end - return RandomAI.cb_table["AskForResponseCard"](self, jsonData) end --- PlayCard: 出牌阶段空闲时间点使用牌/技能 --- 老规矩得丢一个UseReply回来,但是自由度就高得多了 --- 需要完成的任务:从众多亮着的卡、技能中选一个 --- 考虑要不要用?用的话就用,否则选下个 --- 至于如何使用,可以复用askFor中的函数 ------------------------------------------------ -smart_cb["PlayCard"] = function(self) - local extra_use_data = { playing = true } - local cards = self:getCards(".", "use", extra_use_data) +--- 将spec中的键值保存到这个技能的ai中 +---@param key string +---@param spec TriggerSkillAISpec +---@diagnostic disable-next-line +function SmartAI:setTriggerSkillAI(key, spec) + error("This is a static method. Please use SmartAI:setTriggerSkillAI(...)") +end - local card_names = {} - for _, cd in ipairs(cards) do - -- TODO: 视为技 - -- 视为技对应的function一般会返回一张印出来的卡,又要纳入新的考虑范围了 - -- 不过这种根据牌名判断的逻辑而言 可能需要调用多次视为技函数了 - -- 要用好空间换时间 - table.insertIfNeed(card_names, cd.name) +---@param cid_or_skill integer|string +function SmartAI:getBasicBenefit(cid_or_skill) +end + +local function hasKey(t1, t2, key) + if (t1 and t1[key]) or (t2 and t2[key]) then return true end +end + +local function callFromTables(tab, backup, key, ...) + local fn + if tab and tab[key] then + fn = tab[key] + elseif backup and backup[key] then + fn = backup[key] end - -- TODO: 主动技 + if not fn then return end + return fn(...) +end - -- 第二步:考虑使用其中之一 - local value_func = function(str) return #str end - for _, name in fk.sorted_pairs(card_names, value_func) do - if true then - local ret = self:callFromTable(fk.ai_use_card, nil, - fk.ai_use_card[name] and name or name:split("__")[2], - self, name, "", true, extra_use_data) +function SmartAI:handleAskForUseActiveSkill() + local name = self.handler.skill_name + local current_skill = self:currentSkill() - if ret then return json.encode(ret) end + local ai + if current_skill then ai = fk.ai_skills[current_skill.name] end + if not ai then ai = fk.ai_skills[name] end + if not ai then return "" end + return ai:think(self) +end + +function SmartAI:handlePlayCard() + local card_ids = self:getEnabledCards() + local skill_ai_list = {} + for _, id in ipairs(card_ids) do + local cd = Fk:getCardById(id) + local ai = fk.ai_skills[cd.skill.name] + if ai then + table.insertIfNeed(skill_ai_list, ai) end end + for _, sname in ipairs(self:getEnabledSkills()) do + local ai = fk.ai_skills[sname] + if ai then + table.insertIfNeed(skill_ai_list, ai) + end + end + verbose("======== %s: 开始计算出牌阶段 ========", tostring(self)) + verbose("待选技能:[%s]", table.concat(table.map(skill_ai_list, function(ai) return ai.skill.name end), ", ")) + local value_func = function(ai) + if not ai then return -500 end + local val = ai:getEstimatedBenefit(self) + return val or 0 + end + + local cancel_val = math.min(-90 * (self.player:getMaxCards() - self.player:getHandcardNum()), 0) + + local best_ret, best_val + for _, ai, val in fk.sorted_pairs(skill_ai_list, value_func) do + verbose("[*] 考虑 %s (预估收益%d)", ai.skill.name, val) + if val < cancel_val then + verbose("由于预估收益小于取消的收益,不再思考") + break + end + local ret, real_val = ai:think(self) + -- if ret and ret ~= "" then return ret end + if not best_ret or (best_val < real_val) then + best_ret, best_val = ret, real_val + end + self:unSelectAll() + end + + if best_ret and best_ret ~= "" then return best_ret end return "" end @@ -265,24 +269,33 @@ end -- 函数返回true或者false即可。 ----------------------------- ----@type table -fk.ai_skill_invoke = {} +--[[ +---@type table +fk.ai_skill_invoke = { AskForLuckCard = false } -smart_cb["AskForSkillInvoke"] = function(self, jsonData) - local data = json.decode(jsonData) +function SmartAI:handleAskForSkillInvoke(data) local skillName, prompt = data[1], data[2] - local ask = fk.ai_skill_invoke[skillName] + local skill = Fk.skills[skillName] + local spec = fk.ai_skills[skillName] + local ask + if spec then + ask = spec.skill_invoke + else + ask = fk.ai_skill_invoke[skillName] + end + if type(ask) == "function" then - return ask(self, prompt) and "1" or "" + return ask(skill, self) and "1" or "" elseif type(ask) == "boolean" then return ask and "1" or "" elseif Fk.skills[skillName].frequency == Skill.Frequent then return "1" else - return RandomAI.cb_table["AskForSkillInvoke"](self, jsonData) + return math.random() < 0.5 and "1" or "" end end +--]] -- 敌友判断相关。 -- 目前才开始,做个明身份打牌的就行了。 @@ -309,38 +322,7 @@ end -- sorted_pairs 见 core/util.lua --- 合法性检测相关 --- 以后估计会单开个合法性类然后改成套壳吧 +-- 基于事件的收益推理;内置事件 --================================================= --- TODO: 这东西估计会变成一个单独模块 -local invalid_func_table = { - use = function(player, card, extra_data) - local playing = extra_data and extra_data.playing - return Player.prohibitUse(player, card) or (playing and not player:canUse(card)) - end, - response = Player.prohibitResponse, - discard = Player.prohibitDiscard, -} - ---- 根据pattern获得所有手中的牌。 ----@param pattern string ----@param validator? string @ 合法检测,须为use, response, discard之一或空 ----@param extra_data? UseExtraData @ 出牌阶段用 ----@return Card[] -function SmartAI:getCards(pattern, validator, extra_data) - validator = validator or "" - extra_data = extra_data or Util.DummyTable - local invalid_func = invalid_func_table[validator] or Util.FalseFunc - local exp = Exppattern:Parse(pattern) - - local cards = table.map(self.player:getHandlyIds(), Util.Id2CardMapper) - local ret = table.filter(cards, function(c) - return exp:match(c) and not invalid_func(self.player, c, extra_data) - end) - - -- TODO: 考虑视为技,这里可以再返回一些虚拟牌 - return ret -end - return SmartAI diff --git a/lua/server/ai/trigger_skill.lua b/lua/server/ai/trigger_skill.lua new file mode 100644 index 00000000..b364cc89 --- /dev/null +++ b/lua/server/ai/trigger_skill.lua @@ -0,0 +1,29 @@ + +--- 关于某个触发技在AI中如何影响基于事件的收益推理。 +--- +--- 类似于真正的触发技,这种技能AI也需要指定触发时机,以及在某个时机之下 +--- 如何进行收益计算。收益计算中亦可返回true,表明事件被这个技能终止, +--- 也就是不再进行后续其他技能的计算。 +--- +--- 触发技本身又会不断触发新的事件,比如刚烈反伤、反馈拿牌等。对于衍生事件 +--- 亦可进一步进行推理,但是AI会限制自己的搜索深度,所以推理结果不一定准确。 +---@class TriggerSkillAI +---@field public skill TriggerSkill +local TriggerSkillAI = class("TriggerSkillAI") + +---@param skill string +function TriggerSkillAI:initialize(skill) + self.skill = Fk.skills[skill] +end + +--- 获取触发技对收益评测的影响,通过基于logic触发更多模拟事件来模拟收益的变化 +--- +--- 返回true表示打断后续收益判断逻辑 +---@return boolean? +function TriggerSkillAI:getCorrect(logic, event, target, player, data) +end + +---@class TriggerSkillAISpec +---@field correct_func fun(self: TriggerSkillAI, logic: AIGameLogic, event: Event, target: ServerPlayer?, player: ServerPlayer, data: any): boolean? + +return TriggerSkillAI diff --git a/lua/server/ai/trust_ai.lua b/lua/server/ai/trust_ai.lua index 152e6a1d..b9f4af75 100644 --- a/lua/server/ai/trust_ai.lua +++ b/lua/server/ai/trust_ai.lua @@ -1,15 +1,47 @@ -- SPDX-License-Identifier: GPL-3.0-or-later -- Trust AI +-- 需要打出牌时,有的话就打出 +-- 需要使用闪、对自己使用无懈、酒、桃时,只要有就使用 +-- 除此之外什么都不做 ---@class TrustAI: AI local TrustAI = AI:subclass("TrustAI") -local trust_cb = {} - function TrustAI:initialize(player) AI.initialize(self, player) - self.cb_table = trust_cb +end + +function TrustAI:handleAskForUseCard(data) + local pattern = data[2] + local prompt = data[3] + + local wontuse = true + if pattern == "jink" then + wontuse = false + elseif pattern == "nullification" then + wontuse = prompt:split(":")[3] ~= tostring(self.player.id) + elseif pattern == "peach" or pattern == "peach,analeptic" then + wontuse = not prompt:startsWith("#AskForPeachesSelf") + end + if wontuse then return "" end + + local cards = self:getEnabledCards() + for _, cd in ipairs(cards) do + self:selectCard(cd, true) -- 默认按下卡牌后直接可确定 懒得管了 + return self:doOKButton() + end + return "" +end + +function TrustAI:handleAskForResponseCard(data) + -- local cancelable = data[4] -- 算了,不按取消 + local cards = self:getEnabledCards() + for _, cd in ipairs(cards) do + self:selectCard(cd, true) -- 默认按下卡牌后直接可确定 懒得管了 + return self:doOKButton() + end + return "" end return TrustAI diff --git a/lua/server/event.lua b/lua/server/event.lua index 78fe463a..80cc9194 100644 --- a/lua/server/event.lua +++ b/lua/server/event.lua @@ -48,6 +48,7 @@ fk.FinishJudge = 27 fk.RoundStart = 28 fk.RoundEnd = 29 +fk.AfterRoundEnd = 85 fk.BeforeTurnOver = 79 fk.TurnedOver = 30 @@ -126,7 +127,7 @@ fk.AfterSkillEffect = 82 -- 83 = PreTurnStart -- 84 = AfterTurnEnd --- 85 = xxx +-- 85 = AfterRoundEnd -- 86 = AfterPhaseEnd fk.AreaAborted = 87 diff --git a/lua/server/events/gameflow.lua b/lua/server/events/gameflow.lua index 5371c40f..11989c4c 100644 --- a/lua/server/events/gameflow.lua +++ b/lua/server/events/gameflow.lua @@ -80,11 +80,11 @@ function DrawInitial:main() return end - room:notifyMoveFocus(room.alive_players, "AskForLuckCard") - local request = Request:new("AskForSkillInvoke", room.alive_players) + local request = Request:new(room.alive_players, "AskForSkillInvoke") for _, p in ipairs(room.alive_players) do request:setData(p, { "AskForLuckCard", "#AskForLuckCard:::" .. room.settings.luckTime }) end + request.focus_text = "AskForLuckCard" request.luck_data = luck_data request.accept_cancel = true request:ask() @@ -101,28 +101,22 @@ 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() + local currentPlayer + + while true do + GameEvent.Turn:create(room.current):exec() if room.game_finished then break end - local changingData = { from = room.current, to = room.current:getNextAlive(true, nil, true), skipRoundPlus = false } + local changingData = { from = room.current, to = room.current.next, 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 + local nextTurnOwner = changingData.to + if room.current.seat > nextTurnOwner.seat and not changingData.skipRoundPlus then + break else - room.current = nextAlive + room.current = nextTurnOwner end - until p.seat >= (nextTurnOwner or p:getNextAlive(true, nil, true)).seat and not skipRoundPlus + end end function Round:main() @@ -153,7 +147,8 @@ function Round:main() logic:trigger(fk.RoundStart, room.current) self:action() - logic:trigger(fk.RoundEnd, p) + logic:trigger(fk.RoundEnd, room.current) + logic:trigger(fk.AfterRoundEnd, room.current) end function Round:clear() @@ -188,7 +183,10 @@ local Turn = GameEvent:subclass("GameEvent.Turn") function Turn:prepare() local room = self.room local logic = room.logic - local player = room.current + local player = self.data[1]---@type ServerPlayer + if self.data[2] == nil then self.data[2] = {} end + local data = self.data[2]---@type TurnStruct + data.reason = data.reason or "game_rule" if player.rest > 0 and player.rest < 999 then room:setPlayerRest(player, player.rest - 1) @@ -208,21 +206,24 @@ function Turn:prepare() return true end - return logic:trigger(fk.BeforeTurnStart, player) + return logic:trigger(fk.BeforeTurnStart, player, data) end function Turn:main() local room = self.room - room.current.phase = Player.PhaseNone - room.logic:trigger(fk.TurnStart, room.current) - room.current.phase = Player.NotActive - room.current:play() + local player = self.data[1]---@type ServerPlayer + local data = self.data[2]---@type TurnStruct + player.phase = Player.PhaseNone + room.logic:trigger(fk.TurnStart, player, data) + player.phase = Player.NotActive + player:play(data.phase_table) end function Turn:clear() local room = self.room + local current = self.data[1]---@type ServerPlayer + local data = self.data[2]---@type TurnStruct - local current = room.current local logic = room.logic if self.interrupted then if current.phase ~= Player.NotActive then @@ -239,8 +240,8 @@ function Turn:clear() end current.phase = Player.PhaseNone - logic:trigger(fk.TurnEnd, current, nil, self.interrupted) - logic:trigger(fk.AfterTurnEnd, current, nil, self.interrupted) + logic:trigger(fk.TurnEnd, current, data, self.interrupted) + logic:trigger(fk.AfterTurnEnd, current, data, self.interrupted) current.phase = Player.NotActive room:setTag("endTurn", false) @@ -275,7 +276,7 @@ function Phase:main() local room = self.room local logic = room.logic - local player = self.data[1] ---@type Player + local player = self.data[1] ---@type ServerPlayer if not logic:trigger(fk.EventPhaseStart, player) then if player.phase ~= Player.NotActive then logic:trigger(fk.EventPhaseProceeding, player) @@ -322,7 +323,7 @@ function Phase:main() } room.logic:trigger(fk.DrawNCards, player, data) if not player._phase_end then - room:drawCards(player, data.n, "game_rule") + room:drawCards(player, data.n, "phase_draw") end room.logic:trigger(fk.AfterDrawNCards, player, data) end, @@ -332,8 +333,8 @@ function Phase:main() 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) + + local result = Request:new(player, "PlayCard"):getResult(player) if result == "" then break end local useResult = room:handleUseCardReply(player, result) @@ -354,7 +355,7 @@ function Phase:main() ) - player:getMaxCards() room:broadcastProperty(player, "MaxCards") if discardNum > 0 then - room:askForDiscard(player, discardNum, discardNum, false, "game_rule", false) + room:askForDiscard(player, discardNum, discardNum, false, "phase_discard", false) end end, [Player.Finish] = function() diff --git a/lua/server/events/judge.lua b/lua/server/events/judge.lua index 23602633..27346f72 100644 --- a/lua/server/events/judge.lua +++ b/lua/server/events/judge.lua @@ -100,29 +100,17 @@ end ---@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 move1 = {} ---@type CardsMoveInfo + move1.ids = { card:getEffectiveId() } + move1.from = self.owner_map[card:getEffectiveId()] + move1.toArea = Card.Processing + move1.moveReason = fk.ReasonJustMove + move1.skillName = skillName + self:moveCards(move1) local oldJudge = judge.card judge.card = card - local rebyre = judge.retrial_by_response - judge.retrial_by_response = player self:sendLog{ type = "#ChangedJudge", @@ -134,16 +122,18 @@ function JudgeEventWrappers:retrial(card, player, judge, skillName, exchange) Fk:filterCard(judge.card.id, judge.who, judge) - exchange = exchange and not player.dead + if self:getCardArea(oldJudge) == Card.Processing then + 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 + 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 - self:moveCards(move2) end return { Judge, JudgeEventWrappers } diff --git a/lua/server/events/movecard.lua b/lua/server/events/movecard.lua index 6ae5e86d..1d0b2805 100644 --- a/lua/server/events/movecard.lua +++ b/lua/server/events/movecard.lua @@ -397,16 +397,17 @@ end ---@param cards integer|integer[] @ 移动的牌 ---@param skillName? string @ 技能名 ---@param convert? boolean @ 是否可以替换装备(默认可以) ----@param proposer? ServerPlayer @ 操作者 +---@param proposer? ServerPlayer | integer @ 操作者 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} + proposer = type(proposer) == "number" and self:getPlayerById(proposer) or proposer + local proposerId = proposer and proposer.id or nil 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}) diff --git a/lua/server/events/pindian.lua b/lua/server/events/pindian.lua index 0d4d4ad9..0cd7ab4d 100644 --- a/lua/server/events/pindian.lua +++ b/lua/server/events/pindian.lua @@ -42,17 +42,19 @@ function Pindian:main() table.insert(targets, pindianData.from) pindianData.from.request_data = json.encode(data) else - local _pindianCard = pindianData.fromCard - local pindianCard = _pindianCard:clone(_pindianCard.suit, _pindianCard.number) - pindianCard:addSubcard(_pindianCard.id) + if not pindianData._fromCard then + local _pindianCard = pindianData.fromCard + local pindianCard = _pindianCard:clone(_pindianCard.suit, _pindianCard.number) + pindianCard:addSubcard(_pindianCard.id) - pindianData.fromCard = pindianCard - pindianData._fromCard = _pindianCard + pindianData.fromCard = pindianCard + pindianData._fromCard = _pindianCard + end table.insert(moveInfos, { - ids = { _pindianCard.id }, - from = room.owner_map[_pindianCard.id], - fromArea = room:getCardArea(_pindianCard.id), + ids = { pindianData._fromCard.id }, + from = room.owner_map[pindianData._fromCard.id], + fromArea = room:getCardArea(pindianData._fromCard.id), toArea = Card.Processing, moveReason = fk.ReasonPut, skillName = pindianData.reason, @@ -61,17 +63,19 @@ function Pindian:main() end for _, to in ipairs(pindianData.tos) do if pindianData.results[to.id] and pindianData.results[to.id].toCard then - local _pindianCard = pindianData.results[to.id].toCard - local pindianCard = _pindianCard:clone(_pindianCard.suit, _pindianCard.number) - pindianCard:addSubcard(_pindianCard.id) + if not pindianData.results[to.id]._toCard then + local _pindianCard = pindianData.results[to.id].toCard + local pindianCard = _pindianCard:clone(_pindianCard.suit, _pindianCard.number) + pindianCard:addSubcard(_pindianCard.id) - pindianData.results[to.id].toCard = pindianCard - pindianData.results[to.id]._toCard = _pindianCard + pindianData.results[to.id].toCard = pindianCard + pindianData.results[to.id]._toCard = _pindianCard + end table.insert(moveInfos, { - ids = { _pindianCard.id }, - from = room.owner_map[_pindianCard.id], - fromArea = room:getCardArea(_pindianCard.id), + ids = { pindianData.results[to.id]._toCard.id }, + from = room.owner_map[pindianData.results[to.id]._toCard.id], + fromArea = room:getCardArea(pindianData.results[to.id]._toCard.id), toArea = Card.Processing, moveReason = fk.ReasonPut, skillName = pindianData.reason, @@ -79,48 +83,51 @@ function Pindian:main() }) else table.insert(targets, to) - to.request_data = json.encode(data) end end - room:notifyMoveFocus(targets, "AskForPindian") - room:doBroadcastRequest("AskForUseActiveSkill", targets) + if #targets ~= 0 then + local req = Request:new(targets, "AskForUseActiveSkill") + for _, p in ipairs(targets) do req:setData(p, data) end + req.focus_text = "AskForPindian" - for _, p in ipairs(targets) do - local _pindianCard - if p.reply_ready then - local replyCard = json.decode(p.client_reply).card - _pindianCard = Fk:getCardById(json.decode(replyCard).subcards[1]) - else - _pindianCard = Fk:getCardById(p:getCardIds(Player.Hand)[1]) + for _, p in ipairs(targets) do + local _pindianCard + local result = req:getResult(p) + if result ~= "" then + local replyCard = result.card + _pindianCard = Fk:getCardById(json.decode(replyCard).subcards[1]) + else + _pindianCard = Fk:getCardById(p:getCardIds(Player.Hand)[1]) + end + + local pindianCard = _pindianCard:clone(_pindianCard.suit, _pindianCard.number) + pindianCard:addSubcard(_pindianCard.id) + + if p == pindianData.from then + pindianData.fromCard = pindianCard + pindianData._fromCard = _pindianCard + else + pindianData.results[p.id] = pindianData.results[p.id] or {} + pindianData.results[p.id].toCard = pindianCard + pindianData.results[p.id]._toCard = _pindianCard + end + + table.insert(moveInfos, { + ids = { _pindianCard.id }, + from = p.id, + toArea = Card.Processing, + moveReason = fk.ReasonPut, + skillName = pindianData.reason, + moveVisible = true, + }) + + room:sendLog{ + type = "#ShowPindianCard", + from = p.id, + arg = _pindianCard:toLogString(), + } end - - local pindianCard = _pindianCard:clone(_pindianCard.suit, _pindianCard.number) - pindianCard:addSubcard(_pindianCard.id) - - if p == pindianData.from then - pindianData.fromCard = pindianCard - pindianData._fromCard = _pindianCard - else - pindianData.results[p.id] = pindianData.results[p.id] or {} - pindianData.results[p.id].toCard = pindianCard - pindianData.results[p.id]._toCard = _pindianCard - end - - table.insert(moveInfos, { - ids = { _pindianCard.id }, - from = p.id, - toArea = Card.Processing, - moveReason = fk.ReasonPut, - skillName = pindianData.reason, - moveVisible = true, - }) - - room:sendLog{ - type = "#ShowPindianCard", - from = p.id, - card = { _pindianCard.id }, - } end room:moveCards(table.unpack(moveInfos)) diff --git a/lua/server/events/skill.lua b/lua/server/events/skill.lua index be30874f..95ce3a8d 100644 --- a/lua/server/events/skill.lua +++ b/lua/server/events/skill.lua @@ -110,7 +110,7 @@ function SkillEventWrappers:handleAddLoseSkills(player, skill_names, source_skil if #skill_names == 0 then return end local losts = {} ---@type boolean[] local triggers = {} ---@type Skill[] - local lost_piles = {} ---@type integer[] + -- 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) @@ -132,11 +132,11 @@ function SkillEventWrappers:handleAddLoseSkills(player, skill_names, source_skil 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 + -- 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 @@ -169,19 +169,26 @@ function SkillEventWrappers:handleAddLoseSkills(player, skill_names, source_skil 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]) + if losts[i] then + local skill = triggers[i] + skill:onLose(player) + self.logic:trigger(fk.EventLoseSkill, player, skill) + else + local skill = triggers[i] + self.logic:trigger(fk.EventAcquireSkill, player, skill) + skill:onAcquire(player) + end end end - if #lost_piles > 0 then - self:moveCards({ - ids = lost_piles, - from = player.id, - toArea = Card.DiscardPile, - moveReason = fk.ReasonPutIntoDiscardPile, - }) - 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 3a8a9e27..cf45b277 100644 --- a/lua/server/events/usecard.lua +++ b/lua/server/events/usecard.lua @@ -858,6 +858,9 @@ function UseCardEventWrappers:handleCardEffect(event, cardEffectEvent) extra_data = { useEventId = parentUseEvent.id, effectTo = cardEffectEvent.to } end end + if #players > 0 and cardEffectEvent.card.trueName == "nullification" then + self:animDelay(2) + end local use = self:askForNullification(players, nil, nil, prompt, true, extra_data, cardEffectEvent) if use then use.toCard = cardEffectEvent.card diff --git a/lua/server/gameevent.lua b/lua/server/gameevent.lua index 0c9fc5a9..e7892e82 100644 --- a/lua/server/gameevent.lua +++ b/lua/server/gameevent.lua @@ -181,7 +181,7 @@ local function bin_search(events, from, to, n, func) end -- 从某个区间中,找出类型符合且符合func函数检测的至多n个事件。 ----@param eventType integer @ 要查找的事件类型 +---@param eventType GameEvent @ 要查找的事件类型 ---@param n integer @ 最多找多少个 ---@param func fun(e: GameEvent): boolean? @ 过滤用的函数 ---@param endEvent? GameEvent @ 区间终止点,默认为本事件结束 diff --git a/lua/server/gamelogic.lua b/lua/server/gamelogic.lua index cdecad6b..046619a0 100644 --- a/lua/server/gamelogic.lua +++ b/lua/server/gamelogic.lua @@ -132,29 +132,18 @@ function GameLogic:chooseGenerals() local nonlord = room:getOtherPlayers(lord, true) local generals = table.random(room.general_pile, #nonlord * generalNum) + + local req = Request:new(nonlord, "AskForGeneral") for i, p in ipairs(nonlord) do local arg = table.slice(generals, (i - 1) * generalNum + 1, i * generalNum + 1) - p.request_data = json.encode{ arg, n } - p.default_reply = table.random(arg, n) + req:setData(p, { arg, n }) + req:setDefaultReply(p, table.random(arg, n)) end - room:notifyMoveFocus(nonlord, "AskForGeneral") - room:doBroadcastRequest("AskForGeneral", nonlord) - for _, p in ipairs(nonlord) do - local general, deputy - if p.general == "" and p.reply_ready then - local general_ret = json.decode(p.client_reply) - general = general_ret[1] - deputy = general_ret[2] - else - general = p.default_reply[1] - deputy = p.default_reply[2] - end - room:findGeneral(general) - room:findGeneral(deputy) + local result = req:getResult(p) + local general, deputy = result[1], result[2] room:prepareGeneral(p, general, deputy) - p.default_reply = "" end room:askForChooseKingdom(nonlord) @@ -233,7 +222,9 @@ function GameLogic:attachSkillToPlayers() return end - room:handleAddLoseSkills(player, skillName, nil, false) + room:handleAddLoseSkills(player, skillName, nil, false, true) + self:trigger(fk.EventAcquireSkill, player, skill) + skill:onAcquire(player, true) end for _, p in ipairs(room.alive_players) do local skills = Fk.generals[p.general].skills @@ -278,6 +269,8 @@ function GameLogic:action() while true do execGameEvent(GameEvent.Round) if room.game_finished then break end + if table.every(room.players, function(p) return p.dead and p.rest == 0 end) then room:gameOver("") end + room.current = room.players[1] end end @@ -570,9 +563,10 @@ function GameLogic:getMostRecentEvent(eventType) end --- 如果当前事件刚好是技能生效事件,就返回那个技能名,否则返回空串。 +---@return string|nil function GameLogic:getCurrentSkillName() local skillEvent = self:getCurrentEvent() - local ret = "" + local ret = nil if skillEvent.event == GameEvent.SkillEffect then local _, _, _skill = table.unpack(skillEvent.data) local skill = _skill.main_skill and _skill.main_skill or _skill @@ -605,7 +599,7 @@ function GameLogic:getEventsOfScope(eventType, n, func, scope) end -- 在指定历史范围中找符合条件的事件(逆序) ----@param eventType integer @ 要查找的事件类型 +---@param eventType GameEvent @ 要查找的事件类型 ---@param func fun(e: GameEvent): boolean @ 过滤用的函数 ---@param n integer @ 最多找多少个 ---@param end_id integer @ 查询历史范围:从最后的事件开始逆序查找直到id为end_id的事件(不含) diff --git a/lua/server/mark_enum.lua b/lua/server/mark_enum.lua index dd6eba1d..a74aca99 100644 --- a/lua/server/mark_enum.lua +++ b/lua/server/mark_enum.lua @@ -29,7 +29,7 @@ MarkEnum.BypassTimesLimitTo = "BypassTimesLimitTo" MarkEnum.BypassDistancesLimitTo = "BypassDistancesLimitTo" ---非锁定技失效 MarkEnum.UncompulsoryInvalidity = "UncompulsoryInvalidity" ----失效技能表 +---失效技能表(用``Room:addTableMark``和``Room:removeTableMark``控制) MarkEnum.InvalidSkills = "InvalidSkills" ---不可明置(值为表,m - 主将, d - 副将) MarkEnum.RevealProhibited = "RevealProhibited" diff --git a/lua/server/network.lua b/lua/server/network.lua index 29978dd1..3611bec0 100644 --- a/lua/server/network.lua +++ b/lua/server/network.lua @@ -1,19 +1,23 @@ ---@class Request : Object ---@field public room Room ---@field public players ServerPlayer[] ----@field public n integer @ n个人做出回复后,询问结束 ----@field public accept_cancel? boolean @ 是否将取消也算作是收到回复 +---@field public n integer @ 产生n个winner后,询问直接结束 +---@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 public default_reply table @ 玩家id - 默认回复内容 默认空串 +---@field public send_encode boolean? @ 是否需要对data使用json.encode,默认true +---@field public receive_decode boolean? @ 是否需要对reply使用json.decode,默认true ---@field private send_success table @ 数据是否发送成功,不成功的后面全部视为AI ---@field public result table @ 玩家id - 回复内容 nil表示完全未回复 +---@field public winners ServerPlayer[] @ 按肯定回复先后顺序排序 由于有概率所有人烧条 可能会空 ---@field public luck_data any? @ 是否是询问手气卡 TODO: 有需求的话把这个通用化一点 ---@field private pending_requests table @ 一控多时暂存的请求 +---@field private _asked boolean? @ 是否询问过了 +---@field public focus_players? ServerPlayer[] @ 要moveFocus的玩家们 默认参与者 +---@field public focus_text? string @ 要moveFocus的文字 默认self.command local Request = class("Request") -- TODO: 懒得思考了 @@ -23,9 +27,10 @@ local Request = class("Request") -- 若还能再用一次,那就重新发Request并继续等 ---@param command string ----@param players ServerPlayer[] +---@param players ServerPlayer|ServerPlayer[] ---@param n? integer -function Request:initialize(command, players, n) +function Request:initialize(players, command, n) + if (not players[1]) and players.class then players = { players } end assert(#players > 0) self.command = command self.players = players @@ -36,29 +41,43 @@ function Request:initialize(command, players, n) 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.send_encode = true + self.receive_decode = true -- 除了几个特殊字符串之外都decode self.pending_requests = setmetatable({}, { __mode = "k" }) self.send_success = setmetatable({}, { __mode = "k" }) self.result = {} + self.winners = {} end function Request:__tostring() - return "" + return string.format("", self.command) end +---@param player ServerPlayer +---@param data any function Request:setData(player, data) self.data[player.id] = data end +---@param player ServerPlayer +---@param data any @ 注意不要json.encode function Request:setDefaultReply(player, data) self.default_reply[player.id] = data end +--- 获取本次Request中此人的回复,若还未询问过,那么先询问 +--- * : 成功发出回复 获取的是decode后的回复 +--- * "" (空串): 发出了“取消” 或者烧完了绳子 反正就是取消 +---@param player ServerPlayer +---@return any +function Request:getResult(player) + if not self._asked then self:ask() end + return self.result[player.id] +end + -- 将相应请求数据发给player -- 不能向thinking中的玩家发送,这种情况下暂存起来等待收到答复后 ---@param player ServerPlayer @@ -86,7 +105,7 @@ function Request:_sendPacket(player) -- 发送请求数据并将控制者标记为烧条中 local jsonData = self.data[player.id] - if self.send_json then jsonData = json.encode(jsonData) end + if self.send_encode 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) @@ -142,34 +161,21 @@ function Request:_checkReply(player, use_ai) 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 + player.ai.data = self.data[player.id] + reply = Pcall(player.ai.makeReply, player.ai) else -- 还没轮到AI呢,所以需要标记为未答复 reply = "__notready" end end + if reply == '' then reply = '__cancel' 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() + if self._asked then return end + local room = self.room -- 0. 设置计时器,防止因无人回复一直等下去 room.room:setRequestTimer(self.timeout * 1000 + 500) @@ -183,6 +189,10 @@ function Request:ask() p.serverplayer:setThinking(false) end + -- 发送focus + room:notifyMoveFocus(self.focus_players or self.players, self.focus_text or self.command, + math.floor(self.timeout * 1000)) + -- 1. 向所有人发送询问请求 for _, p in ipairs(players) do self:_sendPacket(p) @@ -191,10 +201,9 @@ function Request:ask() -- 2. 进入循环等待,结束条件为已有n个回复或者超时或者有人点了 -- 若很多人都取消了导致最多回复数达不到n了,那么也结束 local replied_players = 0 - local ready_players = 0 while true do local changed = false - -- 判断1:若投降则直接结束全部询问,若超时则踢掉所有人类玩家(这样AI还可计算) + -- 若投降则直接结束全部询问,若超时则踢掉所有人类玩家(让AI还可计算) if room.hasSurrendered then break end local elapsed = os.time() - currentTime if self.timeout - elapsed <= 0 or resume_reason == "request_timer" then @@ -205,22 +214,23 @@ function Request:ask() end end + -- 若players中只剩人机,那么允许人机进行计算 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 + -- 轮询所有参与回答的玩家,如果作出了答复,那么就把他从名单移除; + -- 然后如果作出的是“肯定”答复,那么添加到winner里面 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 + if reply ~= "__cancel" and (self.receive_decode and not use_ai) then reply = json.decode(reply) end self.result[player.id] = reply @@ -229,35 +239,39 @@ function Request:ask() 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 + table.insert(self.winners, player) + if #self.winners >= self.n then + -- winner数量已经足够,剩下的人不用算了 + for _, p in ipairs(players) do -- 避免触发后续的烧条检测 if self.result[p.id] == nil then self.result[p.id] = "__failed_in_race" end end + players = {} -- 清空参与者名单 break -- 注意外面还有一层循环 end end end end - if #players + ready_players < self.n then break end - if ready_players >= self.n then break end + if #players == 0 then break end + if #self.winners >= self.n then break end -- 防止万一,如果AI算完后还是有机器人notready的话也别等了 -- 不然就永远别想被唤醒了 if self.ai_start_time then break end - -- 需要等待呢,等待被唤醒吧 + -- 需要等待呢,等待被唤醒吧,唤醒后继续下一次轮询检测 if not changed then resume_reason = coroutine.yield("__handleRequest") end end room.room:destroyRequestTimer() - self:finish() + self:_finish() + + self._asked = true end local function surrenderCheck(room) @@ -283,15 +297,19 @@ local function surrenderCheck(room) end -- 善后工作,主要是result规范化、投降检测等 -function Request:finish() +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) + -- 这个什么timewaste_count也该扔了 + if self.result[p.id] == "__failed_in_race" then + p:doNotify("CancelRequest", "") + self.result[p.id] = self.default_reply[p.id] or "" + end if self.result[p.id] == nil then - self.result[p.id] = self.default_reply[p.id] + self.result[p.id] = self.default_reply[p.id] or "" p._timewaste_count = p._timewaste_count + 1 if p._timewaste_count >= 3 and p.serverplayer:getState() == fk.Player_Online then p._timewaste_count = 0 @@ -301,10 +319,7 @@ function Request:finish() 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 + self.result[p.id] = (not self.accept_cancel) and self.default_reply[p.id] or "" end end room.last_request = self @@ -312,7 +327,7 @@ function Request:finish() 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 + local to_delay = 800 - (os.getms() - self.ai_start_time) / 1000 room:delay(to_delay) break end diff --git a/lua/server/room.lua b/lua/server/room.lua index e3244e8f..3000b621 100644 --- a/lua/server/room.lua +++ b/lua/server/room.lua @@ -15,18 +15,20 @@ ---@field public game_finished boolean @ 游戏是否已经结束 ---@field public tag table @ Tag清单,其实跟Player的标记是差不多的东西 ---@field public general_pile string[] @ 武将牌堆,这是可用武将名的数组 +---@field public disabled_packs string[] @ 未开启的扩展包名(是小包名,不是大包名) ---@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之用 +---@field public current_cost_skill TriggerSkill? @ AI用 local Room = AbstractRoom:subclass("Room") -- load classes used by the game Request = require "server.network" GameEvent = require "server.gameevent" -local GameEventWrappers = require "lua/server/events" +GameEventWrappers = require "lua/server/events" Room:include(GameEventWrappers) GameLogic = require "server.gamelogic" ServerPlayer = require "server.serverplayer" @@ -508,10 +510,11 @@ function Room:setDeputyGeneral(player, general) self:notifyProperty(player, player, "deputyGeneral") end +--- 为角色设置武将,并从武将池中抽出,若有隐匿技变为隐匿将。注意此时不会进行选择势力,请随后自行处理 ---@param player ServerPlayer ----@param general string ----@param deputy string ----@param broadcast boolean|nil +---@param general string @ 主将名 +---@param deputy? string @ 副将名 +---@param broadcast? boolean @ 是否公示,默认否 function Room:prepareGeneral(player, general, deputy, broadcast) self:findGeneral(general) self:findGeneral(deputy) @@ -597,9 +600,9 @@ end function Room:doRequest(player, command, jsonData, wait) -- 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 + local request = Request:new(player, command) + request.send_encode = false -- 因为参数已经json.encode过了,该死的兼容性 + request.receive_decode = false request.accept_cancel = true request:setData(player, jsonData) request:ask() @@ -613,9 +616,9 @@ end function Room:doBroadcastRequest(command, players, jsonData) -- fk.qCritical("Room:doBroadcastRequest is deprecated!") players = players or self.players - local request = Request:new(command, players) - request.send_json = false -- 因为参数已经json.encode过了 - request.receive_json = false + local request = Request:new(players, command) + request.send_encode = false -- 因为参数已经json.encode过了 + request.receive_decode = false request.accept_cancel = true for _, p in ipairs(players) do request:setData(p, jsonData or p.request_data) @@ -635,14 +638,14 @@ end function Room:doRaceRequest(command, players, jsonData) -- fk.qCritical("Room:doRaceRequest is deprecated!") players = players or self.players - local request = Request:new(command, players, 1) - request.send_json = false -- 因为参数已经json.encode过了 - request.receive_json = false + local request = Request:new(players, command, 1) + request.send_encode = false -- 因为参数已经json.encode过了 + request.receive_decode = false for _, p in ipairs(players) do request:setData(p, jsonData or p.request_data) end request:ask() - return request:getWinners()[1] + return request.winners[1] end --- 延迟一段时间。 @@ -656,6 +659,15 @@ function Room:delay(ms) coroutine.yield("__handleRequest", ms) end +--- 延迟一段时间。界面上会显示所有人读条了。注意这个只能延迟多少秒。 +---@param sec integer @ 要延迟的秒数 +function Room:animDelay(sec) + local req = Request:new(self.alive_players, "EmptyRequest") + req.focus_text = '' + req.timeout = sec + req:ask() +end + --- 向多名玩家告知一次移牌行为。 ---@param players? ServerPlayer[] @ 要被告知的玩家列表,默认为全员 ---@param card_moves CardsMoveStruct[] @ 要告知的移牌信息列表 @@ -691,7 +703,8 @@ end --- 形象点说,就是在那些玩家下面显示一个“弃牌 思考中...”之类的烧条提示。 ---@param players ServerPlayer | ServerPlayer[] @ 要获得焦点的一名或者多名角色 ---@param command string @ 烧条的提示文字 -function Room:notifyMoveFocus(players, command) +---@param timeout integer? @ focus的烧条时长 +function Room:notifyMoveFocus(players, command, timeout) if (players.class) then players = {players} end @@ -712,7 +725,8 @@ function Room:notifyMoveFocus(players, command) self:doBroadcastNotify("MoveFocus", json.encode{ ids, - command + command, + timeout }) end @@ -896,18 +910,20 @@ function Room:askForUseActiveSkill(player, skill_name, prompt, cancelable, extra end local command = "AskForUseActiveSkill" - self:notifyMoveFocus(player, extra_data.skillName or skill_name) -- for display skill name instead of command name local data = {skill_name, prompt, cancelable, extra_data} Fk.currentResponseReason = extra_data.skillName - local result = self:doRequest(player, command, json.encode(data)) + local req = Request:new(player, command) + req:setData(player, data) + req.focus_text = extra_data.skillName or skill_name + local result = req:getResult(player) Fk.currentResponseReason = nil if result == "" then return false end - data = json.decode(result) + data = result local card = data.card local targets = data.targets local card_data = json.decode(card) @@ -969,7 +985,7 @@ function Room:askForDiscard(player, minNum, maxNum, includeEquip, skillName, can return false end end - if skillName == "game_rule" then + if skillName == "phase_discard" then status_skills = Fk:currentRoom().status_skills[MaxCardsSkill] or Util.DummyTable for _, skill in ipairs(status_skills) do if skill:excludeFrom(player, card) then @@ -1213,7 +1229,7 @@ end ---@param minNum? integer @ 最少交出的卡牌数,默认0 ---@param maxNum? integer @ 最多交出的卡牌数,默认所有牌 ---@param prompt? string @ 询问提示信息 ----@param expand_pile? string @ 可选私人牌堆名称,如要分配你武将牌上的牌请填写 +---@param expand_pile? string|integer[] @ 可选私人牌堆名称,如要分配你武将牌上的牌请填写 ---@param skipMove? boolean @ 是否跳过移动。默认不跳过 ---@param single_max? integer|table @ 限制每人能获得的最大牌数。输入整数或(以角色id为键以整数为值)的表 ---@return table @ 返回一个表,键为角色id转字符串,值为分配给其的牌id数组 @@ -1346,7 +1362,7 @@ function Room:returnToGeneralPile(g, position) end --- 抽特定名字的武将(抽了就没了) ----@param name string @ 武将name,如找不到则查找truename,再找不到则返回nil +---@param name string? @ 武将name,如找不到则查找truename,再找不到则返回nil ---@return string? @ 抽出的武将名 function Room:findGeneral(name) if not Fk.generals[name] then return nil end @@ -1384,25 +1400,18 @@ end ---@return string|string[] @ 选择的武将 function Room:askForGeneral(player, generals, n, noConvert) local command = "AskForGeneral" - self:notifyMoveFocus(player, command) n = n or 1 if #generals == n then return n == 1 and generals[1] or generals end local defaultChoice = table.random(generals, n) - if (player.serverplayer:getState() == fk.Player_Online) then - local result = self:doRequest(player, command, json.encode{ generals, n, noConvert }) - local choices - if result == "" then - choices = defaultChoice - else - choices = json.decode(result) - end - if #choices == 1 then return choices[1] end - return choices - end - - return n == 1 and defaultChoice[1] or defaultChoice + local req = Request:new(player, command) + local data = { generals, n, noConvert } + req:setData(player, data) + req:setDefaultReply(player, defaultChoice) + local choices = req:getResult(player) + if #choices == 1 then return choices[1] end + return choices end --- 询问玩家若为神将、双势力需选择一个势力。 @@ -1414,7 +1423,9 @@ function Room:askForChooseKingdom(players) end) if #specialKingdomPlayers > 0 then - local choiceMap = {} + local req = Request:new(specialKingdomPlayers, "AskForChoice") + req.focus_text = "AskForKingdom" + req.receive_decode = false for _, p in ipairs(specialKingdomPlayers) do local allKingdoms = {} local curGeneral = Fk.generals[p.general] @@ -1424,25 +1435,13 @@ function Room:askForChooseKingdom(players) allKingdoms = Fk:getKingdomMap(p.kingdom) end if #allKingdoms > 0 then - choiceMap[p.id] = allKingdoms - - local data = json.encode({ allKingdoms, allKingdoms, "AskForKingdom", "#ChooseInitialKingdom" }) - p.request_data = data + req:setData(p, { allKingdoms, allKingdoms, "AskForKingdom", "#ChooseInitialKingdom" }) + req:setDefaultReply(p, allKingdoms[1]) end end - self:notifyMoveFocus(players, "AskForKingdom") - self:doBroadcastRequest("AskForChoice", specialKingdomPlayers) - for _, p in ipairs(specialKingdomPlayers) do - local kingdomChosen - if p.reply_ready then - kingdomChosen = p.client_reply - else - kingdomChosen = choiceMap[p.id][1] - end - - p.kingdom = kingdomChosen + p.kingdom = req:getResult(p) self:notifyProperty(p, p, "kingdom") end end @@ -1458,9 +1457,10 @@ end function Room:askForCardChosen(chooser, target, flag, reason, prompt) local command = "AskForCardChosen" prompt = prompt or "" - self:notifyMoveFocus(chooser, command) local data = {target.id, flag, reason, prompt} - local result = self:doRequest(chooser, command, json.encode(data)) + local req = Request:new(chooser, command) + req:setData(chooser, data) + local result = req:getResult(chooser) if result == "" then local areas = {} @@ -1478,8 +1478,6 @@ function Room:askForCardChosen(chooser, target, flag, reason, prompt) end if #handcards == 0 then return end result = handcards[math.random(1, #handcards)] - else - result = tonumber(result) end if result == -1 then @@ -1507,18 +1505,20 @@ function Room:askForPoxi(player, poxi_type, data, extra_data, cancelable) if not poxi then return {} end local command = "AskForPoxi" - self:notifyMoveFocus(player, poxi_type) - local result = self:doRequest(player, command, json.encode { + local req = Request:new(player, command) + req.focus_text = poxi_type + req:setData(player, { type = poxi_type, data = data, extra_data = extra_data, cancelable = (cancelable == nil) and true or cancelable }) + local result = req:getResult(player) if result == "" then return poxi.default_choice(data, extra_data) else - return poxi.post_select(json.decode(result), data, extra_data) + return poxi.post_select(result, data, extra_data) end end @@ -1535,7 +1535,7 @@ end ---@return integer[] @ 选择的id function Room:askForCardsChosen(chooser, target, min, max, flag, reason, prompt) if min == 1 and max == 1 then - return { self:askForCardChosen(chooser, target, flag, reason) } + return { self:askForCardChosen(chooser, target, flag, reason, prompt) } end local cards @@ -1607,10 +1607,15 @@ function Room:askForChoice(player, choices, skill_name, prompt, detailed, all_ch local command = "AskForChoice" prompt = prompt or "" all_choices = all_choices or choices - self:notifyMoveFocus(player, skill_name) - local result = self:doRequest(player, command, json.encode{ + + local req = Request:new(player, command) + req.focus_text = skill_name + req.receive_decode = false -- 这个不用decode + req:setData(player, { choices, all_choices, skill_name, prompt, detailed }) + local result = req:getResult(player) + if result == "" then if table.contains(choices, "Cancel") then result = "Cancel" @@ -1637,15 +1642,19 @@ function Room:askForChoices(player, choices, minNum, maxNum, skill_name, prompt, if #choices <= minNum and not all_choices and not cancelable then return choices end assert(minNum <= maxNum) assert(not all_choices or table.every(choices, function(c) return table.contains(all_choices, c) end)) + local command = "AskForChoices" skill_name = skill_name or "" prompt = prompt or "" all_choices = all_choices or choices detailed = detailed or false - self:notifyMoveFocus(player, skill_name) - local result = self:doRequest(player, command, json.encode{ + + local req = Request:new(player, command) + req.focus_text = skill_name + req:setData(player, { choices, all_choices, {minNum, maxNum}, cancelable, skill_name, prompt, detailed }) + local result = req:getResult(player) if result == "" then if cancelable then return {} @@ -1653,7 +1662,7 @@ function Room:askForChoices(player, choices, minNum, maxNum, skill_name, prompt, return table.random(choices, math.min(minNum, #choices)) end end - return json.decode(result) + return result end --- 询问玩家是否发动技能。 @@ -1664,11 +1673,11 @@ end ---@return boolean function Room:askForSkillInvoke(player, skill_name, data, prompt) local command = "AskForSkillInvoke" - self:notifyMoveFocus(player, skill_name) - local invoked = false - local result = self:doRequest(player, command, json.encode{ skill_name, prompt }) - if result ~= "" then invoked = true end - return invoked + local req = Request:new(player, command) + req.focus_text = skill_name + req.receive_decode = false -- 这个返回的都是"1" 不用decode + req:setData(player, { skill_name, prompt }) + return req:getResult(player) ~= "" end -- 获取使用牌的合法额外目标(【借刀杀人】等带副目标的卡牌除外) @@ -1795,7 +1804,9 @@ function Room:askForArrangeCards(player, skillname, cardMap, prompt, free_arrang poxi_type = poxi_type or "", cancelable = ((pattern ~= "." or poxi_type ~= "") and (default_choice == nil)) } - local result = self:doRequest(player, command, json.encode(data)) + local req = Request:new(player, command) + req:setData(player, data) + local result = req:getResult(player) -- local result = player.room:askForCustomDialog(player, skillname, -- "RoomElement/ArrangeCardsBox.qml", { -- cardMap, prompt, box_size, max_limit, min_limit, free_arrange or false, areaNames, @@ -1829,7 +1840,7 @@ function Room:askForArrangeCards(player, skillname, cardMap, prompt, free_arrang end return cardMap end - return json.decode(result) + return result end -- TODO: guanxing type @@ -1867,7 +1878,6 @@ function Room:askForGuanxing(player, cards, top_limit, bottom_limit, customNotif areaNames = { "Top", "Bottom" } end local command = "AskForGuanxing" - self:notifyMoveFocus(player, customNotify or command) local max_top = top_limit[2] local card_map = {} if max_top > 0 then @@ -1888,10 +1898,13 @@ function Room:askForGuanxing(player, cards, top_limit, bottom_limit, customNotif bottom_area_name = areaNames[2], } - local result = self:doRequest(player, command, json.encode(data)) + local req = Request:new(player, command) + req.focus_text = customNotify + req:setData(player, data) + local result = req:getResult(player) local top, bottom if result ~= "" then - local d = json.decode(result) + local d = result if top_limit[2] == 0 then top = Util.DummyTable bottom = d[1] @@ -1942,15 +1955,17 @@ function Room:askForExchange(player, piles, piles_name, customNotify) elseif x < 0 then piles_name = table.slice(piles_name, 1, #piles + 1) end - self:notifyMoveFocus(player, customNotify or command) local data = { piles = piles, piles_name = piles_name, } - local result = self:doRequest(player, command, json.encode(data)) + + local req = Request:new(player, command) + req.focus_text = customNotify + req:setData(player, data) + local result = req:getResult(player) if result ~= "" then - local d = json.decode(result) - return d + return result else return piles end @@ -1960,7 +1975,7 @@ end ---@param data string ---@return CardUseStruct function Room:handleUseCardReply(player, data) - data = json.decode(data) + -- data = json.decode(data) local card = data.card local targets = data.targets if type(card) == "string" then @@ -2062,7 +2077,6 @@ function Room:askForUseCard(player, card_name, pattern, prompt, cancelable, extr end local command = "AskForUseCard" - self:notifyMoveFocus(player, card_name) cancelable = (cancelable == nil) and true or cancelable extra_data = extra_data or Util.DummyTable prompt = prompt or "" @@ -2088,7 +2102,12 @@ function Room:askForUseCard(player, card_name, pattern, prompt, cancelable, extr Fk.currentResponsePattern = pattern self.logic:trigger(fk.HandleAskForPlayCard, nil, askForUseCardData, true) - local result = self:doRequest(player, command, json.encode(data)) + + local req = Request:new(player, command) + req.focus_text = card_name or "" + req:setData(player, data) + local result = req:getResult(player) + askForUseCardData.afterRequest = true self.logic:trigger(fk.HandleAskForPlayCard, nil, askForUseCardData, true) Fk.currentResponsePattern = nil @@ -2124,7 +2143,6 @@ function Room:askForResponse(player, card_name, pattern, prompt, cancelable, ext end local command = "AskForResponseCard" - self:notifyMoveFocus(player, card_name) cancelable = (cancelable == nil) and true or cancelable extra_data = extra_data or Util.DummyTable pattern = pattern or card_name @@ -2152,7 +2170,12 @@ function Room:askForResponse(player, card_name, pattern, prompt, cancelable, ext Fk.currentResponsePattern = pattern eventData.isResponse = true self.logic:trigger(fk.HandleAskForPlayCard, nil, eventData, true) - local result = self:doRequest(player, command, json.encode(data)) + + local req = Request:new(player, command) + req.focus_text = card_name or "" + req:setData(player, data) + local result = req:getResult(player) + eventData.afterRequest = true self.logic:trigger(fk.HandleAskForPlayCard, nil, eventData, true) Fk.currentResponsePattern = nil @@ -2206,8 +2229,6 @@ function Room:askForNullification(players, card_name, pattern, prompt, cancelabl repeat useResult = nil - self:notifyMoveFocus(self.alive_players, card_name) - self:doBroadcastNotify("WaitForNullification", "") local data = {card_name, pattern, prompt, cancelable, extra_data, disabledSkillNames} @@ -2220,12 +2241,19 @@ function Room:askForNullification(players, card_name, pattern, prompt, cancelabl eventData = effectData, } self.logic:trigger(fk.HandleAskForPlayCard, nil, eventData, true) - local winner = self:doRaceRequest(command, players, json.encode(data)) + + local req = Request:new(players, command, 1) + req.focus_players = self.alive_players + req.focus_text = card_name + for _, p in ipairs(players) do req:setData(p, data) end + req:ask() + local winner = req.winners[1] + eventData.afterRequest = true self.logic:trigger(fk.HandleAskForPlayCard, nil, eventData, true) if winner then - local result = winner.client_reply + local result = req:getResult(winner) useResult = self:handleUseCardReply(winner, result) if type(useResult) == "string" and useResult ~= "" then @@ -2259,13 +2287,17 @@ function Room:askForAG(player, id_list, cancelable, reason) end local command = "AskForAG" - self:notifyMoveFocus(player, reason or command) + local data = { id_list, cancelable, reason } - local ret = self:doRequest(player, command, json.encode(data)) + local req = Request:new(player, command) + req.focus_text = reason + req:setData(player, data) + local ret = req:getResult(player) + if ret == "" and not cancelable then ret = table.random(id_list) end - return tonumber(ret) + return ret end --- 给player发一条消息,在他的窗口中用一系列卡牌填充一个AG。 @@ -2356,22 +2388,22 @@ function Room:askForMiniGame(players, focus, game_type, data_table) local command = "MiniGame" local game = Fk.mini_games[game_type] if #players == 0 or not game then return end + + local req = Request:new(players, command) + req.focus_text = focus + req.receive_decode = false -- 和customDialog同理 + for _, p in ipairs(players) do local data = data_table[p.id] p.mini_game_data = { type = game_type, data = data } - p.request_data = json.encode(p.mini_game_data) - p.default_reply = game.default_choice and json.encode(game.default_choice(p, data)) or "" + req:setData(p, p.mini_game_data) + req:setDefaultReply(p, game.default_choice and json.encode(game.default_choice(p, data))) end - self:notifyMoveFocus(players, focus) - self:doBroadcastRequest(command, players) + req:ask() for _, p in ipairs(players) do p.mini_game_data = nil - if not p.reply_ready then - p.client_reply = p.default_reply - p.reply_ready = true - end end end @@ -2386,11 +2418,14 @@ end ---@return string function Room:askForCustomDialog(player, focustxt, qmlPath, extra_data) local command = "CustomDialog" - self:notifyMoveFocus(player, focustxt) - return self:doRequest(player, command, json.encode{ + local req = Request:new(player, command) + req.focus_text = focustxt + req.receive_decode = false -- 没法知道要不要decode,所以我写false (json.decode该杀啊) + req:setData(player, { path = qmlPath, data = extra_data, }) + return req:getResult(player) end --- 询问移动场上的一张牌 @@ -2475,14 +2510,13 @@ function Room:askForMoveCardInBoard(player, targetOne, targetTwo, skillName, fla playerIds = { targetOne.id, targetTwo.id } } local command = "AskForMoveCardInBoard" - self:notifyMoveFocus(player, command) - local result = self:doRequest(player, command, json.encode(data)) + local req = Request:new(player, command) + req:setData(player, data) + local result = req:getResult(player) if result == "" then local randomIndex = math.random(1, #cards) result = { cardId = cards[randomIndex], pos = cardsPosition[randomIndex] } - else - result = json.decode(result) end local from, to @@ -2942,10 +2976,5 @@ function Room:removeTableMark(sth, mark, value) end end -function Room:__index(k) - if k == "room_settings" then - return self.settings - end -end return Room diff --git a/lua/server/scheduler.lua b/lua/server/scheduler.lua index 740c636b..f9e0adc7 100644 --- a/lua/server/scheduler.lua +++ b/lua/server/scheduler.lua @@ -1,6 +1,6 @@ -- SPDX-License-Identifier: GPL-3.0-or-later -local Room = require "server.room" +Room = require "server.room" -- 所有当前正在运行的房间(即游戏尚未结束的房间) ---@type table diff --git a/lua/server/serverplayer.lua b/lua/server/serverplayer.lua index 0142c95c..9e3e926a 100644 --- a/lua/server/serverplayer.lua +++ b/lua/server/serverplayer.lua @@ -30,13 +30,6 @@ function ServerPlayer:initialize(_self) self.id = _self:getId() self.room = nil - -- Below are for doBroadcastRequest - -- 但是几乎全部被船新request杀了 - self.request_data = "" - --self.client_reply = "" - self.default_reply = "" - --self.reply_ready = false - --self.reply_cancel = false self.phases = {} self.skipped_phases = {} self.phase_state = {} @@ -47,7 +40,7 @@ function ServerPlayer:initialize(_self) self._prelighted_skills = {} self._timewaste_count = 0 - self.ai = RandomAI:new(self) + self.ai = SmartAI:new(self) end ---@param command string @@ -284,6 +277,9 @@ end function ServerPlayer:play(phase_table) phase_table = phase_table or {} if #phase_table > 0 then + if not table.contains(phase_table, Player.RoundStart) then + table.insert(phase_table, 1, Player.RoundStart) + end if not table.contains(phase_table, Player.NotActive) then table.insert(phase_table, Player.NotActive) end @@ -384,17 +380,19 @@ function ServerPlayer:endCurrentPhase() end --- 获得一个额外回合 ----@param delay? boolean ----@param skillName? string -function ServerPlayer:gainAnExtraTurn(delay, skillName) +---@param delay? boolean @ 是否延迟到当前回合结束再开启额外回合,默认是 +---@param skillName? string @ 额外回合原因 +---@param turnData? TurnStruct @ 额外回合的信息 +function ServerPlayer:gainAnExtraTurn(delay, skillName, turnData) local room = self.room delay = (delay == nil) and true or delay - skillName = (skillName == nil) and room.logic:getCurrentSkillName() or skillName + skillName = skillName or room.logic:getCurrentSkillName() or "game_rule" + turnData = turnData or {} + turnData.reason = skillName if delay then - local logic = room.logic - local turn = logic:getCurrentEvent():findParent(GameEvent.Turn, true) + local turn = room.logic:getCurrentEvent():findParent(GameEvent.Turn, true) if turn then - turn:prependExitFunc(function() self:gainAnExtraTurn(false, skillName) end) + turn:prependExitFunc(function() self:gainAnExtraTurn(false, skillName, turnData) end) return end end @@ -407,13 +405,15 @@ function ServerPlayer:gainAnExtraTurn(delay, skillName) local current = room.current room.current = self - self.tag["_extra_turn_count"] = self.tag["_extra_turn_count"] or {} - local ex_tag = self.tag["_extra_turn_count"] - table.insert(ex_tag, skillName) + room:addTableMark(self, "_extra_turn_count", skillName) - GameEvent.Turn:create(self):exec() + GameEvent.Turn:create(self, turnData):exec() - table.remove(ex_tag) + local mark = self:getTableMark("_extra_turn_count") + if #mark > 0 then + table.remove(mark) + room:setPlayerMark(self, "_extra_turn_count", mark) + end room.current = current end @@ -421,17 +421,14 @@ end --- 当前是否处于额外的回合。 --- @return boolean function ServerPlayer:insideExtraTurn() - return self.tag["_extra_turn_count"] and #self.tag["_extra_turn_count"] > 0 + return self:getCurrentExtraTurnReason() ~= "game_rule" end ---- 当前额外回合的技能原因。 +--- 当前额外回合的技能原因。非额外回合则为game_rule ---@return string function ServerPlayer:getCurrentExtraTurnReason() - local ex_tag = self.tag["_extra_turn_count"] - if (not ex_tag) or #ex_tag == 0 then - return "game_rule" - end - return ex_tag[#ex_tag] + local mark = self:getTableMark("_extra_turn_count") + return mark[#mark] or "game_rule" end --- 角色摸牌。 @@ -461,6 +458,7 @@ function ServerPlayer:bury() self:throwAllCards() self:throwAllMarks() self:clearPiles() + self:onAllSkillLose() self:reset() end @@ -482,6 +480,12 @@ function ServerPlayer:throwAllCards(flag) self.room:throwCard(cardIds, "", self) end +function ServerPlayer:onAllSkillLose() + for _, skill in ipairs(self:getAllSkills()) do + skill:onLose(self, true) + end +end + function ServerPlayer:throwAllMarks() for name, _ in pairs(self.mark) do self.room:setPlayerMark(self, name, 0) diff --git a/lua/server/system_enum.lua b/lua/server/system_enum.lua index cb9d3bc3..31393632 100644 --- a/lua/server/system_enum.lua +++ b/lua/server/system_enum.lua @@ -246,6 +246,11 @@ fk.IceDamage = 4 ---@field public skillName string @ 技能名 ---@field public fromPlace "top"|"bottom" @ 摸牌的位置 +---@--- TurnStruct 回合事件的数据 +---@class TurnStruct +---@field public reason string? @ 当前额外回合的原因,不为额外回合则为game_rule +---@field public phase_table? Phase[] @ 此回合将进行的阶段,填空则为正常流程 + --- 移动理由 ---@alias CardMoveReason integer fk.ReasonJustMove = 1 diff --git a/lua/ui_emu/common.lua b/lua/ui_emu/common.lua index 0e569663..0b92d2cf 100644 --- a/lua/ui_emu/common.lua +++ b/lua/ui_emu/common.lua @@ -4,6 +4,11 @@ local SelectableItem = base.SelectableItem ---@class CardItem: SelectableItem local CardItem = SelectableItem:subclass("CardItem") +function CardItem:initialize(scene, id) + SelectableItem.initialize(self, scene, id) + Fk:filterCard(id, Fk:currentRoom():getCardOwner(id)) +end + ---@class Photo: SelectableItem ---@field public state string local Photo = SelectableItem:subclass("Photo") diff --git a/packages/maneuvering/ai/init.lua b/packages/maneuvering/ai/init.lua index 7785abb0..bc340f6d 100644 --- a/packages/maneuvering/ai/init.lua +++ b/packages/maneuvering/ai/init.lua @@ -1,180 +1,10 @@ ---[[ -fk.ai_card.thunder__slash = fk.ai_card.slash -fk.ai_use_play.thunder__slash = fk.ai_use_play.slash -fk.ai_card.fire__slash = fk.ai_card.slash -fk.ai_use_play.fire__slash = fk.ai_use_play.slash -fk.ai_card.analeptic = { - intention = 60, -- 身份值 - value = 5, -- 卡牌价值 - priority = 3 -- 使用优先值 -} +local slash = fk.ai_skills["slash_skill"] +local just_use = fk.ai_skills["__just_use"] +local use_to_enemy = fk.ai_skills["__use_to_enemy"] +SmartAI:setSkillAI("thunder__slash_skill", slash) +SmartAI:setSkillAI("fire__slash_skill", slash) +SmartAI:setSkillAI("analeptic_skill", just_use) +SmartAI:setSkillAI("iron_chain_skill", just_use) +SmartAI:setSkillAI("fire_attack_skill", use_to_enemy) +SmartAI:setSkillAI("supply_shortage_skill", use_to_enemy) -fk.ai_use_play["analeptic"] = function(self, card) - local cards = table.map(self.player:getCardIds("&he"), function(id) - return Fk:getCardById(id) - end) - self:sortValue(cards) - for _, sth in ipairs(self:getActives("slash")) do - local slash = nil - if sth:isInstanceOf(Card) then - if sth.skill:canUse(self.player, sth) and not self.player:prohibitUse(sth) then - slash = sth - end - else - local selected = {} - for _, c in ipairs(cards) do - if sth:cardFilter(c.id, selected) then - table.insert(selected, c.id) - end - end - local tc = sth:viewAs(selected) - if tc and tc:matchPattern("slash") and tc.skill:canUse(self.player, tc) and not self.player:prohibitUse(tc) then - slash = tc - end - end - if slash then - fk.ai_use_play.slash(self, slash) - if self.use_id then - self.use_id = card.id - self.use_tos = {} - break - end - end - end -end - -fk.ai_card.iron_chain = { - intention = function(self, card, from) - if self.player.chained then - return -80 - end - return 80 - end, -- 身份值 - value = 2, -- 卡牌价值 - priority = 3 -- 使用优先值 -} - -fk.ai_use_play["iron_chain"] = function(self, card) - for _, p in ipairs(self.friends) do - if card.skill:targetFilter(p.id, self.use_tos, {}, card) and p.chained then - table.insert(self.use_tos, p.id) - end - end - self:sort(self.enemies) - for _, p in ipairs(self.enemies) do - if card.skill:targetFilter(p.id, self.use_tos, {}, card) and not p.chained then - table.insert(self.use_tos, p.id) - end - end - if #self.use_tos < 2 then - self.use_tos = {} - else - self.use_id = card.id - end -end - -fk.ai_use_play["recast"] = function(self, card) - if self.command == "PlayCard" then - self.use_id = card.id - self.special_skill = "recast" - end -end - -fk.ai_card.fire_attack = { - intention = 90, -- 身份值 - value = 3, -- 卡牌价值 - priority = 4 -- 使用优先值 -} - -fk.ai_use_play["fire_attack"] = function(self, card) - self:sort(self.enemies) - for _, p in ipairs(self.enemies) do - if card.skill:targetFilter(p.id, self.use_tos, {}, card) and #self.player:getCardIds("h") > 2 then - self.use_id = card.id - table.insert(self.use_tos, p.id) - end - end -end - -fk.ai_discard["fire_attack_skill"] = function(self, min_num, num, include_equip, cancelable, pattern, prompt) - local use = self:eventData("UseCard") - for _, p in ipairs(TargetGroup:getRealTargets(use.tos)) do - if self:isEnemy(p) then - local cards = table.map(self.player:getCardIds("h"), function(id) - return Fk:getCardById(id) - end) - local exp = Exppattern:Parse(pattern) - cards = table.filter(cards, function(c) - return exp:match(c) - end) - if #cards > 0 then - self:sortValue(cards) - return { cards[1].id } - end - end - end -end - -fk.ai_nullification.fire_attack = function(self, card, to, from, positive) - if positive then - if self:isFriend(to) and #to:getCardIds("h") > 0 and #from:getCardIds("h") > 0 then - if #self.avail_cards > 1 or self:isWeak(to) or to.id == self.player.id then - self.use_id = self.avail_cards[1] - end - end - else - if self:isEnemy(to) and #to:getCardIds("h") > 0 and #from:getCardIds("h") > 1 then - if #self.avail_cards > 1 or self:isWeak(to) then - self.use_id = self.avail_cards[1] - end - end - end -end - -fk.ai_card.fire_attack = { - intention = 120, -- 身份值 - value = 2, -- 卡牌价值 - priority = 2 -- 使用优先值 -} - -fk.ai_use_play["supply_shortage"] = function(self, card) - self:sort(self.enemies) - for _, p in ipairs(self.enemies) do - if card.skill:targetFilter(p.id, self.use_tos, {}, card) and not p.chained then - self.use_id = card.id - table.insert(self.use_tos, p.id) - end - end -end - -fk.ai_nullification.supply_shortage = function(self, card, to, from, positive) - if positive then - if self:isFriend(to) then - if #self.avail_cards > 1 or self:isWeak(to) or to.id == self.player.id then - self.use_id = self.avail_cards[1] - end - end - else - if self:isEnemy(to) then - if #self.avail_cards > 1 or self:isWeak(to) then - self.use_id = self.avail_cards[1] - end - end - end -end - -fk.ai_card.supply_shortage = { - intention = 130, -- 身份值 - value = 2, -- 卡牌价值 - priority = 1 -- 使用优先值 -} - -fk.ai_skill_invoke["#fan_skill"] = function(self) - local use = self:eventData("UseCard") - for _, p in ipairs(TargetGroup:getRealTargets(use.tos)) do - if not self:isFriend(p) then - return true - end - end -end ---]] diff --git a/packages/standard/ai/aux_skills.lua b/packages/standard/ai/aux_skills.lua index df608b0e..eb12a3d9 100644 --- a/packages/standard/ai/aux_skills.lua +++ b/packages/standard/ai/aux_skills.lua @@ -1,55 +1,11 @@ --- aux_skill的AI文件。aux_skill的重量级程度无需多说。 --- 这个文件说是第二个smart_ai.lua也不为过。 +SmartAI:setSkillAI("discard_skill", { + choose_targets = function(_, ai) + return ai:doOKButton() + end, +}) --- discard_skill: 弃牌相关AI ------------------------------ - ---- 弃牌相关判定函数的表。键为技能名,值为原型如下的函数。 ----@type table -fk.ai_discard = {} - -local default_discard = function(self, min_num, num, include_equip, cancelable, pattern, prompt) - if cancelable then return nil end - local flag = "h" - if include_equip then - flag = "he" - end - local ret = {} - local cards = self.player:getCardIds(flag) - for _, cid in ipairs(cards) do - table.insert(ret, cid) - if #ret >= min_num then - break - end - end - return ret -end - -fk.ai_active_skill["discard_skill"] = function(self, prompt, cancelable, data) - local ret = self:callFromTable(fk.ai_discard, not cancelable and default_discard, data.skillName, - self, data.min_num, data.num, data.include_equip, cancelable, data.pattern, prompt) - - if ret == nil or #ret < data.min_num then return nil end - - return self:buildUseReply { skill = "discard_skill", subcards = ret } -end - --- choose_players_skill: 选人相关AI -------------------------------------- - ----@class ChoosePlayersReply ----@field cardId? integer ----@field targets integer[] - ---- 选人相关判定函数的表。键为技能名,值为原型如下的函数。 ----@type table -fk.ai_choose_players = {} - -fk.ai_active_skill["choose_players_skill"] = function(self, prompt, cancelable, data) - local ret = self:callFromTable(fk.ai_choose_players, nil, data.skillName, - self, data.targets, data.min_num, data.num, cancelable) - - if ret then - return self:buildUseReply({ skill = "choose_players_skill", subcards = { ret.cardId } }, ret.targets) - end -end +SmartAI:setSkillAI("choose_cards_skill", { + choose_targets = function(_, ai) + return ai:doOKButton() + end, +}) diff --git a/packages/standard/ai/init.lua b/packages/standard/ai/init.lua index e25c8612..36dcdc49 100644 --- a/packages/standard/ai/init.lua +++ b/packages/standard/ai/init.lua @@ -1,80 +1,186 @@ -require "packages.standard.ai.aux_skills" +SmartAI:setSkillAI("ganglie", { + think = function(self, ai) + -- 刚烈的think中要处理两种情况:一是askForSkillInvoke的确定取消,二是被刚烈的人决定是否弃置2牌 + if ai:getPrompt():startsWith("#AskForDiscard") then + -- 权衡一下弃牌与扣血的收益 + -- local cancel_val = 模拟自己扣血的收益 + -- local ok_val = 模拟弃两张最垃圾牌的收益 + -- 比如说,等于discard_skill_ai:think()的收益什么的 + -- if ok_val > cancel_val then + -- return ai:doOKButton() + -- else + -- return "" + -- end + else + -- 模拟一下self.skill:use 计算收益是否为正 + return false + end + end, +}) + +SmartAI:setTriggerSkillAI("dawu", { + correct_func = function(self, logic, event, target, player, data) + if event ~= fk.DamageInflicted then return end + return self.skill:triggerable(event, target, player, data) + end, +}) + +--[=[ +if UsingNewCore then + require "standard.ai.aux_skills" +else + require "packages.standard.ai.aux_skills" +end + +local true_invoke = { skill_invoke = true } +local enemy_damage_invoke = { + skill_invoke = function(skill, ai) + local room = ai.room + local logic = room.logic + + local event = logic:getCurrentEvent() + local dmg = event.data[1] + return ai:isEnemy(dmg.from) + end +} +---@type SmartAISkillSpec +local active_random_select_card = { + will_use = Util.TrueFunc, + ---@param skill ViewAsSkill + choose_cards = function(skill, ai) + repeat + local cids = ai:getEnabledCards() + if #cids == 0 then return ai:okButtonEnabled() end + ai:selectCard(cids[1], true) + until ai:okButtonEnabled() or ai:hasEnabledTarget() + return true + end, +} + +local use_to_enemy = fk.ai_skills["__use_to_enemy"] +local use_to_friend = fk.ai_skills["__use_to_friend"] +local just_use = fk.ai_skills["__just_use"] -- 魏国 -fk.ai_skill_invoke["jianxiong"] = true +SmartAI:setSkillAI("jianxiong", true_invoke) -- TODO: hujia + -- TODO: guicai 关于如何界定判定的好坏 需要向AI中单独说明 +SmartAI:setSkillAI("fankui", enemy_damage_invoke) -fk.ai_skill_invoke["fankui"] = function(self) - local room = self.room - local logic = room.logic +SmartAI:setSkillAI("ganglie", { + skill_invoke = function(skill, ai) + local room = ai.room + local logic = room.logic - -- 询问反馈时,处于on_cost环节,当前事件必是damage且有from - local event = logic:getCurrentEvent() - local dmg = event.data[1] - return self:isEnemy(dmg.from) -end + local event = logic:getCurrentEvent() + local dmg = event.data[1] + return ai:isEnemy(dmg.from) + end, + choose_cards = function(skill, ai) + local cards = ai:getEnabledCards() + if #cards > 2 then + for i = 1, 2 do ai:selectCard(cards[i], true) end + return true + end + return false -- 直接按取消键 + end, + -- choose_targets只有个按ok 复用默认 +}) -fk.ai_skill_invoke["ganglie"] = fk.ai_skill_invoke["fankui"] +SmartAI:setSkillAI("tuxi", { + choose_targets = function(skill, ai) + local targets = ai:getEnabledTargets() + local i = 0 + for _, p in ipairs(targets) do + if ai:isEnemy(p) then + ai:selectTarget(p, true) + i = i + 1 + if i >= 2 then return ai:doOKButton() end + end + end + end +}) --- TODO: tuxi +SmartAI:setSkillAI("luoyi", { skill_invoke = false }) -fk.ai_skill_invoke["luoyi"] = function(self) - return false -end +SmartAI:setSkillAI("tiandu", true_invoke) +SmartAI:setSkillAI("yiji", { + skill_invoke = true, + -- ask_active = function +}) -fk.ai_skill_invoke["tiandu"] = true - --- TODO: yiji - -fk.ai_skill_invoke["luoshen"] = true - --- TODO: qingguo +SmartAI:setSkillAI("luoshen", true_invoke) +SmartAI:setSkillAI("qingguo", active_random_select_card) -- 蜀国 --- TODO: rende +SmartAI:setSkillAI("rende", active_random_select_card) +SmartAI:setSkillAI("rende", use_to_friend) + -- TODO: jijiang --- TODO: wusheng +SmartAI:setSkillAI("wusheng", active_random_select_card) + -- TODO: guanxing + -- TODO: longdan +SmartAI:setSkillAI("longdan", active_random_select_card) -fk.ai_skill_invoke["tieqi"] = function(self) - local room = self.room - local logic = room.logic +SmartAI:setSkillAI("tieqi", { + skill_invoke = function(skill, ai) + local room = ai.room + local logic = room.logic - -- 询问反馈时,处于on_cost环节,当前事件必是damage且有from - local event = logic:getCurrentEvent() - local use = event.data[1] ---@type CardUseStruct - return table.find(use.tos, function(t) - return self:isEnemy(room:getPlayerById(t[1])) - end) -end + -- 询问反馈时,处于on_cost环节,当前事件必是damage且有from + local event = logic:getCurrentEvent() + local use = event.data[1] ---@type CardUseStruct + return table.find(use.tos, function(t) + return ai:isEnemy(room:getPlayerById(t[1])) + end) + end +}) -fk.ai_skill_invoke["jizhi"] = true +SmartAI:setSkillAI("jizhi", true_invoke) -- 吴国 --- TODO: zhiheng +SmartAI:setSkillAI("zhiheng", { + choose_cards = function(self, ai) + for _, cid in ipairs(ai:getEnabledCards()) do + ai:selectCard(cid, true) + end + return true + end, +}) +SmartAI:setSkillAI("zhiheng", just_use) + -- TODO: qixi +SmartAI:setSkillAI("qixi", active_random_select_card) -fk.ai_skill_invoke["keji"] = true +SmartAI:setSkillAI("keji", true_invoke) --- TODO: kurou +SmartAI:setSkillAI("kurou", just_use) -fk.ai_skill_invoke["yingzi"] = true +SmartAI:setSkillAI("yingzi", true_invoke) --- TODO: fanjian --- TODO: guose +SmartAI:setSkillAI("fanjian", use_to_enemy) + +SmartAI:setSkillAI("guose", active_random_select_card) -- TODO: liuli -fk.ai_skill_invoke["lianying"] = true -fk.ai_skill_invoke["xiaoji"] = true +SmartAI:setSkillAI("lianying", true_invoke) --- TODO: jieyin +SmartAI:setSkillAI("xiaoji", true_invoke) + +SmartAI:setSkillAI("jieyin", active_random_select_card) +SmartAI:setSkillAI("jieyin", use_to_friend) -- 群雄 --- TODO: qingnang +SmartAI:setSkillAI("qingnang", active_random_select_card) +SmartAI:setSkillAI("qingnang", use_to_friend) + -- TODO: jijiu --- TODO: wushuang +SmartAI:setSkillAI("qingnang", active_random_select_card) + -- TODO: lijian -fk.ai_skill_invoke["biyue"] = true +SmartAI:setSkillAI("biyue", true_invoke) +--]=] diff --git a/packages/standard/aux_skills.lua b/packages/standard/aux_skills.lua index c57369eb..ebb2d74a 100644 --- a/packages/standard/aux_skills.lua +++ b/packages/standard/aux_skills.lua @@ -27,7 +27,7 @@ local discardSkill = fk.CreateActiveSkill{ return false end end - if Fk.currentResponseReason == "game_rule" then + if Fk.currentResponseReason == "phase_discard" then status_skills = Fk:currentRoom().status_skills[MaxCardsSkill] or Util.DummyTable for _, skill in ipairs(status_skills) do if skill:excludeFrom(Self, card) then diff --git a/packages/standard/game_rule.lua b/packages/standard/game_rule.lua index 6e8c57fd..f795c9b7 100644 --- a/packages/standard/game_rule.lua +++ b/packages/standard/game_rule.lua @@ -1,14 +1,5 @@ -- SPDX-License-Identifier: GPL-3.0-or-later ----@param killer ServerPlayer -local function rewardAndPunish(killer, victim) - if killer.dead then return end - if victim.role == "rebel" then - killer:drawCards(3, "kill") - elseif victim.role == "loyalist" and killer.role == "lord" then - killer:throwAllCards("he") - end -end GameRule = fk.CreateTriggerSkill{ name = "game_rule", @@ -90,14 +81,11 @@ GameRule = fk.CreateTriggerSkill{ end, [fk.BuryVictim] = function() player:bury() - if room.tag["SkipNormalDeathProcess"] or player.rest > 0 then + if room.tag["SkipNormalDeathProcess"] or player.rest > 0 or (data.extra_data and data.extra_data.skip_reward_punish) then return false end local damage = data.damage - if damage and damage.from then - local killer = damage.from - rewardAndPunish(killer, player); - end + Fk.game_modes[room.settings.gameMode]:deathRewardAndPunish(player, damage and damage.from) end, default = function() print("game_rule: Event=" .. event) diff --git a/packages/standard/i18n/en_US.lua b/packages/standard/i18n/en_US.lua index 64121e6f..eae67567 100644 --- a/packages/standard/i18n/en_US.lua +++ b/packages/standard/i18n/en_US.lua @@ -222,7 +222,7 @@ Fk:loadTranslationTable({ ["revealMain"] = "Reveal main character %arg", ["revealDeputy"] = "Reveal deputy character %arg", - ["game_rule"] = "Discard", + ["game_rule"] = "GameRule", }, "en_US") -- init diff --git a/packages/standard/i18n/zh_CN.lua b/packages/standard/i18n/zh_CN.lua index eab672cd..1d2c53f2 100644 --- a/packages/standard/i18n/zh_CN.lua +++ b/packages/standard/i18n/zh_CN.lua @@ -298,6 +298,8 @@ Fk:loadTranslationTable{ ["lijian"] = "离间", [":lijian"] = "出牌阶段限一次,你可以弃置一张牌并选择两名其他男性角色,后选择的角色视为对先选择的角色使用了一张不能被【无懈可击】的【决斗】。", ["#lijian-active"] = "发动 离间,弃置一张手牌并选择两名其他男性角色,后选择的角色视为对先选择的角色使用了一张不能被【无懈可击】的【决斗】", + ["lijian_tip_1"] = "先出杀", + ["lijian_tip_2"] = "后出杀", ["$biyue1"] = "失礼了~", ["$biyue2"] = "羡慕吧~", ["biyue"] = "闭月", @@ -529,13 +531,23 @@ Fk:loadTranslationTable{ ["ex__choose_skill"] = "选择", ["distribution_select_skill"] = "分配", ["choose_players_to_move_card_in_board"] = "选择角色", + + ["AskForUseActiveSkill"] = "使用技能", + ["AskForSkillInvoke"] = "发动技能", + ["AskForUseCard"] = "使用", + ["AskForResponseCard"] = "打出", + ["AskForDiscard"] = "弃牌", + ["AskForCardChosen"] = "选牌", + ["AskForCardsChosen"] = "选牌", + ["AskForPindian"] = "拼点", + ["reveal_skill&"] = "亮将", ["#reveal_skill&"] = "选择一个武将亮将(点击左侧选择框展开)", [":reveal_skill&"] = "出牌阶段,你可明置一张有锁定技的武将。", ["revealMain"] = "明置主将 %arg", ["revealDeputy"] = "明置副将 %arg", - ["game_rule"] = "弃牌阶段", + ["game_rule"] = "游戏规则", ["replace_equip"] = "替换装备", ["#EquipmentChoice"] = "%arg", ["#GameRuleReplaceEquipment"] = "请选择要置入的区域", diff --git a/packages/standard/init.lua b/packages/standard/init.lua index c9b4bd96..da069f0a 100644 --- a/packages/standard/init.lua +++ b/packages/standard/init.lua @@ -1097,6 +1097,14 @@ local lijian = fk.CreateActiveSkill{ } room:useCard(new_use) end, + target_tip = function(self, to_select, selected, _, __, selectable, ____) + if not selectable then return end + if #selected == 0 or (#selected > 0 and selected[1] == to_select) then + return "lijian_tip_1" + else + return "lijian_tip_2" + end + end, } local biyue = fk.CreateTriggerSkill{ name = "biyue", @@ -1215,30 +1223,20 @@ local role_getlogic = function() end local nonlord = room:getOtherPlayers(lord, true) + local req = Request:new(nonlord, "AskForGeneral") local generals = table.random(room.general_pile, #nonlord * generalNum) for i, p in ipairs(nonlord) do local arg = table.slice(generals, (i - 1) * generalNum + 1, i * generalNum + 1) - p.request_data = json.encode{ arg, n } - p.default_reply = table.random(arg, n) + req:setData(p, { arg, n }) + req:setDefaultReply(p, table.random(arg, n)) end - room:notifyMoveFocus(nonlord, "AskForGeneral") - room:doBroadcastRequest("AskForGeneral", nonlord) - for _, p in ipairs(nonlord) do - local general, deputy - if p.general == "" and p.reply_ready then - local general_ret = json.decode(p.client_reply) - general = general_ret[1] - deputy = general_ret[2] - else - general = p.default_reply[1] - deputy = p.default_reply[2] - end + local result = req:getResult(p) + local general, deputy = result[1], result[2] room:findGeneral(general) room:findGeneral(deputy) room:prepareGeneral(p, general, deputy) - p.default_reply = "" end room:askForChooseKingdom(nonlord) @@ -1252,6 +1250,7 @@ local role_mode = fk.CreateGameMode{ minPlayer = 2, maxPlayer = 8, logic = role_getlogic, + main_mode = "role_mode", is_counted = function(self, room) return #room.players >= 5 end, diff --git a/packages/standard_cards/ai/init.lua b/packages/standard_cards/ai/init.lua index 9c94733a..494018e0 100644 --- a/packages/standard_cards/ai/init.lua +++ b/packages/standard_cards/ai/init.lua @@ -1,87 +1,71 @@ --- TODO: 合法性的方便函数 --- TODO: 关于如何选择多个目标 --- TODO: 关于装备牌 +SmartAI:setCardSkillAI("slash_skill", { + estimated_benefit = 120, --- 基本牌:杀,闪,桃 + on_effect = function(self, logic, effect) + self.skill:onEffect(logic, effect) + end, +}) ----@param from ServerPlayer ----@param to ServerPlayer ----@param card Card -local function tgtValidator(from, to, card) - return not from:prohibitUse(card) and - not from:isProhibited(to, card) and - true -- feasible -end +SmartAI:setTriggerSkillAI("#nioh_shield_skill", { + correct_func = function(self, logic, event, target, player, data) + return self.skill:triggerable(event, target, player, data) + end, +}) -local function justUse(self, card_name, extra_data) - local slashes = self:getCards(card_name, "use", extra_data) - if #slashes == 0 then return nil end +--[=====[ +local just_use = { + name = "__just_use", + will_use = Util.TrueFunc, + choose_targets = function(skill, ai, card) + return ai:doOKButton() + end, +} - return self:buildUseReply(slashes[1].id) -end +local use_to_friend = { + name = "__use_to_friend", + will_use = Util.TrueFunc, + choose_targets = function(skill, ai, card) + local targets = ai:getEnabledTargets() + for _, p in ipairs(targets) do + if ai:isFriend(p) then + ai:selectTarget(p, true) + break + end + end + return ai:doOKButton() + end, +} ----@param self SmartAI ----@param card_name string -local function useToEnemy(self, card_name, extra_data) - local slashes = self:getCards(card_name, "use", extra_data) - if #slashes == 0 then return nil end +local use_to_enemy = { + name = "__use_to_enemy", + will_use = Util.TrueFunc, + choose_targets = function(skill, ai, card) + local targets = ai:getEnabledTargets() + for _, p in ipairs(targets) do + if ai:isEnemy(p) then + ai:selectTarget(p, true) + break + end + end + return ai:doOKButton() + end, +} - -- TODO: 目标合法性 - local targets = {} - if self.enemies[1] then - table.insert(targets, self.enemies[1].id) - else - return nil - end - - return self:buildUseReply(slashes[1].id, targets) -end - -fk.ai_use_card["slash"] = function(self, pattern, prompt, cancelable, extra_data) - return useToEnemy(self, "slash", extra_data) -end - -fk.ai_use_card["jink"] = function(self, pattern, prompt, cancelable, extra_data) - return justUse(self, "jink", extra_data) -end - -fk.ai_use_card["peach"] = function(self, _, _, _, extra_data) - local cards = self:getCards("peach", "use", extra_data) - if #cards == 0 then return nil end - - return self:buildUseReply(cards[1].id) -end - --- 自救见军争卡牌AI -fk.ai_use_card["#AskForPeaches"] = function(self) - local room = self.room - local deathEvent = room.logic:getCurrentEvent() - local data = deathEvent.data[1] ---@type DyingStruct - - -- TODO: 关于救不回来、神关羽之类的更复杂逻辑 - -- TODO: 这些逻辑感觉不能写死在此函数里面,得想出更加多样的办法 - if self:isFriend(room:getPlayerById(data.who)) then - return fk.ai_use_card["peach"](self) - end - return nil -end - -fk.ai_use_card["dismantlement"] = function(self, pattern, prompt, cancelable, extra_data) - return useToEnemy(self, "dismantlement", extra_data) -end - -fk.ai_use_card["snatch"] = function(self, pattern, prompt, cancelable, extra_data) - return useToEnemy(self, "snatch", extra_data) -end - -fk.ai_use_card["duel"] = function(self, pattern, prompt, cancelable, extra_data) - return useToEnemy(self, "duel", extra_data) -end - -fk.ai_use_card["ex_nihilo"] = function(self, pattern, prompt, cancelable, extra_data) - return justUse(self, "ex_nihilo", extra_data) -end - -fk.ai_use_card["indulgence"] = function(self, pattern, prompt, cancelable, extra_data) - return useToEnemy(self, "indulgence", extra_data) -end +SmartAI:setSkillAI("__just_use", just_use) +SmartAI:setSkillAI("__use_to_enemy", use_to_enemy) +SmartAI:setSkillAI("__use_to_friend", use_to_friend) +SmartAI:setSkillAI("slash_skill", use_to_enemy) +SmartAI:setSkillAI("dismantlement_skill", use_to_enemy) +SmartAI:setSkillAI("snatch_skill", use_to_enemy) +SmartAI:setSkillAI("duel_skill", use_to_enemy) +SmartAI:setSkillAI("indulgence_skill", use_to_enemy) +SmartAI:setSkillAI("jink_skill", just_use) +SmartAI:setSkillAI("peach_skill", just_use) +SmartAI:setSkillAI("ex_nihilo_skill", just_use) +SmartAI:setSkillAI("savage_assault_skill", just_use) +SmartAI:setSkillAI("archery_attack_skill", just_use) +SmartAI:setSkillAI("god_salvation_skill", just_use) +SmartAI:setSkillAI("amazing_grace_skill", just_use) +SmartAI:setSkillAI("lightning_skill", just_use) +SmartAI:setSkillAI("default_equip_skill", just_use) +--]=====] diff --git a/packages/standard_cards/i18n/zh_CN.lua b/packages/standard_cards/i18n/zh_CN.lua index 39d90453..39f9a439 100644 --- a/packages/standard_cards/i18n/zh_CN.lua +++ b/packages/standard_cards/i18n/zh_CN.lua @@ -26,10 +26,12 @@ Fk:loadTranslationTable{ ["basic_char"] = "基", ["trick_char"] = "锦", ["equip_char"] = "装", + ["non_basic_char"] = "非基", ["basic"] = "基本牌", ["trick"] = "锦囊牌", ["equip"] = "装备牌", + ["non_basic"] = "非基本牌", ["weapon"] = "武器牌", ["armor"] = "防具牌", ["defensive_horse"] = "防御坐骑牌", diff --git a/packages/standard_cards/init.lua b/packages/standard_cards/init.lua index 89713f09..758adc48 100644 --- a/packages/standard_cards/init.lua +++ b/packages/standard_cards/init.lua @@ -462,7 +462,6 @@ extension:addCards({ local nullificationSkill = fk.CreateActiveSkill{ name = "nullification_skill", can_use = Util.FalseFunc, - on_use = function() RoomInstance:delay(1200) end, on_effect = function(self, room, effect) if effect.responseToEvent then effect.responseToEvent.isCancellOut = true @@ -713,7 +712,7 @@ local lightningSkill = fk.CreateActiveSkill{ } room:moveCards{ - ids = Card:getIdList(effect.card), + ids = room:getSubcardsByRule(effect.card, { Card.Processing }), toArea = Card.DiscardPile, moveReason = fk.ReasonUse } diff --git a/src/client/client.cpp b/src/client/client.cpp index 8b336abc..f5d47a68 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -46,9 +46,19 @@ Client::~Client() { } void Client::connectToHost(const QString &server, ushort port) { + start_connent_timestamp = QDateTime::currentMSecsSinceEpoch(); router->getSocket()->connectToHost(server, port); } +void Client::setupServerLag(qint64 server_time) { + auto now = QDateTime::currentMSecsSinceEpoch(); + auto ping = now - start_connent_timestamp; + auto lag = now - server_time; + server_lag = lag - ping / 2; +} + +qint64 Client::getServerLag() const { return server_lag; } + void Client::replyToServer(const QString &command, const QString &jsonData) { int type = Router::TYPE_REPLY | Router::SRC_CLIENT | Router::DEST_SERVER; router->reply(type, command, jsonData); diff --git a/src/client/client.h b/src/client/client.h index 08e8ee27..e4153236 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -17,6 +17,8 @@ public: ~Client(); void connectToHost(const QString &server, ushort port); + void setupServerLag(qint64 server_time); + qint64 getServerLag() const; Q_INVOKABLE void replyToServer(const QString &command, const QString &jsonData); Q_INVOKABLE void notifyServer(const QString &command, const QString &jsonData); @@ -51,6 +53,8 @@ private: Router *router; QMap players; ClientPlayer *self; + qint64 start_connent_timestamp; // 连接时的时间戳 单位毫秒 + qint64 server_lag = 0; // 与服务器时差,单位毫秒,正数表示自己快了 负数表示慢了 lua_State *L; QFileSystemWatcher fsWatcher; diff --git a/src/server/server.cpp b/src/server/server.cpp index 2a4f5785..abf95259 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -212,6 +212,7 @@ void Server::setupPlayer(ServerPlayer *player, bool all_info) { arr << player->getId(); arr << player->getScreenName(); arr << player->getAvatar(); + arr << QDateTime::currentMSecsSinceEpoch(); player->doNotify("Setup", JsonArray2Bytes(arr)); if (all_info) { diff --git a/src/swig/client.i b/src/swig/client.i index 4fa12b5d..34e6859a 100644 --- a/src/swig/client.i +++ b/src/swig/client.i @@ -18,6 +18,8 @@ extern QmlBackend *Backend; %nodefaultdtor Client; class Client : public QObject { public: + void setupServerLag(long long server_time); + void replyToServer(const QString &command, const QString &json_data); void notifyServer(const QString &command, const QString &json_data); diff --git a/src/swig/qt.i b/src/swig/qt.i index 3bf94c3d..f573fc2b 100644 --- a/src/swig/qt.i +++ b/src/swig/qt.i @@ -63,7 +63,7 @@ public: QVariant ret; if (high < 0) { if (low < 1) { - ret.setValue($self->bounded(0, 100001) / 100000); + ret.setValue(qreal($self->bounded(0, 100000001)) / 100000000); } else { ret.setValue($self->bounded(1, low + 1)); } diff --git a/src/ui/qmlbackend.cpp b/src/ui/qmlbackend.cpp index 251d1be7..63070307 100644 --- a/src/ui/qmlbackend.cpp +++ b/src/ui/qmlbackend.cpp @@ -289,16 +289,21 @@ QVariant QmlBackend::callLuaFunction(const QString &func_name, if (!ClientInstance) return QVariantMap(); lua_State *L = ClientInstance->getLuaState(); + + lua_getglobal(L, "debug"); + lua_getfield(L, -1, "traceback"); + lua_replace(L, -2); + lua_getglobal(L, func_name.toLatin1().data()); foreach (QVariant v, params) { pushLuaValue(L, v); } - int err = lua_pcall(L, params.length(), 1, 0); + int err = lua_pcall(L, params.length(), 1, -params.length() - 2); if (err) { qCritical() << lua_tostring(L, -1); - lua_pop(L, 1); + lua_pop(L, 2); return QVariant(); } auto result = readLuaValue(L); @@ -649,7 +654,10 @@ QJsonObject QmlBackend::getRequestData() const { auto router = ClientInstance->getRouter(); obj["id"] = router->getRequestId(); obj["timeout"] = router->getTimeout(); - obj["timestamp"] = router->getRequestTimestamp(); + auto timestamp = router->getRequestTimestamp(); + // 因为timestamp是服务器发来的时间,如果自己比服务器的时钟快的话,那么就得加上这个差值才行 + timestamp += ClientInstance->getServerLag(); + obj["timestamp"] = timestamp; return obj; }