From 203736e38ecfafe73574296d4a423252e3281de6 Mon Sep 17 00:00:00 2001 From: notify Date: Tue, 19 Sep 2023 14:27:54 +0800 Subject: [PATCH] Enhancement (#263) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - smart_ai搭了个壳子 - askForPoxi - 增加判断额外回合之法以及fix - 修trigger - 增加使用和打出的禁止技提示 - 修复卡牌标记,attach主动技显示为蓝色按钮 --- Fk/Pages/Lobby.qml | 2 +- Fk/Pages/Room.qml | 2 +- Fk/Pages/RoomLogic.js | 25 ++++ Fk/PhotoElement/MarkArea.qml | 7 +- Fk/RoomElement/CardItem.qml | 30 ++++ Fk/RoomElement/Dashboard.qml | 32 +++++ Fk/RoomElement/HandcardArea.qml | 1 + Fk/RoomElement/PlayerCardBox.qml | 36 +---- Fk/RoomElement/PoxiBox.qml | 138 +++++++++++++++++++ Fk/RoomElement/SkillButton.qml | 14 +- image/button/skill/active/normal-attach.png | Bin 0 -> 6370 bytes image/button/skill/active/pressed-attach.png | Bin 0 -> 8130 bytes image/card/chosen.png | Bin 0 -> 2077 bytes lua/client/client_util.lua | 48 +++++++ lua/client/i18n/en_US.lua | 2 +- lua/client/i18n/zh_CN.lua | 2 +- lua/core/engine.lua | 12 ++ lua/fk_ex.lua | 10 ++ lua/lsp/freekill.lua | 1 + lua/server/ai/init.lua | 22 +++ lua/server/ai/smart_ai.lua | 10 ++ lua/server/gameevent.lua | 7 + lua/server/gamelogic.lua | 35 ++--- lua/server/room.lua | 27 ++++ lua/server/serverplayer.lua | 28 +++- packages/maneuvering/ai/init.lua | 0 packages/standard/ai/init.lua | 0 packages/standard_cards/ai/init.lua | 0 packages/test/init.lua | 22 +++ 29 files changed, 452 insertions(+), 61 deletions(-) create mode 100644 Fk/RoomElement/PoxiBox.qml create mode 100644 image/button/skill/active/normal-attach.png create mode 100644 image/button/skill/active/pressed-attach.png create mode 100644 image/card/chosen.png create mode 100644 lua/server/ai/smart_ai.lua create mode 100644 packages/maneuvering/ai/init.lua create mode 100644 packages/standard/ai/init.lua create mode 100644 packages/standard_cards/ai/init.lua diff --git a/Fk/Pages/Lobby.qml b/Fk/Pages/Lobby.qml index 7d4fd364..317acfdd 100644 --- a/Fk/Pages/Lobby.qml +++ b/Fk/Pages/Lobby.qml @@ -139,7 +139,7 @@ Item { Text { width: parent.width horizontalAlignment: Text.AlignHCenter - text: Backend.translate("Room List") + text: Backend.translate("Room List").arg(roomModel.count) } ListView { id: roomList diff --git a/Fk/Pages/Room.qml b/Fk/Pages/Room.qml index a8622381..0194157f 100644 --- a/Fk/Pages/Room.qml +++ b/Fk/Pages/Room.qml @@ -59,7 +59,7 @@ Item { id: bgm source: config.bgmFile - // loops: MediaPlayer.Infinite + loops: MediaPlayer.Infinite onPlaybackStateChanged: { if (playbackState == MediaPlayer.StoppedState && roomScene.isStarted) play(); diff --git a/Fk/Pages/RoomLogic.js b/Fk/Pages/RoomLogic.js index 35bb4c5c..03c32b55 100644 --- a/Fk/Pages/RoomLogic.js +++ b/Fk/Pages/RoomLogic.js @@ -1070,6 +1070,31 @@ callbacks["AskForCardsChosen"] = (jsonData) => { }); } +callbacks["AskForPoxi"] = (jsonData) => { + const { type, data } = JSON.parse(jsonData); + + roomScene.state = "replying"; + roomScene.popupBox.sourceComponent = Qt.createComponent("../RoomElement/PoxiBox.qml"); + const box = roomScene.popupBox.item; + box.poxi_type = type; + box.card_data = data; + for (let d of data) { + const arr = []; + const ids = d[1]; + + ids.forEach(id => { + const card_data = JSON.parse(Backend.callLuaFunction("GetCardData", [id])); + arr.push(card_data); + }); + box.addCustomCards(d[0], arr); + } + + roomScene.popupBox.moveToCenter(); + box.cardsSelected.connect((ids) => { + replyToServer(JSON.stringify(ids)); + }); +} + callbacks["AskForMoveCardInBoard"] = (jsonData) => { const data = JSON.parse(jsonData); const { cards, cardsPosition, generalNames, playerIds } = data; diff --git a/Fk/PhotoElement/MarkArea.qml b/Fk/PhotoElement/MarkArea.qml index ebf98e67..adda007c 100644 --- a/Fk/PhotoElement/MarkArea.qml +++ b/Fk/PhotoElement/MarkArea.qml @@ -62,7 +62,12 @@ Item { } if (mark_name.startsWith('@$')) { - params.cardNames = mark_extra.split(','); + let data = mark_extra.split(','); + if (!Object.is(parseInt(data[0]), NaN)) { + params.ids = data.map(s => parseInt(s)); + } else { + params.cardNames = data; + } } else { let data = JSON.parse(Backend.callLuaFunction("GetPile", [root.parent.playerid, mark_name])); data = data.filter((e) => e !== -1); diff --git a/Fk/RoomElement/CardItem.qml b/Fk/RoomElement/CardItem.qml index c41d789a..d74ea8d3 100644 --- a/Fk/RoomElement/CardItem.qml +++ b/Fk/RoomElement/CardItem.qml @@ -31,11 +31,13 @@ Item { property string color: "" // only use when suit is empty property string footnote: "" // footnote, e.g. "A use card to B" property bool footnoteVisible: false + property string prohibitReason: "" property bool known: true // if false it only show a card back property bool enabled: true // if false the card will be grey property alias card: cardItem property alias glow: glowItem property var mark: ({}) + property alias chosenInBox: chosen.visible function getColor() { if (suit != "") @@ -88,6 +90,7 @@ Item { visible: false } + Image { id: cardItem source: known ? SkinBank.getCardPicture(cid || name) @@ -217,6 +220,15 @@ Item { } } + Image { + id: chosen + visible: false + source: SkinBank.CARD_DIR + "chosen" + anchors.horizontalCenter: parent.horizontalCenter + y: 90 + scale: 1.25 + } + Rectangle { visible: !root.selectable anchors.fill: parent @@ -224,6 +236,24 @@ Item { opacity: 0.7 } + Text { + id: prohibitText + visible: !root.selectable + anchors.centerIn: parent + font.family: fontLibian.name + font.pixelSize: 18 + opacity: 0.9 + horizontalAlignment: Text.AlignHCenter + lineHeight: 18 + lineHeightMode: Text.FixedHeight + color: "snow" + width: 20 + wrapMode: Text.WrapAnywhere + style: Text.Outline + styleColor: "red" + text: prohibitReason + } + TapHandler { acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.NoButton gesturePolicy: TapHandler.WithinBounds diff --git a/Fk/RoomElement/Dashboard.qml b/Fk/RoomElement/Dashboard.qml index 2158a9eb..09faeb03 100644 --- a/Fk/RoomElement/Dashboard.qml +++ b/Fk/RoomElement/Dashboard.qml @@ -166,17 +166,35 @@ RowLayout { const ids = []; let cards = handcardAreaItem.cards; for (let i = 0; i < cards.length; i++) { + cards[i].prohibitReason = ""; if (cardValid(cards[i].cid, cname)) { ids.push(cards[i].cid); + } else { + const prohibitReason = Backend.callLuaFunction( + "GetCardProhibitReason", + [cards[i].cid, roomScene.respond_play ? "response" : "use", cname] + ); + if (prohibitReason) { + cards[i].prohibitReason = prohibitReason; + } } } cards = self.equipArea.getAllCards(); cards.forEach(c => { + c.prohibitReason = ""; if (cardValid(c.cid, cname)) { ids.push(c.cid); if (!expanded_piles["_equip"]) { expandPile("_equip"); } + } else { + const prohibitReason = Backend.callLuaFunction( + "GetCardProhibitReason", + [c.cid, roomScene.respond_play ? "response" : "use", cname] + ); + if (prohibitReason) { + c.prohibitReason = prohibitReason; + } } }); @@ -202,6 +220,7 @@ RowLayout { const ids = [], cards = handcardAreaItem.cards; for (let i = 0; i < cards.length; i++) { + cards[i].prohibitReason = ""; if (JSON.parse(Backend.callLuaFunction("CanUseCard", [cards[i].cid, Self.id]))) { ids.push(cards[i].cid); } else { @@ -214,6 +233,14 @@ RowLayout { break; } } + + // still cannot use? show message on card + if (!ids.includes(cards[i].cid)) { + const prohibitReason = Backend.callLuaFunction("GetCardProhibitReason", [cards[i].cid, "play"]); + if (prohibitReason) { + cards[i].prohibitReason = prohibitReason; + } + } } } handcardAreaItem.enableCards(ids) @@ -372,6 +399,11 @@ RowLayout { item.enabled = item.pressed; } + const cards = handcardAreaItem.cards; + for (let i = 0; i < cards.length; i++) { + cards[i].prohibitReason = ""; + } + updatePending(); } diff --git a/Fk/RoomElement/HandcardArea.qml b/Fk/RoomElement/HandcardArea.qml index 3b282ff1..2d96bea1 100644 --- a/Fk/RoomElement/HandcardArea.qml +++ b/Fk/RoomElement/HandcardArea.qml @@ -48,6 +48,7 @@ Item { card.selectable = false; card.showDetail = false; card.selectedChanged.disconnect(adjustCards); + card.prohibitReason = ""; } return result; } diff --git a/Fk/RoomElement/PlayerCardBox.qml b/Fk/RoomElement/PlayerCardBox.qml index d6554702..e9d4a5bf 100644 --- a/Fk/RoomElement/PlayerCardBox.qml +++ b/Fk/RoomElement/PlayerCardBox.qml @@ -78,10 +78,10 @@ GraphicsBox { } onSelectedChanged: { if (selected) { - virt_name = "$Selected"; + chosenInBox = true; root.selected_ids.push(cid); } else { - virt_name = ""; + chosenInBox = false; root.selected_ids.splice(root.selected_ids.indexOf(cid), 1); } root.selected_ids = root.selected_ids; @@ -122,38 +122,6 @@ GraphicsBox { return ret; } - function addHandcards(cards) { - let handcards = findAreaModel('$Hand').areaCards; - if (cards instanceof Array) { - for (let i = 0; i < cards.length; i++) - handcards.append(cards[i]); - } else { - handcards.append(cards); - } - } - - function addEquips(cards) - { - let equips = findAreaModel('$Equip').areaCards; - if (cards instanceof Array) { - for (let i = 0; i < cards.length; i++) - equips.append(cards[i]); - } else { - equips.append(cards); - } - } - - function addDelayedTricks(cards) - { - let delayedTricks = findAreaModel('$Judge').areaCards; - if (cards instanceof Array) { - for (let i = 0; i < cards.length; i++) - delayedTricks.append(cards[i]); - } else { - delayedTricks.append(cards); - } - } - function addCustomCards(name, cards) { let area = findAreaModel(name).areaCards; if (cards instanceof Array) { diff --git a/Fk/RoomElement/PoxiBox.qml b/Fk/RoomElement/PoxiBox.qml new file mode 100644 index 00000000..1f30c8c1 --- /dev/null +++ b/Fk/RoomElement/PoxiBox.qml @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtQuick.Layouts +import Fk.Pages + +GraphicsBox { + id: root + + title.text: Backend.callLuaFunction("PoxiPrompt", [poxi_type, card_data]) + + // TODO: Adjust the UI design in case there are more than 7 cards + width: 70 + 700 + height: 64 + Math.min(cardView.contentHeight, 400) + 20 + + signal cardSelected(int cid) + signal cardsSelected(var ids) + property var selected_ids: [] + property string poxi_type + property var card_data + + ListModel { + id: cardModel + } + + ListView { + id: cardView + anchors.fill: parent + anchors.topMargin: 40 + anchors.leftMargin: 20 + anchors.rightMargin: 20 + anchors.bottomMargin: 20 + spacing: 20 + model: cardModel + clip: true + + delegate: RowLayout { + spacing: 15 + visible: areaCards.count > 0 + + Rectangle { + border.color: "#A6967A" + radius: 5 + color: "transparent" + width: 18 + height: 130 + Layout.alignment: Qt.AlignTop + + Text { + color: "#E4D5A0" + text: Backend.translate(areaName) + anchors.fill: parent + wrapMode: Text.WrapAnywhere + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + font.pixelSize: 15 + } + } + + GridLayout { + columns: 7 + Repeater { + model: areaCards + + CardItem { + cid: model.cid + name: model.name || "" + suit: model.suit || "" + number: model.number || 0 + autoBack: false + known: model.cid !== -1 + selectable: { + return root.selected_ids.includes(model.cid) || JSON.parse(Backend.callLuaFunction( + "PoxiFilter", + [root.poxi_type, model.cid, root.selected_ids, root.card_data] + )); + } + onSelectedChanged: { + if (selected) { + chosenInBox = true; + root.selected_ids.push(cid); + } else { + chosenInBox = false; + root.selected_ids.splice(root.selected_ids.indexOf(cid), 1); + } + root.selected_ids = root.selected_ids; + } + } + } + } + } + } + + MetroButton { + anchors.bottom: parent.bottom + text: Backend.translate("OK") + enabled: { + return JSON.parse(Backend.callLuaFunction( + "PoxiFeasible", + [root.poxi_type, root.selected_ids, root.card_data] + )); + } + onClicked: root.cardsSelected(root.selected_ids) + } + + onCardSelected: finished(); + + function findAreaModel(name) { + let ret; + for (let i = 0; i < cardModel.count; i++) { + let item = cardModel.get(i); + if (item.areaName == name) { + ret = item; + break; + } + } + if (!ret) { + ret = { + areaName: name, + areaCards: [], + } + cardModel.append(ret); + ret = findAreaModel(name); + } + return ret; + } + + + function addCustomCards(name, cards) { + let area = findAreaModel(name).areaCards; + if (cards instanceof Array) { + for (let i = 0; i < cards.length; i++) + area.append(cards[i]); + } else { + area.append(cards); + } + } +} diff --git a/Fk/RoomElement/SkillButton.qml b/Fk/RoomElement/SkillButton.qml index 09459869..d7c9c7f7 100644 --- a/Fk/RoomElement/SkillButton.qml +++ b/Fk/RoomElement/SkillButton.qml @@ -23,9 +23,17 @@ Item { x: -13 - 120 * 0.166 y: -6 - 55 * 0.166 scale: 0.66 - source: type === "notactive" ? "" - : AppPath + "/image/button/skill/" + type + "/" - + (enabled ? (pressed ? "pressed" : "normal") : "disabled") + source: { + if (type === "notactive") { + return ""; + } + let ret = AppPath + "/image/button/skill/" + type + "/"; + let suffix = enabled ? (pressed ? "pressed" : "normal") : "disabled"; + if (enabled && type === "active" && orig.endsWith("&")) { + suffix += "-attach"; + } + return ret + suffix; + } } Image { diff --git a/image/button/skill/active/normal-attach.png b/image/button/skill/active/normal-attach.png new file mode 100644 index 0000000000000000000000000000000000000000..792d7143e665756e7c6918e8e2152846b3762c65 GIT binary patch literal 6370 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 literal 0 HcmV?d00001 diff --git a/image/button/skill/active/pressed-attach.png b/image/button/skill/active/pressed-attach.png new file mode 100644 index 0000000000000000000000000000000000000000..28ffc10d3ea25aca20b2031a62bf66e5698e152a GIT binary patch literal 8130 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 literal 0 HcmV?d00001 diff --git a/image/card/chosen.png b/image/card/chosen.png new file mode 100644 index 0000000000000000000000000000000000000000..6c9049e60503b7295a08dc6f1ff9fca5633d1d35 GIT binary patch literal 2077 zcmV+&2;%pNP)75>h>JG1Lu@7VjoUfYquvEq$?fU^mSpb~A#qY@=m6y+fbs12=QN1;_g!4brO zM3gv>J_IPNn+Hw*P=!bZ1r#JV&)s;_q!J{^zF7SC0*mtIeN8nR znUl_Vo@a2}PXGWhE+HXC7lZ#!pfv`Taz$BbX0zSKdn4~J;(&4JNxjLjbNGeM04bFr(#Z&x%`eOe)| zgV9P;%A=0Yu~X^_Lgo?!5NKTJ0o<*4mBFT0{IpOw03Ztp$TR@>n`@)4$5;daGM5;{ zNNL%iYVOX_o&i07?`iN`)s+WrZAv4M{G(i}gb1!j2ot{% z$Xr&p0yr=q|6PgqHP;+2{|0-^xryf3m>=MvpjM}Od5|KNZFgQ?#t(^zd9vp9_?5xm z0svE;y0(W!+>bKYTO`tNa2-H{;|l~H0E{CNO30dP_5)zp>T9mu3MspDKs0e>@X}2F zk1XD|yhgV#=TB?9ii=V|OOU`Cqu=rBQ+h2Z=CXtHl)I6q$IRCbrQGAPUD ze(o{_Bn}89UzH#sq5vRG!If1?o9k-~Vp1ZpSNt&6J5V$iea*Gq#m4i<0nx-h(VBjX zkm`~MykdQM<3AEG2gISugSIxc8A!b$G4+yp2mmC(Bc(iDl6b97^D6H0cD0^;uWTa& z9Z^4(1atuKhJqWECG45fVc5#%b(W)t{uEBKVKx!%3ck& zj`j{6pI5xMIrx-K<-c$}PH;yU$PT36WDzfF|1#DyaGEiuSG2J?+n3yjK&;h7?!Tfv zc4cthT^(~0JRLj?AiR*zoQJ}gUUim-mHJyYqfTc^EwdQVIn&$cEzp?J~y8zz$#9z zN9Kwv23r$B>322&#ux-nr_Rf8<{dz3vkY)75DWVFPB19-1Y12I7;6CVw#X!-iM>t`q$i)(0AQRnx094{dkgO>y541RDD@7y zNy_+G@6c<-#miK5va_sR8U*)u8OT{k;@(M+huM1j7iWkUK)g*Lb4~1D=u@AK+>pGO zH73>myBMXJwnRGsBv z$(TOVR;Z>Q)1y5DKgfI_BcopzNS-Mqnpp=@=OiYNOJt`ee|)FHZwGHExZbtd*5yBG z+v3`6>+(0(M(6Y8wfvax1OpEM)2~Y;e`g-c+sf7vd?zf4%t;`ipu-exb+Mj-7d@ex ze~$0L-^Cb1`QwFop8F3FZx=9g=7z+FiINT}v!T^#-uZ0ASc7c|;8;y?tR@U!kP*$$ z_9#wwizL~QZ)Zw~tgD`o?UP9C6UYJ4RF`bcd{shiCa@dbA^Dns6gYZY@c5d3^$a8n)KU_DST1nX zdExnv;rbTA-a=5DtxLPj>0z`_+-jYe)6|N?yS!?R&91yzxtC%5X9DR&wjuHE^$$u` z0P9$5e}F~G9xAQJ13cW?R?AhA0)Du%~lEKCx&%ANNXJ% z2)qtp<;D5Mq=d0g6daLDj>NMpcpvAao2HD$-yJ$%vVs}#HC3-7Vp~h~HwoSORtMbr zk&(@1PS5;Fq19<#?k-=)BsCD^Jlkf9i%c?VwaicO@y7oFwu>WqQ!EwH00000NkvXX Hu0mjfqPp(` literal 0 HcmV?d00001 diff --git a/lua/client/client_util.lua b/lua/client/client_util.lua index 2ecf8572..e2529057 100644 --- a/lua/client/client_util.lua +++ b/lua/client/client_util.lua @@ -684,4 +684,52 @@ function SaveRecord() c.client:saveRecord(json.encode(c.record), c.record[2]) end +function GetCardProhibitReason(cid, method, pattern) + local card = Fk:getCardById(cid) + if not card then return "" 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 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 + if fn(skill, Self, card) then + s = skill + break + end + end + if not s then return "" end + + -- try to return a translated string + local skillName = s.name + local ret = Fk:translate(skillName) + if ret ~= skillName then + -- TODO: translate + return ret .. "禁" .. (method == "use" and "使用" or "打出") + elseif skillName:endsWith("_prohibit") and skillName:startsWith("#") then + return Fk:translate(skillName:sub(2, -10)) .. "禁" .. (method == "use" and "使用" or "打出") + end +end + +function PoxiPrompt(poxi_type, data) + local poxi = Fk.poxi_methods[poxi_type] + if not poxi or not poxi.prompt then return "" end + if type(poxi.prompt) == "string" then return Fk:translate(poxi.prompt) end + return poxi.prompt(data) +end + +function PoxiFilter(poxi_type, to_select, selected, data) + local poxi = Fk.poxi_methods[poxi_type] + if not poxi then return "false" end + return json.encode(poxi.card_filter(to_select, selected, data)) +end + +function PoxiFeasible(poxi_type, selected, data) + local poxi = Fk.poxi_methods[poxi_type] + if not poxi then return "false" end + return json.encode(poxi.feasible(selected, data)) +end + dofile "lua/client/i18n/init.lua" diff --git a/lua/client/i18n/en_US.lua b/lua/client/i18n/en_US.lua index 983c4489..13c6ad0e 100644 --- a/lua/client/i18n/en_US.lua +++ b/lua/client/i18n/en_US.lua @@ -2,7 +2,7 @@ Fk:loadTranslationTable({ -- Lobby - -- ["Room List"] = "房间列表", + ["Room List"] = "Room List (currently have %1 rooms)", -- ["Enter"] = "进入", -- ["Observe"] = "旁观", diff --git a/lua/client/i18n/zh_CN.lua b/lua/client/i18n/zh_CN.lua index 14137fb5..5071ee73 100644 --- a/lua/client/i18n/zh_CN.lua +++ b/lua/client/i18n/zh_CN.lua @@ -2,7 +2,7 @@ Fk:loadTranslationTable{ -- Lobby - ["Room List"] = "房间列表", + ["Room List"] = "房间列表 (共%1个房间)", ["Enter"] = "进入", ["Observe"] = "旁观", diff --git a/lua/core/engine.lua b/lua/core/engine.lua index 3cd53dce..ebbb63e6 100644 --- a/lua/core/engine.lua +++ b/lua/core/engine.lua @@ -25,6 +25,7 @@ ---@field public filtered_cards table @ 被锁视技影响的卡牌 ---@field public printed_cards table @ 被某些房间现场打印的卡牌,id都是负数且从-2开始 ---@field private _custom_events any[] @ 自定义事件列表 +---@field public poxi_methods table @ “魄袭”框操作方法表 local Engine = class("Engine") --- Engine的构造函数。 @@ -55,6 +56,7 @@ function Engine:initialize() self.game_mode_disabled = {} self.kingdoms = {} self._custom_events = {} + self.poxi_methods = {} self:loadPackages() self:loadDisabled() @@ -334,6 +336,16 @@ function Engine:addGameEvent(name, pfunc, mfunc, cfunc, efunc) table.insert(self._custom_events, { name = name, p = pfunc, m = mfunc, c = cfunc, e = efunc }) end +---@param spec PoxiSpec +function Engine:addPoxiMethod(spec) + assert(type(spec.name) == "string") + assert(type(spec.card_filter) == "function") + assert(type(spec.feasible) == "function") + self.poxi_methods[spec.name] = spec + spec.default_choice = spec.default_choice or function() return {} end + spec.post_select = spec.post_select or function(s) return s end +end + --- 从已经开启的拓展包中,随机选出若干名武将。 --- --- 对于同名武将不会重复选取。 diff --git a/lua/fk_ex.lua b/lua/fk_ex.lua index 5d1882b5..3a1bcf85 100644 --- a/lua/fk_ex.lua +++ b/lua/fk_ex.lua @@ -588,3 +588,13 @@ function fk.CreateGameMode(spec) end return ret end + +-- other + +---@class PoxiSpec +---@field name string +---@field card_filter fun(to_select: int, selected: int[], data: any): bool +---@field feasible fun(selected: int[], data: any): bool +---@field post_select nil | fun(selected: int[], data: any): int[] +---@field default_choice nil | fun(data: any): int[] +---@field prompt nil | string | fun(data: any): string diff --git a/lua/lsp/freekill.lua b/lua/lsp/freekill.lua index f5f4ef65..994ce5dc 100644 --- a/lua/lsp/freekill.lua +++ b/lua/lsp/freekill.lua @@ -7,6 +7,7 @@ ---@alias null nil ---@alias bool boolean | nil +---@alias int integer ---@class fk ---FreeKill's lua API diff --git a/lua/server/ai/init.lua b/lua/server/ai/init.lua index 8426698b..2b150ff8 100644 --- a/lua/server/ai/init.lua +++ b/lua/server/ai/init.lua @@ -3,3 +3,25 @@ AI = require "server.ai.ai" TrustAI = require "server.ai.trust_ai" RandomAI = require "server.ai.random_ai" +SmartAI = require "server.ai.smart_ai" + +-- load ai module from packages +local directories = FileIO.ls("packages") +require "packages.standard.ai" +require "packages.standard_cards.ai" +require "packages.maneuvering.ai" +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 + if (not string.find(dir, ".disabled")) and not table.contains(_disable_packs, dir) + and FileIO.isDir("packages/" .. dir) + and FileIO.exists("packages/" .. dir .. "/ai/init.lua") then + + require(string.format("packages.%s.ai", dir)) + + end +end diff --git a/lua/server/ai/smart_ai.lua b/lua/server/ai/smart_ai.lua new file mode 100644 index 00000000..184d439b --- /dev/null +++ b/lua/server/ai/smart_ai.lua @@ -0,0 +1,10 @@ +-- SPDX-License-Identifier: GPL-3.0-or-later + +---@class SmartAI: AI +local SmartAI = AI:subclass("RandomAI") + +function SmartAI:initialize(player) + AI.initialize(self, player) +end + +return SmartAI diff --git a/lua/server/gameevent.lua b/lua/server/gameevent.lua index 61e4cfbe..4c884f96 100644 --- a/lua/server/gameevent.lua +++ b/lua/server/gameevent.lua @@ -73,6 +73,13 @@ function GameEvent:addExitFunc(f) table.insert(self.extra_exit_funcs, f) end +function GameEvent:prependExitFunc(f) + if self.extra_exit_funcs == Util.DummyTable then + self.extra_exit_funcs = {} + end + table.insert(self.extra_exit_funcs, 1, f) +end + function GameEvent:findParent(eventType, includeSelf) if includeSelf and self.event == eventType then return self end local e = self.parent diff --git a/lua/server/gamelogic.lua b/lua/server/gamelogic.lua index 31d0e44a..b0d28f53 100644 --- a/lua/server/gamelogic.lua +++ b/lua/server/gamelogic.lua @@ -376,14 +376,14 @@ function GameLogic:trigger(event, target, data, refresh_only) end repeat do - local triggerables = table.filter(skills, function(skill) + local invoked_skills = {} + local filter_func = function(skill) return skill.priority_table[event] == prio and + not table.contains(invoked_skills, skill) and skill:triggerable(event, target, player, data) - end) + end - local skill_names = table.map(triggerables, function(skill) - return skill.name - end) + local skill_names = table.map(table.filter(skills, filter_func), Util.NameMapper) while #skill_names > 0 do local skill_name = prio <= 0 and table.random(skill_names) or @@ -392,23 +392,14 @@ function GameLogic:trigger(event, target, data, refresh_only) local skill = skill_name == "game_rule" and GameRule or Fk.skills[skill_name] - local len = #skills + table.insert(invoked_skills, skill) broken = skill:trigger(event, target, player, data) - - table.insertTable( - skill_names, - table.map(table.filter(table.slice(skills, len - #skills), function(s) - return - s.priority_table[event] == prio and - s:triggerable(event, target, player, data) - end), function(s) return s.name end) - ) + skill_names = table.map(table.filter(skills, filter_func), Util.NameMapper) broken = broken or (event == fk.AskForPeaches and room:getPlayerById(data.who).hp > 0) if broken then break end - table.removeOne(skill_names, skill_name) end if broken then break end @@ -428,6 +419,18 @@ function GameLogic:getCurrentEvent() return self.game_event_stack.t[self.game_event_stack.p] end +--- 如果当前事件刚好是技能生效事件,就返回那个技能名,否则返回空串。 +function GameLogic:getCurrentSkillName() + local skillEvent = self:getCurrentEvent() + local ret = "" + if skillEvent.event == GameEvent.SkillEffect then + local _, _, _skill = table.unpack(skillEvent.data) + local skill = _skill.main_skill and _skill.main_skill or _skill + ret = skill.name + end + return ret +end + -- 在指定历史范围中找至多n个符合条件的事件 ---@param eventType integer @ 要查找的事件类型 ---@param n integer @ 最多找多少个 diff --git a/lua/server/room.lua b/lua/server/room.lua index 65cd0791..721cc1ef 100644 --- a/lua/server/room.lua +++ b/lua/server/room.lua @@ -1457,6 +1457,33 @@ function Room:askForCardsChosen(chooser, target, min, max, flag, reason) return new_ret end +--- 谋askForCardsChosen,需使用Fk:addPoxiMethod定义好方法 +--- +--- 选卡规则和返回值啥的全部自己想办法解决,data填入所有卡的列表(类似ui.card_data) +--- +--- 注意一定要返回一个表,毕竟本质上是选卡函数 +---@param player ServerPlayer +---@param poxi_type string +---@param data any +---@return integer[] +function Room:askForPoxi(player, poxi_type, data) + local poxi = Fk.poxi_methods[poxi_type] + if not poxi then return {} end + + local command = "AskForPoxi" + self:notifyMoveFocus(player, poxi_type) + local result = self:doRequest(player, command, json.encode { + type = poxi_type, + data = data, + }) + + if result == "" then + return poxi.default_choice(data) + else + return poxi.post_select(json.decode(result), data) + end +end + --- 询问一名玩家从众多选项中选择一个。 ---@param player ServerPlayer @ 要询问的玩家 ---@param choices string[] @ 可选选项列表 diff --git a/lua/server/serverplayer.lua b/lua/server/serverplayer.lua index e07d2b90..4f253350 100644 --- a/lua/server/serverplayer.lua +++ b/lua/server/serverplayer.lua @@ -469,7 +469,7 @@ function ServerPlayer:gainAnExtraPhase(phase, delay) local logic = room.logic local turn = logic:getCurrentEvent():findParent(GameEvent.Phase, true) if turn then - turn:addExitFunc(function() self:gainAnExtraPhase(phase, false) end) + turn:prependExitFunc(function() self:gainAnExtraPhase(phase, false) end) return end end @@ -484,7 +484,6 @@ function ServerPlayer:gainAnExtraPhase(phase, delay) arg = phase_name_table[phase], } - GameEvent(GameEvent.Phase, self, self.phase):exec() self.phase = current @@ -580,6 +579,7 @@ function ServerPlayer:skip(phase) end end +--- 当进行到出牌阶段空闲点时,结束出牌阶段。 function ServerPlayer:endPlayPhase() self._play_phase_end = true -- TODO: send log @@ -592,7 +592,7 @@ function ServerPlayer:gainAnExtraTurn(delay) local logic = room.logic local turn = logic:getCurrentEvent():findParent(GameEvent.Turn, true) if turn then - turn:addExitFunc(function() self:gainAnExtraTurn(false) end) + turn:prependExitFunc(function() self:gainAnExtraTurn(false) end) return end end @@ -604,10 +604,32 @@ function ServerPlayer:gainAnExtraTurn(delay) 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"] + local skillName = room.logic:getCurrentSkillName() + table.insert(ex_tag, skillName) + GameEvent(GameEvent.Turn, self):exec() + + table.remove(ex_tag) + room.current = current end +function ServerPlayer:insideExtraTurn() + return self.tag["_extra_turn_count"] and #self.tag["_extra_turn_count"] > 0 +end + +---@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] +end + function ServerPlayer:drawCards(num, skillName, fromPlace) return self.room:drawCards(self, num, skillName, fromPlace) end diff --git a/packages/maneuvering/ai/init.lua b/packages/maneuvering/ai/init.lua new file mode 100644 index 00000000..e69de29b diff --git a/packages/standard/ai/init.lua b/packages/standard/ai/init.lua new file mode 100644 index 00000000..e69de29b diff --git a/packages/standard_cards/ai/init.lua b/packages/standard_cards/ai/init.lua new file mode 100644 index 00000000..e69de29b diff --git a/packages/test/init.lua b/packages/test/init.lua index 17ab4006..95ce37d8 100644 --- a/packages/test/init.lua +++ b/packages/test/init.lua @@ -90,6 +90,12 @@ local control = fk.CreateActiveSkill{ -- room:swapSeat(from, to) for _, pid in ipairs(effect.tos) do local to = room:getPlayerById(pid) + -- p(room:askForPoxi(from, "test", { + -- { "你自己", from:getCardIds "h" }, + -- { "对方", to:getCardIds "h" }, + -- })) + -- room:setPlayerMark(from, "@$a", {1,2,3}) + -- room:setPlayerMark(from, "@$b", {'slash','duel','axe'}) if to:getMark("mouxushengcontrolled") == 0 then room:addPlayerMark(to, "mouxushengcontrolled") from:control(to) @@ -124,6 +130,22 @@ local control = fk.CreateActiveSkill{ -- room:useVirtualCard("slash", nil, from, room:getOtherPlayers(from), self.name, true) end, } +--[[ +Fk:addPoxiMethod{ + name = "test", + card_filter = function(to_select, selected, data) + local s = Fk:getCardById(to_select).suit + for _, id in ipairs(selected) do + if Fk:getCardById(id).suit == s then return false end + end + return true + end, + feasible = function(selected, data) + return #selected == 0 or #selected == 4 + end, + prompt = "魄袭:选你们俩手牌总共四个花色,或者不选直接按确定按钮" +} +--]] local test_vs = fk.CreateViewAsSkill{ name = "test_vs", pattern = "nullification",