From 59da054a2d80a36504a278348168601d896d7d48 Mon Sep 17 00:00:00 2001 From: notify Date: Thu, 21 Sep 2023 23:21:28 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 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/SkillButton.qml | 14 +- README.md | 6 - lua/.vscode/settings.json | 14 + lua/client/client_util.lua | 1021 ++++++------ lua/client/i18n/en_US.lua | 2 +- lua/client/i18n/zh_CN.lua | 2 +- lua/core/engine.lua | 12 - lua/core/player.lua | 69 +- lua/fk_ex.lua | 10 - lua/lsp/freekill.lua | 1 - lua/server/ai/ai说明.txt | 45 + lua/server/ai/init.lua | 22 - lua/server/ai/random_ai.lua | 410 ++--- lua/server/ai/trust_ai.lua | 1082 ++++++++++++- lua/server/events/usecard.lua | 132 +- lua/server/gameevent.lua | 7 - lua/server/gamelogic.lua | 765 ++++----- lua/server/room.lua | 1405 ++++++++++------- lua/server/room.lua.rej | 46 + lua/server/serverplayer.lua | 34 +- packages/maneuvering/init.lua | 805 +++++----- packages/maneuvering/maneuvering_ai.lua | 140 ++ packages/standard/game_rule.lua | 143 +- packages/standard/init.lua | 596 +++---- packages/standard/standard_ai.lua | 227 +++ packages/standard_cards/i18n/zh_CN.lua | 17 +- packages/standard_cards/init.lua | 848 +++++----- packages/standard_cards/standard_cards_ai.lua | 407 +++++ packages/test/init.lua | 22 - 37 files changed, 5265 insertions(+), 3174 deletions(-) create mode 100644 lua/.vscode/settings.json create mode 100644 lua/server/ai/ai说明.txt create mode 100644 lua/server/room.lua.rej create mode 100644 packages/maneuvering/maneuvering_ai.lua create mode 100644 packages/standard/standard_ai.lua create mode 100644 packages/standard_cards/standard_cards_ai.lua diff --git a/Fk/Pages/Lobby.qml b/Fk/Pages/Lobby.qml index 317acfdd..7d4fd364 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").arg(roomModel.count) + text: Backend.translate("Room List") } ListView { id: roomList diff --git a/Fk/Pages/Room.qml b/Fk/Pages/Room.qml index 0194157f..a8622381 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 03c32b55..35bb4c5c 100644 --- a/Fk/Pages/RoomLogic.js +++ b/Fk/Pages/RoomLogic.js @@ -1070,31 +1070,6 @@ 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 adda007c..ebf98e67 100644 --- a/Fk/PhotoElement/MarkArea.qml +++ b/Fk/PhotoElement/MarkArea.qml @@ -62,12 +62,7 @@ Item { } if (mark_name.startsWith('@$')) { - let data = mark_extra.split(','); - if (!Object.is(parseInt(data[0]), NaN)) { - params.ids = data.map(s => parseInt(s)); - } else { - params.cardNames = data; - } + params.cardNames = mark_extra.split(','); } 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 d74ea8d3..c41d789a 100644 --- a/Fk/RoomElement/CardItem.qml +++ b/Fk/RoomElement/CardItem.qml @@ -31,13 +31,11 @@ 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 != "") @@ -90,7 +88,6 @@ Item { visible: false } - Image { id: cardItem source: known ? SkinBank.getCardPicture(cid || name) @@ -220,15 +217,6 @@ 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 @@ -236,24 +224,6 @@ 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 09faeb03..2158a9eb 100644 --- a/Fk/RoomElement/Dashboard.qml +++ b/Fk/RoomElement/Dashboard.qml @@ -166,35 +166,17 @@ 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; - } } }); @@ -220,7 +202,6 @@ 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 { @@ -233,14 +214,6 @@ 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) @@ -399,11 +372,6 @@ 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 2d96bea1..3b282ff1 100644 --- a/Fk/RoomElement/HandcardArea.qml +++ b/Fk/RoomElement/HandcardArea.qml @@ -48,7 +48,6 @@ 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 e9d4a5bf..d6554702 100644 --- a/Fk/RoomElement/PlayerCardBox.qml +++ b/Fk/RoomElement/PlayerCardBox.qml @@ -78,10 +78,10 @@ GraphicsBox { } onSelectedChanged: { if (selected) { - chosenInBox = true; + virt_name = "$Selected"; root.selected_ids.push(cid); } else { - chosenInBox = false; + virt_name = ""; root.selected_ids.splice(root.selected_ids.indexOf(cid), 1); } root.selected_ids = root.selected_ids; @@ -122,6 +122,38 @@ 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/SkillButton.qml b/Fk/RoomElement/SkillButton.qml index d7c9c7f7..09459869 100644 --- a/Fk/RoomElement/SkillButton.qml +++ b/Fk/RoomElement/SkillButton.qml @@ -23,17 +23,9 @@ Item { x: -13 - 120 * 0.166 y: -6 - 55 * 0.166 scale: 0.66 - 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; - } + source: type === "notactive" ? "" + : AppPath + "/image/button/skill/" + type + "/" + + (enabled ? (pressed ? "pressed" : "normal") : "disabled") } Image { diff --git a/README.md b/README.md index 4ab0d5d0..bf763cc9 100644 --- a/README.md +++ b/README.md @@ -63,9 +63,3 @@ ___ ## 许可证 本仓库使用GPLv3作为许可证。详见`LICENSE`文件。 - -___ - -## 点一下小星星呗! - -[![Star History Chart](https://api.star-history.com/svg?repos=Qsgs-Fans/FreeKill&type=Date)](https://star-history.com/#Qsgs-Fans/FreeKill&Date) diff --git a/lua/.vscode/settings.json b/lua/.vscode/settings.json new file mode 100644 index 00000000..fb039827 --- /dev/null +++ b/lua/.vscode/settings.json @@ -0,0 +1,14 @@ +{ + "editor.renderLineHighlight": "none", + "Lua.diagnostics.disable": [ + "undefined-field", + "inject-field", + "return-type-mismatch", + "cast-local-type", + "param-type-mismatch", + "invisible", + "missing-fields", + "assign-type-mismatch", + "undefined-doc-name" + ] +} \ No newline at end of file diff --git a/lua/client/client_util.lua b/lua/client/client_util.lua index e2529057..6a3966d7 100644 --- a/lua/client/client_util.lua +++ b/lua/client/client_util.lua @@ -1,735 +1,698 @@ -- SPDX-License-Identifier: GPL-3.0-or-later - -- All functions in this file are used by Qml - function Translate(src) - return Fk:translate(src) + return Fk:translate(src) end function GetGeneralData(name) - local general = Fk.generals[name] - if general == nil then general = Fk.generals["diaochan"] end - return json.encode { - package = general.package.name, - extension = general.package.extensionName, - kingdom = general.kingdom, - subkingdom = general.subkingdom, - hp = general.hp, - maxHp = general.maxHp, - shield = general.shield, - hidden = general.hidden, - total_hidden = general.total_hidden, - } + local general = Fk.generals[name] + if general == nil then + general = Fk.generals["diaochan"] + end + return json.encode { + package = general.package.name, + extension = general.package.extensionName, + kingdom = general.kingdom, + subkingdom = general.subkingdom, + hp = general.hp, + maxHp = general.maxHp, + shield = general.shield, + hidden = general.hidden, + total_hidden = general.total_hidden + } end function GetGeneralDetail(name) - local general = Fk.generals[name] - if general == nil then general = Fk.generals["diaochan"] end - local ret = { - package = general.package.name, - extension = general.package.extensionName, - kingdom = general.kingdom, - hp = general.hp, - maxHp = general.maxHp, - gender = general.gender, - skill = {}, - related_skill = {}, - companions = general.companions - } - for _, s in ipairs(general.skills) do - table.insert(ret.skill, { - name = s.name, - description = Fk:getDescription(s.name) - }) - end - for _, s in ipairs(general.other_skills) do - table.insert(ret.skill, { - name = s, - description = Fk:getDescription(s) - }) - end - for _, s in ipairs(general.related_skills) do - table.insert(ret.related_skill, { - name = s.name, - description = Fk:getDescription(s.name) - }) - end - for _, s in ipairs(general.related_other_skills) do - table.insert(ret.related_skill, { - name = s, - description = Fk:getDescription(s) - }) - end - for _, g in pairs(Fk.generals) do - if table.contains(g.companions, general.name) then - table.insertIfNeed(ret.companions, g.name) + local general = Fk.generals[name] + if general == nil then + general = Fk.generals["diaochan"] end - end - return json.encode(ret) + local ret = { + package = general.package.name, + extension = general.package.extensionName, + kingdom = general.kingdom, + hp = general.hp, + maxHp = general.maxHp, + gender = general.gender, + skill = {}, + related_skill = {}, + companions = general.companions + } + for _, s in ipairs(general.skills) do + table.insert(ret.skill, { + name = s.name, + description = Fk:getDescription(s.name) + }) + end + for _, s in ipairs(general.other_skills) do + table.insert(ret.skill, { + name = s, + description = Fk:getDescription(s) + }) + end + for _, s in ipairs(general.related_skills) do + table.insert(ret.related_skill, { + name = s.name, + description = Fk:getDescription(s.name) + }) + end + for _, s in ipairs(general.related_other_skills) do + table.insert(ret.related_skill, { + name = s, + description = Fk:getDescription(s) + }) + end + for _, g in pairs(Fk.generals) do + if table.contains(g.companions, general.name) then + table.insertIfNeed(ret.companions, g.name) + end + end + return json.encode(ret) end function GetSameGenerals(name) - return json.encode(Fk:getSameGenerals(name)) + return json.encode(Fk:getSameGenerals(name)) end local cardSubtypeStrings = { - [Card.SubtypeNone] = "none", - [Card.SubtypeDelayedTrick] = "delayed_trick", - [Card.SubtypeWeapon] = "weapon", - [Card.SubtypeArmor] = "armor", - [Card.SubtypeDefensiveRide] = "defensive_horse", - [Card.SubtypeOffensiveRide] = "offensive_horse", - [Card.SubtypeTreasure] = "treasure", + [Card.SubtypeNone] = "none", + [Card.SubtypeDelayedTrick] = "delayed_trick", + [Card.SubtypeWeapon] = "weapon", + [Card.SubtypeArmor] = "armor", + [Card.SubtypeDefensiveRide] = "defensive_horse", + [Card.SubtypeOffensiveRide] = "offensive_horse", + [Card.SubtypeTreasure] = "treasure" } function GetCardData(id, virtualCardForm) - local card = Fk:getCardById(id) - if card == nil then return json.encode{ - cid = id, - known = false - } end - local mark = {} - for k, v in pairs(card.mark) do - if k and k:startsWith("@") and v and v ~= 0 then - table.insert(mark, { - k = k, v = v, - }) + local card = Fk:getCardById(id) + if card == nil then + return json.encode { + cid = id, + known = false + } end - end - local ret = { - cid = id, - name = card.name, - extension = card.package.extensionName, - number = card.number, - suit = card:getSuitString(), - color = card:getColorString(), - mark = mark, - type = card.type, - subtype = cardSubtypeStrings[card.sub_type] - } - if card.skillName ~= "" then - local orig = Fk:getCardById(id, true) - ret.name = orig.name - ret.virt_name = card.name - end - if virtualCardForm then - local virtualCard = ClientInstance:getPlayerById(virtualCardForm):getVirualEquip(id) - if virtualCard then - ret.virt_name = virtualCard.name - ret.subtype = cardSubtypeStrings[virtualCard.sub_type] + local mark = {} + for k, v in pairs(card.mark) do + if k and k:startsWith("@") and v and v ~= 0 then + table.insert(mark, { + k = k, + v = v + }) + end end - end - return json.encode(ret) + local ret = { + cid = id, + name = card.name, + extension = card.package.extensionName, + number = card.number, + suit = card:getSuitString(), + color = card:getColorString(), + mark = mark, + type = card.type, + subtype = cardSubtypeStrings[card.sub_type] + } + if card.skillName ~= "" then + local orig = Fk:getCardById(id, true) + ret.name = orig.name + ret.virt_name = card.name + end + if virtualCardForm then + local virtualCard = ClientInstance:getPlayerById(virtualCardForm):getVirualEquip(id) + if virtualCard then + ret.virt_name = virtualCard.name + ret.subtype = cardSubtypeStrings[virtualCard.sub_type] + end + end + return json.encode(ret) end function GetCardExtensionByName(cardName) - local card = table.find(Fk.cards, function(card) - return card.name == cardName - end) + local card = table.find(Fk.cards, function(card) + return card.name == cardName + end) - return card and card.package.extensionName or "" + return card and card.package.extensionName or "" end function GetAllGeneralPack() - local ret = {} - for _, name in ipairs(Fk.package_names) do - if Fk.packages[name].type == Package.GeneralPack then - table.insert(ret, name) + local ret = {} + for _, name in ipairs(Fk.package_names) do + if Fk.packages[name].type == Package.GeneralPack then + table.insert(ret, name) + end end - end - return json.encode(ret) + return json.encode(ret) end function GetGenerals(pack_name) - local ret = {} - for _, g in ipairs(Fk.packages[pack_name].generals) do - if not g.total_hidden then - table.insert(ret, g.name) + local ret = {} + for _, g in ipairs(Fk.packages[pack_name].generals) do + if not g.total_hidden then + table.insert(ret, g.name) + end end - end - return json.encode(ret) + return json.encode(ret) end function SearchAllGenerals(word) - local ret = {} - for _, name in ipairs(Fk.package_names) do - if Fk.packages[name].type == Package.GeneralPack then - table.insertTable(ret, json.decode(SearchGenerals(name, word))) + local ret = {} + for _, name in ipairs(Fk.package_names) do + if Fk.packages[name].type == Package.GeneralPack then + table.insertTable(ret, json.decode(SearchGenerals(name, word))) + end end - end - return json.encode(ret) + return json.encode(ret) end function SearchGenerals(pack_name, word) - local ret = {} - if word == "" then return GetGenerals(pack_name) end - for _, g in ipairs(Fk.packages[pack_name].generals) do - if not g.total_hidden and string.find(Fk:translate(g.name), word) then - table.insert(ret, g.name) + local ret = {} + if word == "" then + return GetGenerals(pack_name) end - end - return json.encode(ret) + for _, g in ipairs(Fk.packages[pack_name].generals) do + if not g.total_hidden and string.find(Fk:translate(g.name), word) then + table.insert(ret, g.name) + end + end + return json.encode(ret) end function UpdatePackageEnable(pkg, enabled) - if enabled then - table.removeOne(ClientInstance.disabled_packs, pkg) - else - table.insertIfNeed(ClientInstance.disabled_packs, pkg) - end + if enabled then + table.removeOne(ClientInstance.disabled_packs, pkg) + else + table.insertIfNeed(ClientInstance.disabled_packs, pkg) + end end function GetAvailableGeneralsNum() - local generalPool = Fk:getAllGenerals() - local except = {} - local ret = 0 - for _, g in ipairs(Fk.packages["test_p_0"].generals) do - table.insert(except, g.name) - end - - local availableGenerals = {} - for _, general in pairs(generalPool) do - if not table.contains(except, general.name) then - if (not general.hidden and not general.total_hidden) and - #table.filter(availableGenerals, function(g) - return g.trueName == general.trueName - end) == 0 then - ret = ret + 1 - end + local generalPool = Fk:getAllGenerals() + local except = {} + local ret = 0 + for _, g in ipairs(Fk.packages["test_p_0"].generals) do + table.insert(except, g.name) end - end - return ret + local availableGenerals = {} + for _, general in pairs(generalPool) do + if not table.contains(except, general.name) then + if (not general.hidden and not general.total_hidden) and #table.filter(availableGenerals, function(g) + return g.trueName == general.trueName + end) == 0 then + ret = ret + 1 + end + end + end + + return ret end function GetAllCardPack() - local ret = {} - for _, name in ipairs(Fk.package_names) do - if Fk.packages[name].type == Package.CardPack then - table.insert(ret, name) + local ret = {} + for _, name in ipairs(Fk.package_names) do + if Fk.packages[name].type == Package.CardPack then + table.insert(ret, name) + end end - end - return json.encode(ret) + return json.encode(ret) end function GetCards(pack_name) - local ret = {} - for _, c in ipairs(Fk.packages[pack_name].cards) do - table.insert(ret, c.id) - end - return json.encode(ret) + local ret = {} + for _, c in ipairs(Fk.packages[pack_name].cards) do + table.insert(ret, c.id) + end + return json.encode(ret) end function GetCardSpecialSkills(cid) - return json.encode(Fk:getCardById(cid).special_skills or Util.DummyTable) + return json.encode(Fk:getCardById(cid).special_skills or Util.DummyTable) end function DistanceTo(from, to) - local a = ClientInstance:getPlayerById(from) - local b = ClientInstance:getPlayerById(to) - return a:distanceTo(b) + local a = ClientInstance:getPlayerById(from) + local b = ClientInstance:getPlayerById(to) + return a:distanceTo(b) end function GetPile(id, name) - return json.encode(ClientInstance:getPlayerById(id):getPile(name) or {}) + return json.encode(ClientInstance:getPlayerById(id):getPile(name) or {}) end function GetAllPiles(id) - return json.encode(ClientInstance:getPlayerById(id).special_cards or Util.DummyTable) + return json.encode(ClientInstance:getPlayerById(id).special_cards or Util.DummyTable) end function GetPlayerSkills(id) - local p = ClientInstance:getPlayerById(id) - return json.encode(table.map(p.player_skills, function(s) - return s.visible and { - name = s.name, - description = Fk:getDescription(s.name), - } or nil - end)) + local p = ClientInstance:getPlayerById(id) + return json.encode(table.map(p.player_skills, function(s) + return s.visible and { + name = s.name, + description = Fk:getDescription(s.name) + } or nil + end)) end ---@param card string | integer ---@param player integer function CanUseCard(card, player) - local c ---@type Card - if type(card) == "number" then - c = Fk:getCardById(card) - else - local data = json.decode(card) - local skill = Fk.skills[data.skill] - local selected_cards = data.subcards - if skill:isInstanceOf(ViewAsSkill) then - c = skill:viewAs(selected_cards) - if not c then - return "false" - end + local c ---@type Card + if type(card) == "number" then + c = Fk:getCardById(card) else - -- ActiveSkill should return true here - return "true" - end - end - - player = ClientInstance:getPlayerById(player) - local ret = c.skill:canUse(player, c) - ret = ret and not player:prohibitUse(c) - if ret then - local min_target = c.skill:getMinTargetNum() - if min_target > 0 then - for _, p in ipairs(ClientInstance.players) do - if c.skill:targetFilter(p.id, {}, {}, c) then - return "true" + local data = json.decode(card) + local skill = Fk.skills[data.skill] + local selected_cards = data.subcards + if skill:isInstanceOf(ViewAsSkill) then + c = skill:viewAs(selected_cards) + if not c then + return "false" + end + else + -- ActiveSkill should return true here + return "true" end - end - return "false" end - end - return json.encode(ret) + + player = ClientInstance:getPlayerById(player) + local ret = c.skill:canUse(player, c) + ret = ret and not player:prohibitUse(c) + if ret then + local min_target = c.skill:getMinTargetNum() + if min_target > 0 then + for _, p in ipairs(ClientInstance.players) do + if c.skill:targetFilter(p.id, {}, {}, c) then + return "true" + end + end + return "false" + end + end + return json.encode(ret) end function CardProhibitedUse(card) - local c ---@type Card - local ret = false - if type(card) == "number" then - c = Fk:getCardById(card) - else - local data = json.decode(card) - local skill = Fk.skills[data.skill] - local selected_cards = data.subcards - if skill:isInstanceOf(ViewAsSkill) then - c = skill:viewAs(selected_cards) + local c ---@type Card + local ret = false + if type(card) == "number" then + c = Fk:getCardById(card) + else + local data = json.decode(card) + local skill = Fk.skills[data.skill] + local selected_cards = data.subcards + if skill:isInstanceOf(ViewAsSkill) then + c = skill:viewAs(selected_cards) + end end - end - if c == nil then - return "true" - else - ret = Self:prohibitUse(c) - end - return json.encode(ret) + if c == nil then + return "true" + else + ret = Self:prohibitUse(c) + end + return json.encode(ret) end ---@param card string | integer ---@param to_select integer @ id of the target ---@param selected integer[] @ ids of selected targets function CanUseCardToTarget(card, to_select, selected) - if ClientInstance:getPlayerById(to_select).dead then - return "false" - end - 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 ActiveTargetFilter(t.skill, to_select, selected, t.subcards) - end + if ClientInstance:getPlayerById(to_select).dead then + return "false" + end + 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 ActiveTargetFilter(t.skill, to_select, selected, t.subcards) + end - local ret = c.skill:targetFilter(to_select, selected, selected_cards, c) - ret = ret and not Self:isProhibited(Fk:currentRoom():getPlayerById(to_select), c) - return json.encode(ret) + local ret = c.skill:targetFilter(to_select, selected, selected_cards, c) + ret = ret and not Self:isProhibited(Fk:currentRoom():getPlayerById(to_select), c) + return json.encode(ret) end ---@param card string | integer ---@param to_select integer @ id of a card not selected ---@param selected_targets integer[] @ ids of selected players function CanSelectCardForSkill(card, to_select, selected_targets) - local c ---@type Card - local selected_cards - if type(card) == "number" then - c = Fk:getCardById(card) - selected_cards = {card} - else - error() - end + local c ---@type Card + local selected_cards + if type(card) == "number" then + c = Fk:getCardById(card) + selected_cards = {card} + else + error() + end - local ret = c.skill:cardFilter(to_select, selected_cards, selected_targets) - return json.encode(ret) + local ret = c.skill:cardFilter(to_select, selected_cards, selected_targets) + return json.encode(ret) end ---@param card string | integer ---@param selected_targets integer[] @ ids of selected players function CardFeasible(card, selected_targets) - 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 ActiveFeasible(t.skill, selected_targets, t.subcards) - end + 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 ActiveFeasible(t.skill, selected_targets, t.subcards) + end - local ret = c.skill:feasible(selected_targets, selected_cards, Self, c) - return json.encode(ret) + local ret = c.skill:feasible(selected_targets, selected_cards, Self, c) + return json.encode(ret) end -- Handle skills function GetSkillData(skill_name) - local skill = Fk.skills[skill_name] - if not skill then return "null" end - local freq = "notactive" - if skill:isInstanceOf(ActiveSkill) or skill:isInstanceOf(ViewAsSkill) then - freq = "active" - end - local frequency - if skill.frequency == Skill.Limited then - frequency = "limit" - elseif skill.frequency == Skill.Wake then - frequency = "wake" - elseif skill.frequency == Skill.Quest then - frequency = "quest" - end - return json.encode{ - skill = Fk:translate(skill_name), - orig_skill = skill_name, - extension = skill.package.extensionName, - freq = freq, - frequency = frequency, - switchSkillName = skill.switchSkillName, - isViewAsSkill = skill:isInstanceOf(ViewAsSkill), - } + local skill = Fk.skills[skill_name] + if not skill then + return "null" + end + local freq = "notactive" + if skill:isInstanceOf(ActiveSkill) or skill:isInstanceOf(ViewAsSkill) then + freq = "active" + end + local frequency + if skill.frequency == Skill.Limited then + frequency = "limit" + elseif skill.frequency == Skill.Wake then + frequency = "wake" + elseif skill.frequency == Skill.Quest then + frequency = "quest" + end + return json.encode { + skill = Fk:translate(skill_name), + orig_skill = skill_name, + extension = skill.package.extensionName, + freq = freq, + frequency = frequency, + switchSkillName = skill.switchSkillName, + isViewAsSkill = skill:isInstanceOf(ViewAsSkill) + } end function ActiveCanUse(skill_name) - local skill = Fk.skills[skill_name] - local ret = false - if skill then - if skill:isInstanceOf(ActiveSkill) then - ret = skill:canUse(Self) - elseif skill:isInstanceOf(ViewAsSkill) then - ret = skill:enabledAtPlay(Self) - if ret then - local exp = Exppattern:Parse(skill.pattern) - local cnames = {} - for _, m in ipairs(exp.matchers) do - if m.name then - table.insertTable(cnames, m.name) - end - if m.trueName then - table.insertTable(cnames, m.trueName) - end + local skill = Fk.skills[skill_name] + local ret = false + if skill then + if skill:isInstanceOf(ActiveSkill) then + ret = skill:canUse(Self) + elseif skill:isInstanceOf(ViewAsSkill) then + ret = skill:enabledAtPlay(Self) + if ret then + local exp = Exppattern:Parse(skill.pattern) + local cnames = {} + for _, m in ipairs(exp.matchers) do + if m.name then + table.insertTable(cnames, m.name) + end + if m.trueName then + table.insertTable(cnames, m.trueName) + end + end + for _, n in ipairs(cnames) do + local c = Fk:cloneCard(n) + c.skillName = skill_name + ret = c.skill:canUse(Self, c) + if ret then + break + end + end + end end - for _, n in ipairs(cnames) do - local c = Fk:cloneCard(n) - c.skillName = skill_name - ret = c.skill:canUse(Self, c) - if ret then break end - end - end end - end - return json.encode(ret) + return json.encode(ret) end function ActiveSkillPrompt(skill_name, selected, selected_targets) - local skill = Fk.skills[skill_name] - local ret = false - if skill then - if type(skill.prompt) == "function" then - ret = skill:prompt(selected, selected_targets) - else - ret = skill.prompt + local skill = Fk.skills[skill_name] + local ret = false + if skill then + if type(skill.prompt) == "function" then + ret = skill:prompt(selected, selected_targets) + else + ret = skill.prompt + end end - end - return json.encode(ret or "") + return json.encode(ret or "") end function ActiveCardFilter(skill_name, to_select, selected, selected_targets) - local skill = Fk.skills[skill_name] - local ret = false - if skill then - if skill:isInstanceOf(ActiveSkill) then - ret = skill:cardFilter(to_select, selected, selected_targets) - elseif skill:isInstanceOf(ViewAsSkill) then - ret = skill:cardFilter(to_select, selected) + local skill = Fk.skills[skill_name] + local ret = false + if skill then + if skill:isInstanceOf(ActiveSkill) then + ret = skill:cardFilter(to_select, selected, selected_targets) + elseif skill:isInstanceOf(ViewAsSkill) then + ret = skill:cardFilter(to_select, selected) + end end - end - return json.encode(ret) + return json.encode(ret) end function ActiveTargetFilter(skill_name, to_select, selected, selected_cards) - local skill = Fk.skills[skill_name] - local ret = false - if skill then - if skill:isInstanceOf(ActiveSkill) then - ret = skill:targetFilter(to_select, selected, selected_cards) - elseif skill:isInstanceOf(ViewAsSkill) then - local card = skill:viewAs(selected_cards) - if card then - ret = card.skill:targetFilter(to_select, selected, selected_cards, card) - ret = ret and not Self:isProhibited(Fk:currentRoom():getPlayerById(to_select), card) - end + local skill = Fk.skills[skill_name] + local ret = false + if skill then + if skill:isInstanceOf(ActiveSkill) then + ret = skill:targetFilter(to_select, selected, selected_cards) + elseif skill:isInstanceOf(ViewAsSkill) then + local card = skill:viewAs(selected_cards) + if card then + ret = card.skill:targetFilter(to_select, selected, selected_cards, card) + ret = ret and not Self:isProhibited(Fk:currentRoom():getPlayerById(to_select), card) + end + end end - end - return json.encode(ret) + return json.encode(ret) end function ActiveFeasible(skill_name, selected, selected_cards) - local skill = Fk.skills[skill_name] - local ret = false - if skill then - if skill:isInstanceOf(ActiveSkill) then - ret = skill:feasible(selected, selected_cards, Self, nil) - elseif skill:isInstanceOf(ViewAsSkill) then - local card = skill:viewAs(selected_cards) - if card then - ret = card.skill:feasible(selected, selected_cards, Self, card) - end + local skill = Fk.skills[skill_name] + local ret = false + if skill then + if skill:isInstanceOf(ActiveSkill) then + ret = skill:feasible(selected, selected_cards, Self, nil) + elseif skill:isInstanceOf(ViewAsSkill) then + local card = skill:viewAs(selected_cards) + if card then + ret = card.skill:feasible(selected, selected_cards, Self, card) + end + end end - end - return json.encode(ret) + return json.encode(ret) end function CanViewAs(skill_name, card_ids) - local skill = Fk.skills[skill_name] - local ret = false - if skill then - if skill:isInstanceOf(ViewAsSkill) then - ret = skill:viewAs(card_ids) ~= nil - elseif skill:isInstanceOf(ActiveSkill) then - ret = true + local skill = Fk.skills[skill_name] + local ret = false + if skill then + if skill:isInstanceOf(ViewAsSkill) then + ret = skill:viewAs(card_ids) ~= nil + elseif skill:isInstanceOf(ActiveSkill) then + ret = true + end end - end - return json.encode(ret) + return json.encode(ret) end -- card_name may be id, name of card, or json string function CardFitPattern(card_name, pattern) - local exp = Exppattern:Parse(pattern) - local c - local ret = false - if type(card_name) == "number" then - c = Fk:getCardById(card_name) - ret = exp:match(c) - elseif string.sub(card_name, 1, 1) == "{" then - local data = json.decode(card_name) - local skill = Fk.skills[data.skill] - local selected_cards = data.subcards - if skill:isInstanceOf(ViewAsSkill) then - c = skill:viewAs(selected_cards) - if c then + local exp = Exppattern:Parse(pattern) + local c + local ret = false + if type(card_name) == "number" then + c = Fk:getCardById(card_name) ret = exp:match(c) - end + elseif string.sub(card_name, 1, 1) == "{" then + local data = json.decode(card_name) + local skill = Fk.skills[data.skill] + local selected_cards = data.subcards + if skill:isInstanceOf(ViewAsSkill) then + c = skill:viewAs(selected_cards) + if c then + ret = exp:match(c) + end + else + return "true" + end else - return "true" + ret = exp:matchExp(card_name) end - else - ret = exp:matchExp(card_name) - end - return json.encode(ret) + return json.encode(ret) end function SkillFitPattern(skill_name, pattern) - local skill = Fk.skills[skill_name] - local ret = false - if skill and skill.pattern then - local exp = Exppattern:Parse(pattern) - ret = exp:matchExp(skill.pattern) - end - return json.encode(ret) + local skill = Fk.skills[skill_name] + local ret = false + if skill and skill.pattern then + local exp = Exppattern:Parse(pattern) + ret = exp:matchExp(skill.pattern) + end + return json.encode(ret) end function CardProhibitedResponse(card) - local c ---@type Card - local ret = false - if type(card) == "number" then - c = Fk:getCardById(card) - else - local data = json.decode(card) - local skill = Fk.skills[data.skill] - local selected_cards = data.subcards - if skill:isInstanceOf(ViewAsSkill) then - c = skill:viewAs(selected_cards) + local c ---@type Card + local ret = false + if type(card) == "number" then + c = Fk:getCardById(card) + else + local data = json.decode(card) + local skill = Fk.skills[data.skill] + local selected_cards = data.subcards + if skill:isInstanceOf(ViewAsSkill) then + c = skill:viewAs(selected_cards) + end end - end - if c == nil then - return "true" - else - ret = Self:prohibitUse(c) - end - return json.encode(ret) + if c == nil then + return "true" + else + ret = Self:prohibitUse(c) + end + return json.encode(ret) end function SkillCanResponse(skill_name, cardResponsing) - local skill = Fk.skills[skill_name] - local ret = false - if skill and skill:isInstanceOf(ViewAsSkill) then - ret = skill:enabledAtResponse(Self, cardResponsing) - end - return json.encode(ret) + local skill = Fk.skills[skill_name] + local ret = false + if skill and skill:isInstanceOf(ViewAsSkill) then + ret = skill:enabledAtResponse(Self, cardResponsing) + end + return json.encode(ret) end function GetVirtualEquip(player, cid) - local c = ClientInstance:getPlayerById(player):getVirualEquip(cid) - if not c then return "null" end - return json.encode{ - name = c.name, - cid = c.subcards[1], - } + local c = ClientInstance:getPlayerById(player):getVirualEquip(cid) + if not c then + return "null" + end + return json.encode { + name = c.name, + cid = c.subcards[1] + } end function GetExpandPileOfSkill(skillName) - local skill = Fk.skills[skillName] - return skill and (skill.expand_pile or "") or "" + local skill = Fk.skills[skillName] + return skill and (skill.expand_pile or "") or "" end function GetGameModes() - local ret = {} - for k, v in pairs(Fk.game_modes) do - table.insert(ret, { - name = Fk:translate(v.name), - orig_name = v.name, - minPlayer = v.minPlayer, - maxPlayer = v.maxPlayer, - }) - end - table.sort(ret, function(a, b) return a.name > b.name end) - return json.encode(ret) + local ret = {} + for k, v in pairs(Fk.game_modes) do + table.insert(ret, { + name = Fk:translate(v.name), + orig_name = v.name, + minPlayer = v.minPlayer, + maxPlayer = v.maxPlayer + }) + end + table.sort(ret, function(a, b) + return a.name > b.name + end) + return json.encode(ret) end function GetInteractionOfSkill(skill_name) - local skill = Fk.skills[skill_name] - if skill and skill.interaction then - return json.encode(skill:interaction()) - end - return "null" + local skill = Fk.skills[skill_name] + if skill and skill.interaction then + return json.encode(skill:interaction()) + end + return "null" end function SetInteractionDataOfSkill(skill_name, data) - local skill = Fk.skills[skill_name] - if skill and skill.interaction then - skill.interaction.data = json.decode(data) - end + local skill = Fk.skills[skill_name] + if skill and skill.interaction then + skill.interaction.data = json.decode(data) + end end function ChangeSelf(pid) - local c = ClientInstance - c.client:changeSelf(pid) -- for qml - Self = c:getPlayerById(pid) + ClientInstance.client:changeSelf(pid) -- for qml + Self = ClientInstance:getPlayerById(pid) end function GetPlayerHandcards(pid) - local c = ClientInstance - local p = c:getPlayerById(pid) - return json.encode(p.player_cards[Player.Hand]) + local p = ClientInstance:getPlayerById(pid) + return json.encode(p.player_cards[Player.Hand]) end function GetPlayerEquips(pid) - local c = ClientInstance - local p = c:getPlayerById(pid) - return json.encode(p.player_cards[Player.Equip]) + local p = ClientInstance:getPlayerById(pid) + return json.encode(p.player_cards[Player.Equip]) end function ResetClientLua() - local _data = ClientInstance.enter_room_data; - local data = ClientInstance.room_settings - Self = ClientPlayer:new(fk.Self) - ClientInstance = Client:new() -- clear old client data - ClientInstance.players = {Self} - ClientInstance.alive_players = {Self} - ClientInstance.discard_pile = {} + local _data = ClientInstance.enter_room_data; + local data = ClientInstance.room_settings + Self = ClientPlayer:new(fk.Self) + ClientInstance = Client:new() -- clear old client data + ClientInstance.players = {Self} + ClientInstance.alive_players = {Self} + ClientInstance.discard_pile = {} - ClientInstance.enter_room_data = _data; - ClientInstance.room_settings = data + ClientInstance.enter_room_data = _data; + ClientInstance.room_settings = data - ClientInstance.disabled_packs = data.disabledPack - ClientInstance.disabled_generals = data.disabledGenerals - -- ClientInstance:notifyUI("EnterRoom", jsonData) + ClientInstance.disabled_packs = data.disabledPack + ClientInstance.disabled_generals = data.disabledGenerals + -- ClientInstance:notifyUI("EnterRoom", jsonData) end function ResetAddPlayer(j) - fk.client_callback["AddPlayer"](j) + fk.client_callback["AddPlayer"](j) end function GetRoomConfig() - return json.encode(ClientInstance.room_settings) + return json.encode(ClientInstance.room_settings) end function GetPlayerGameData(pid) - local c = ClientInstance - local p = c:getPlayerById(pid) - if not p then return "[0, 0, 0]" end - local raw = p.player:getGameData() - local ret = {} - for _, i in fk.qlist(raw) do - table.insert(ret, i) - end - return json.encode(ret) + local p = ClientInstance:getPlayerById(pid) + if not p then + return "[0, 0, 0]" + end + local raw = p.player:getGameData() + local ret = {} + for _, i in fk.qlist(raw) do + table.insert(ret, i) + end + return json.encode(ret) end function SetPlayerGameData(pid, data) - local c = ClientInstance - local p = c:getPlayerById(pid) - p.player:setGameData(table.unpack(data)) - table.insert(data, 1, pid) - ClientInstance:notifyUI("UpdateGameData", json.encode(data)) + local p = ClientInstance:getPlayerById(pid) + p.player:setGameData(table.unpack(data)) + table.insert(data, 1, pid) + ClientInstance:notifyUI("UpdateGameData", json.encode(data)) end function FilterMyHandcards() - Self:filterHandcards() + Self:filterHandcards() end function SetObserving(o) - ClientInstance.observing = o + ClientInstance.observing = o end function CheckSurrenderAvailable(playedTime) - local curMode = ClientInstance.room_settings.gameMode - return json.encode(Fk.game_modes[curMode]:surrenderFunc(playedTime)) + local curMode = ClientInstance.room_settings.gameMode + return json.encode(Fk.game_modes[curMode]:surrenderFunc(playedTime)) end function SaveRecord() - local c = ClientInstance - 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)) + local c = ClientInstance + c.client:saveRecord(json.encode(c.record), c.record[2]) end dofile "lua/client/i18n/init.lua" diff --git a/lua/client/i18n/en_US.lua b/lua/client/i18n/en_US.lua index 13c6ad0e..983c4489 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 (currently have %1 rooms)", + -- ["Room List"] = "房间列表", -- ["Enter"] = "进入", -- ["Observe"] = "旁观", diff --git a/lua/client/i18n/zh_CN.lua b/lua/client/i18n/zh_CN.lua index 5071ee73..14137fb5 100644 --- a/lua/client/i18n/zh_CN.lua +++ b/lua/client/i18n/zh_CN.lua @@ -2,7 +2,7 @@ Fk:loadTranslationTable{ -- Lobby - ["Room List"] = "房间列表 (共%1个房间)", + ["Room List"] = "房间列表", ["Enter"] = "进入", ["Observe"] = "旁观", diff --git a/lua/core/engine.lua b/lua/core/engine.lua index ebbb63e6..3cd53dce 100644 --- a/lua/core/engine.lua +++ b/lua/core/engine.lua @@ -25,7 +25,6 @@ ---@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的构造函数。 @@ -56,7 +55,6 @@ function Engine:initialize() self.game_mode_disabled = {} self.kingdoms = {} self._custom_events = {} - self.poxi_methods = {} self:loadPackages() self:loadDisabled() @@ -336,16 +334,6 @@ 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/core/player.lua b/lua/core/player.lua index 5d978940..72ca5657 100644 --- a/lua/core/player.lua +++ b/lua/core/player.lua @@ -136,8 +136,10 @@ function Player:setGeneral(general, setHp, addSkills) end function Player:getGeneralMaxHp() - local general = Fk.generals[type(self:getMark("__heg_general")) == "string" and self:getMark("__heg_general") or self.general] - local deputy = Fk.generals[type(self:getMark("__heg_deputy")) == "string" and self:getMark("__heg_deputy") or self.deputyGeneral] + local general = Fk.generals + [type(self:getMark("__heg_general")) == "string" and self:getMark("__heg_general") or self.general] + local deputy = Fk.generals + [type(self:getMark("__heg_deputy")) == "string" and self:getMark("__heg_deputy") or self.deputyGeneral] if not deputy then return general.maxHp + general.mainMaxHpAdjustedValue @@ -267,7 +269,7 @@ function Player:removeCards(playerArea, cardIds, specialName) if table.contains(fromAreaIds, id) then table.removeOne(fromAreaIds, id) - -- FIXME: 为客户端移动id为-1的牌考虑,但总感觉有地方需要商讨啊! + -- FIXME: 为客户端移动id为-1的牌考虑,但总感觉有地方需要商讨啊! elseif table.every(fromAreaIds, function(e) return e == -1 end) then table.remove(fromAreaIds, 1) elseif id == -1 then @@ -329,12 +331,18 @@ end function Player:getCardIds(playerAreas, specialName) local rightAreas = { Player.Hand, Player.Equip, Player.Judge } playerAreas = playerAreas or rightAreas + local cardIds = {} if type(playerAreas) == "string" then local str = playerAreas playerAreas = {} if str:find("h") then table.insert(playerAreas, Player.Hand) end + if str:find("&") then + for k, v in pairs(self.special_cards) do + if k:endsWith("&") then table.insertTable(cardIds, v) end + end + end if str:find("e") then table.insert(playerAreas, Player.Equip) end @@ -346,7 +354,6 @@ function Player:getCardIds(playerAreas, specialName) local areas = type(playerAreas) == "table" and playerAreas or { playerAreas } local rightAreas = { Player.Hand, Player.Equip, Player.Judge, Player.Special } - local cardIds = {} for _, area in ipairs(areas) do assert(table.contains(rightAreas, area)) assert(area ~= Player.Special or type(specialName) == "string") @@ -510,7 +517,8 @@ function Player:distanceTo(other, mode, ignore_dead) if temp ~= other then print("Distance malfunction: start and end does not matched.") end - local left = #(ignore_dead and Fk:currentRoom().players or Fk:currentRoom().alive_players) - right - #table.filter(Fk:currentRoom().alive_players, function(p) return p:isRemoved() end) + local left = #(ignore_dead and Fk:currentRoom().players or Fk:currentRoom().alive_players) - right - + #table.filter(Fk:currentRoom().alive_players, function(p) return p:isRemoved() end) local ret = 0 if mode == "left" then ret = left @@ -586,7 +594,7 @@ function Player:addCardUseHistory(cardName, num) num = num or 1 assert(type(num) == "number" and num ~= 0) - self.cardUsedHistory[cardName] = self.cardUsedHistory[cardName] or {0, 0, 0, 0} + self.cardUsedHistory[cardName] = self.cardUsedHistory[cardName] or { 0, 0, 0, 0 } local t = self.cardUsedHistory[cardName] for i, _ in ipairs(t) do t[i] = t[i] + num @@ -623,7 +631,7 @@ function Player:addSkillUseHistory(skill_name, num) num = num or 1 assert(type(num) == "number" and num ~= 0) - self.skillUsedHistory[skill_name] = self.skillUsedHistory[skill_name] or {0, 0, 0, 0} + self.skillUsedHistory[skill_name] = self.skillUsedHistory[skill_name] or { 0, 0, 0, 0 } local t = self.skillUsedHistory[skill_name] for i, _ in ipairs(t) do t[i] = t[i] + num @@ -648,7 +656,7 @@ function Player:setSkillUseHistory(skill_name, num, scope) return end - self.skillUsedHistory[skill_name] = self.skillUsedHistory[skill_name] or {0, 0, 0, 0} + self.skillUsedHistory[skill_name] = self.skillUsedHistory[skill_name] or { 0, 0, 0, 0 } self.skillUsedHistory[skill_name][scope] = num end @@ -681,7 +689,7 @@ end --- 获取玩家是否无手牌及装备牌。 function Player:isNude() - return #self:getCardIds{Player.Hand, Player.Equip} == 0 + return #self:getCardIds { Player.Hand, Player.Equip } == 0 end --- 获取玩家所有区域是否无牌。 @@ -729,9 +737,8 @@ function Player:hasSkill(skill, ignoreNullified, ignoreAlive) end if self:isInstanceOf(ServerPlayer) and -- isInstanceOf(nil) will return false - table.contains(self._fake_skills, skill) and - table.contains(self.prelighted_skills, skill) then - + table.contains(self._fake_skills, skill) and + table.contains(self.prelighted_skills, skill) then return true end @@ -751,7 +758,7 @@ end function Player:addSkill(skill, source_skill) skill = getActualSkill(skill) - local toget = {table.unpack(skill.related_skills)} + local toget = { table.unpack(skill.related_skills) } table.insert(toget, skill) local room = Fk:currentRoom() @@ -812,7 +819,7 @@ function Player:loseSkill(skill, source_skill) table.insert(tolose, skill) self.derivative_skills[skill] = nil - local ret = {} ---@type Skill[] + local ret = {} ---@type Skill[] for _, s in ipairs(tolose) do if not self:hasSkill(s, true, true) then table.insert(ret, s) @@ -824,7 +831,7 @@ end --- 获取对应玩家所有技能。 -- return all skills that xxx:hasSkill() == true function Player:getAllSkills() - local ret = {table.unpack(self.player_skills)} + local ret = { table.unpack(self.player_skills) } for _, t in pairs(self.derivative_skills) do for _, s in ipairs(t) do table.insertIfNeed(ret, s) @@ -844,8 +851,7 @@ end ---@param to Player @ 特定玩家 ---@param card Card @ 特定牌 function Player:isProhibited(to, card) - local r = Fk:currentRoom() - + if type(card) == "number" then card = Fk:getCardById(card) end if card.type == Card.TypeEquip and #to:getAvailableEquipSlots(card.sub_type) == 0 then return true end @@ -855,7 +861,12 @@ function Player:isProhibited(to, card) return true end - local status_skills = r.status_skills[ProhibitSkill] or Util.DummyTable + if fk.useMustTargets and + not table.contains(fk.useMustTargets, to.id) then + return true + end + + local status_skills = Fk:currentRoom().status_skills[ProhibitSkill] or Util.DummyTable for _, skill in ipairs(status_skills) do if skill:isProhibited(self, to, card) then return true @@ -907,8 +918,8 @@ function Player:prohibitReveal(isDeputy) return true end for _, m in ipairs(table.map(MarkEnum.TempMarkSuffix, function(s) - return self:getMark(MarkEnum.RevealProhibited .. s) - end)) do + return self:getMark(MarkEnum.RevealProhibited .. s) + end)) do if type(m) == "table" and table.contains(m, place) then return true end @@ -928,9 +939,11 @@ fk.SwitchYin = 1 ---@return number|string @ 转换技状态 function Player:getSwitchSkillState(skillName, afterUse, inWord) if afterUse then - return self:getMark(MarkEnum.SwithSkillPreName .. skillName) < 1 and (inWord and "yin" or fk.SwitchYin) or (inWord and "yang" or fk.SwitchYang) + return self:getMark(MarkEnum.SwithSkillPreName .. skillName) < 1 and (inWord and "yin" or fk.SwitchYin) or + (inWord and "yang" or fk.SwitchYang) else - return self:getMark(MarkEnum.SwithSkillPreName .. skillName) < 1 and (inWord and "yang" or fk.SwitchYang) or (inWord and "yin" or fk.SwitchYin) + return self:getMark(MarkEnum.SwithSkillPreName .. skillName) < 1 and (inWord and "yang" or fk.SwitchYang) or + (inWord and "yin" or fk.SwitchYin) end end @@ -946,12 +959,12 @@ function Player:canMoveCardInBoardTo(to, id) return to:hasEmptyEquipSlot(card.sub_type) else return - not ( - table.find(to:getCardIds(Player.Judge), function(cardId) - return Fk:getCardById(cardId).name == card.name - end) or - table.contains(to.sealedSlots, Player.JudgeSlot) - ) + not ( + table.find(to:getCardIds(Player.Judge), function(cardId) + return Fk:getCardById(cardId).name == card.name + end) or + table.contains(to.sealedSlots, Player.JudgeSlot) + ) end end diff --git a/lua/fk_ex.lua b/lua/fk_ex.lua index 3a1bcf85..5d1882b5 100644 --- a/lua/fk_ex.lua +++ b/lua/fk_ex.lua @@ -588,13 +588,3 @@ 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 994ce5dc..f5f4ef65 100644 --- a/lua/lsp/freekill.lua +++ b/lua/lsp/freekill.lua @@ -7,7 +7,6 @@ ---@alias null nil ---@alias bool boolean | nil ----@alias int integer ---@class fk ---FreeKill's lua API diff --git a/lua/server/ai/ai说明.txt b/lua/server/ai/ai说明.txt new file mode 100644 index 00000000..1aaf7756 --- /dev/null +++ b/lua/server/ai/ai说明.txt @@ -0,0 +1,45 @@ +拓展包ai文件写法: +按照包文件夹名+_ai,例如standard_cards是standard_cards_ai.lua,直接放在init.lua同位置下。 + +分辨敌我: +设置反值内值,同时设置技能和牌的身份值;例如杀的值为100,主公或跳身份的忠被杀时,来源反值+100,当一名角色反值大于50时,将识别为反贼;当跳反者杀反贼(或跳忠者杀主忠)时,他内值就+100,内值大于50的将被识别为内奸;当然,标记身份是以最高值开始标记身份,例如场上两名角色为400和300的最高反值,但是反贼只剩下一个,那么就只将400的标记为反贼,其余身份也是如此。优先针对虚弱,相同虚弱再优先仇恨。 + +要让身份值加给角色,就需要在使用技能或牌时接入ai接口,所以我在触发时机函数的最后加了接口,用于获取此时触发时机的数据,例如1号位杀2号位,此时触发了指定目标时机,就可以通过接口接入ai,然后让来源反值+100。但是N神不想这样增加接口,但我还不了解怎么通过其他方式同步获取使用技能或牌的数据...... + +空闲点ai出牌: +先定义阶段技或卡牌的优先度,然后获取角色所有可用的阶段技和卡牌,按照优先度进行排序,再进行for列表逐一检测是否可使用,可使用则检测是否有使用函数,有则执行使用函数;在使用函数里进行定义self.use_id和给self.use_tos添加角色id;然后系统检测有self.use_id则输出使用(如果是使用卡则self.use_id=卡id,阶段技是self.use_id=子卡表{}),同时可以给self.use_tos添加角色id做为牌或技能的目标,会同步输出。 + +请求ai使用牌: +包含请求无懈,请求桃,借刀请求杀等 +请求无懈有正无懈和反无懈,系统根据生效锦囊牌名定位请求代码(之后可以增加来源和目标技能名来修正是否使用,例如要通过伤害锦囊卖血时,可以通过技能名阻止友方使用无懈),同时请求代码会带有正反无懈的参数,根据这个参数进行分类讨论,请求代码需定义self.use_id为将要使用的无懈id,当然也可以定义self.use_id=true,这样就会根据卡牌优先度选择第一个无懈使用。因为要区分正反无懈,我在room里的请求无懈时增加了fk.askNullification和fk.askNullificationData,前者用来区分正反无懈,后者记录要无懈的锦囊数据,因为直接获取使用数据,可能会是上个无懈的数据而不是最初锦囊的数据,当然我并不知道其他的识别正反无懈和获取源锦囊生效数据的方法,只能是手动增加记录的这个样子来实现..... + +请求桃默认给友军使用,但是可以通过来源和目标的技能来修正来源是否要使用桃 + +剩下的请求牌根据提示信息名进行决策,兜底决策是套用空闲点出牌代码,所以说请求使用牌和空闲点使用牌的的函数是相通的,如果请求使用牌没有定义函数,就会调用空闲点使用牌的函数来兜底。 + +技能转化牌: +急救桃、倾国闪等 +也是通过技能名定位转化代码,也需要定义self.use_id为技能子卡表{}就行,如果没有子卡就定义空表{},如果不定义就表示不使用转化技。 + +请求ai打出牌: +杀响应决斗南蛮等 +根据牌名或技能名来分别决策,同时优先以技能名决策(用于卖血技),兜底决策是默认不响应,因为有部分是技能请求响应,例如鬼才改判打出。响应牌依旧是将要响应的卡的id定义为self.use_id(懒得再定义个新参数) + +请求ai弃置牌: +给每个牌名定义保留值,然后排序并优先弃置低保留值的牌。 + +请求ai发动技能: +直接按照技能名分别决策。 +技能请求选择角色是给self.use_tos添加目标id,例如突袭 +既请求选择角色又请求选择牌就再增加定义self.use_id为技能子卡表{} + +请求ai选择角色区域牌: +按照提示信息名分别决策,同时设置兜底决策,对敌军优先选择其重要的牌。 +依旧是定义self.use_id为技能子卡表{},不论选择一张牌还是多张牌,都是将牌id添加到表然后定义给self.use_id。 + +请求ai选择选项: +按照提示信息名分别决策,同时兜底决策是随机选择。 +这个就直接返回需要选择的选项就行。 + + + diff --git a/lua/server/ai/init.lua b/lua/server/ai/init.lua index 2b150ff8..8426698b 100644 --- a/lua/server/ai/init.lua +++ b/lua/server/ai/init.lua @@ -3,25 +3,3 @@ 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/random_ai.lua b/lua/server/ai/random_ai.lua index f788115c..7e7e037e 100644 --- a/lua/server/ai/random_ai.lua +++ b/lua/server/ai/random_ai.lua @@ -1,5 +1,4 @@ -- SPDX-License-Identifier: GPL-3.0-or-later - ---@class RandomAI: AI local RandomAI = AI:subclass("RandomAI") @@ -7,234 +6,273 @@ local RandomAI = AI:subclass("RandomAI") ---@param skill ActiveSkill ---@param card Card | nil local function useActiveSkill(self, skill, card) - local room = self.room - local player = self.player + local room = self.room + local player = self.player - local filter_func = skill.cardFilter - if card then - filter_func = function() return false end - end - - if self.command == "PlayCard" and (not skill:canUse(player, card) or player:prohibitUse(card)) then - return "" - end - - local max_try_times = 100 - local selected_targets = {} - local selected_cards = {} - local min = skill:getMinTargetNum() - local max = skill:getMaxTargetNum(player, card) - local min_card = skill:getMinCardNum() - local max_card = skill:getMaxCardNum() - for _ = 0, max_try_times do - if skill:feasible(selected_targets, selected_cards, self.player, card) then break end - local avail_targets = table.filter(self.room:getAlivePlayers(), function(p) - local ret = skill:targetFilter(p.id, selected_targets, selected_cards, card or Fk:cloneCard'zixing') - if ret and card then - if player:prohibitUse(card) then - ret = false + local filter_func = skill.cardFilter + if card then + filter_func = function() + return false end - end - return ret - end) - avail_targets = table.map(avail_targets, function(p) return p.id end) - local avail_cards = table.filter(player:getCardIds{ Player.Hand, Player.Equip }, function(id) - return filter_func(skill, id, selected_cards, selected_targets) - end) + end - if #avail_targets == 0 and #avail_cards == 0 then break end - table.insertIfNeed(selected_targets, table.random(avail_targets)) - table.insertIfNeed(selected_cards, table.random(avail_cards)) - end - if skill:feasible(selected_targets, selected_cards, self.player, card) then - local ret = json.encode{ - card = card and card.id or json.encode{ - skill = skill.name, - subcards = selected_cards, - }, - targets = selected_targets, - } - -- print(ret) - return ret - end - return "" + if self.command == "PlayCard" and (not skill:canUse(player, card) or player:prohibitUse(card)) then + return "" + end + + local max_try_times = 100 + local selected_targets = {} + local selected_cards = {} + local min = skill:getMinTargetNum() + local max = skill:getMaxTargetNum(player, card) + local min_card = skill:getMinCardNum() + local max_card = skill:getMaxCardNum() + for _ = 0, max_try_times do + if skill:feasible(selected_targets, selected_cards, self.player, card) then + break + end + local avail_targets = table.filter(self.room:getAlivePlayers(), function(p) + local ret = skill:targetFilter(p.id, selected_targets, selected_cards, card or Fk:cloneCard 'zixing') + if ret and card then + if player:prohibitUse(card) then + ret = false + end + end + return ret + end) + avail_targets = table.map(avail_targets, function(p) + return p.id + end) + local avail_cards = table.filter(player:getCardIds{Player.Hand, Player.Equip}, function(id) + return filter_func(skill, id, selected_cards, selected_targets) + end) + + if #avail_targets == 0 and #avail_cards == 0 then + break + end + table.insertIfNeed(selected_targets, table.random(avail_targets)) + table.insertIfNeed(selected_cards, table.random(avail_cards)) + end + if skill:feasible(selected_targets, selected_cards, self.player, card) then + local ret = json.encode { + card = card and card.id or json.encode { + skill = skill.name, + subcards = selected_cards + }, + targets = selected_targets + } + -- print(ret) + return ret + end + return "" end ---@param self RandomAI ---@param skill ViewAsSkill local function useVSSkill(self, skill, pattern, cancelable, extra_data) - local player = self.player - local room = self.room - local precondition + local player = self.player + local room = self.room + local precondition - if self.command == "PlayCard" then - precondition = skill:enabledAtPlay(player) - if not precondition then return nil 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 + if self.command == "PlayCard" then + precondition = skill:enabledAtPlay(player) + if not precondition then + return nil + 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(Self, c) + if precondition then + break + end + end + else + precondition = skill:enabledAtResponse(player) + if not precondition then + return nil + end + local exp = Exppattern:Parse(pattern) + precondition = exp:matchExp(skill.pattern) end - for _, n in ipairs(cnames) do - local c = Fk:cloneCard(n) - precondition = c.skill:canUse(Self, c) - if precondition then break end + + if (not precondition) or math.random() < 0.2 then + return nil end - else - precondition = skill:enabledAtResponse(player) - if not precondition then return nil end - local exp = Exppattern:Parse(pattern) - precondition = exp:matchExp(skill.pattern) - end - if (not precondition) or math.random() < 0.2 then return nil end + local selected_cards = {} + local max_try_time = 100 - local selected_cards = {} - local max_try_time = 100 - - for _ = 0, max_try_time do - local avail_cards = table.filter(player:getCardIds{ Player.Hand, Player.Equip }, function(id) - return skill:cardFilter(id, selected_cards) - end) - if #avail_cards == 0 then break end - table.insert(selected_cards, table.random(avail_cards)) - if skill:viewAs(selected_cards) then - return { - skill = skill.name, - subcards = selected_cards, - } + for _ = 0, max_try_time do + local avail_cards = table.filter(player:getCardIds{Player.Hand, Player.Equip}, function(id) + return skill:cardFilter(id, selected_cards) + end) + if #avail_cards == 0 then + break + end + table.insert(selected_cards, table.random(avail_cards)) + if skill:viewAs(selected_cards) then + return { + skill = skill.name, + subcards = selected_cards + } + end end - end - return nil + return nil end local random_cb = {} random_cb.AskForUseActiveSkill = function(self, jsonData) - local data = json.decode(jsonData) - local skill = Fk.skills[data[1]] - local cancelable = data[3] - if cancelable and math.random() < 0.25 then return "" end - local extra_data = json.decode(data[4]) - for k, v in pairs(extra_data) do - skill[k] = v - end - return useActiveSkill(self, skill) + local data = json.decode(jsonData) + local skill = Fk.skills[data[1]] + local cancelable = data[3] + if cancelable and math.random() < 0.25 then + return "" + end + local extra_data = json.decode(data[4]) + for k, v in pairs(extra_data) do + skill[k] = v + end + return useActiveSkill(self, skill) end random_cb.AskForSkillInvoke = function(self, jsonData) - return table.random{"1", ""} + return table.random {"1", ""} 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 cancelable = data[4] or true - local exp = Exppattern:Parse(pattern) + local player = self.player + local data = json.decode(jsonData) + local card_name = data[1] + local pattern = data[2] or card_name + local cancelable = data[4] or true + local exp = Exppattern:Parse(pattern) - local avail_cards = table.filter(player:getCardIds("he"), function(id) - return exp:match(Fk:getCardById(id)) and not player:prohibitUse(Fk:getCardById(id)) - end) - if #avail_cards > 0 then - if math.random() < 0.25 then return "" end - for _, card in ipairs(avail_cards) do - local skill = Fk:getCardById(card).skill - local max_try_times = 100 - local selected_targets = {} - local min = skill:getMinTargetNum() - local max = skill:getMaxTargetNum(player, card) - local min_card = skill:getMinCardNum() - local max_card = skill:getMaxCardNum() - for _ = 0, max_try_times do - if skill:feasible(selected_targets, {card}, self.player, card) then break end - local avail_targets = table.filter(self.room:getAlivePlayers(), function(p) - local ret = skill:targetFilter(p.id, selected_targets, {card}, card or Fk:cloneCard'zixing') - return ret + local avail_cards = table.filter(player:getCardIds("he"), function(id) + return exp:match(Fk:getCardById(id)) and not player:prohibitUse(Fk:getCardById(id)) + end) + if #avail_cards > 0 then + if math.random() < 0.25 then + return "" + end + avail_cards = table.map(avail_cards, function(id) + return Fk:getCardById(id) end) - avail_targets = table.map(avail_targets, function(p) return p.id end) + for _, card in ipairs(avail_cards) do + local skill = card.skill + local max_try_times = 100 + local selected_targets = {} + local min = skill:getMinTargetNum() + local max = skill:getMaxTargetNum(player, card) + local min_card = skill:getMinCardNum() + local max_card = skill:getMaxCardNum() + for _ = 0, max_try_times do + if skill:feasible(selected_targets, {card.id}, self.player, card) then + break + end + local avail_targets = table.filter(self.room:getAlivePlayers(), function(p) + local ret = skill:targetFilter(p.id, selected_targets, {card}, card or Fk:cloneCard 'zixing') + return ret + end) + avail_targets = table.map(avail_targets, function(p) + return p.id + end) - if #avail_targets == 0 and #avail_cards == 0 then break end - table.insertIfNeed(selected_targets, table.random(avail_targets)) - table.insertIfNeed({card}, table.random(avail_cards)) - end - if skill:feasible(selected_targets, {card}, self.player, card) then - return json.encode{ - card = table.random(avail_cards), - targets = selected_targets, - } - end + if #avail_targets + #avail_cards < 1 then + break + end + table.insertIfNeed(selected_targets, table.random(avail_targets)) + end + if skill:feasible(selected_targets, {card.id}, self.player, card) then + return json.encode { + card = table.random(avail_cards), + targets = selected_targets + } + end + end end - end - return "" + return "" end ---@param self RandomAI random_cb.AskForResponseCard = function(self, jsonData) - local data = json.decode(jsonData) - local pattern = data[2] - local cancelable = true - local exp = Exppattern:Parse(pattern) - local avail_cards = table.filter(self.player:getCardIds{ Player.Hand, Player.Equip }, function(id) - return exp:match(Fk:getCardById(id)) - end) - if #avail_cards > 0 then return json.encode{ - card = table.random(avail_cards), - targets = {}, - } end - -- TODO: vs skill - return "" + local data = json.decode(jsonData) + local pattern = data[2] + local cancelable = true + local exp = Exppattern:Parse(pattern) + local avail_cards = table.filter(self.player:getCardIds{Player.Hand, Player.Equip}, function(id) + return exp:match(Fk:getCardById(id)) + end) + if #avail_cards > 0 then + return json.encode { + card = table.random(avail_cards), + targets = {} + } + end + -- TODO: vs skill + return "" end ---@param self RandomAI random_cb.PlayCard = function(self, jsonData) - local cards = table.map(self.player:getCardIds(Player.Hand), - function(id) return Fk:getCardById(id) end) - 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) + local cards = table.map(self.player:getCardIds(Player.Hand), function(id) + return Fk:getCardById(id) + end) + 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.random(cards) - if sth:isInstanceOf(Card) then - local card = sth - local skill = card.skill ---@type ActiveSkill - if math.random() > 0.15 then - local ret = useActiveSkill(self, skill, card) - if ret ~= "" then return ret end - table.removeOne(cards, card) - else - table.removeOne(cards, card) - end - elseif sth:isInstanceOf(ActiveSkill) then - local active = sth - if math.random() > 0.30 then - local ret = useActiveSkill(self, active, nil) - if ret ~= "" then return ret end - end - table.removeOne(cards, active) - else - local vs = sth - if math.random() > 0.20 then - local ret = useVSSkill(self, vs) - -- TODO: handle vs result - end - table.removeOne(cards, vs) + while #cards > 0 do + local sth = table.random(cards) + if sth:isInstanceOf(Card) then + local card = sth + local skill = card.skill ---@type ActiveSkill + if math.random() > 0.15 then + local ret = useActiveSkill(self, skill, card) + if ret ~= "" then + return ret + end + table.removeOne(cards, card) + else + table.removeOne(cards, card) + end + elseif sth:isInstanceOf(ActiveSkill) then + local active = sth + if math.random() > 0.30 then + local ret = useActiveSkill(self, active, nil) + if ret ~= "" then + return ret + end + end + table.removeOne(cards, active) + else + local vs = sth + if math.random() > 0.20 then + local ret = useVSSkill(self, vs) + -- TODO: handle vs result + end + table.removeOne(cards, vs) + end end - end - return "" + return "" end function RandomAI:initialize(player) - AI.initialize(self, player) - self.cb_table = random_cb + AI.initialize(self, player) + self.cb_table = random_cb end return RandomAI diff --git a/lua/server/ai/trust_ai.lua b/lua/server/ai/trust_ai.lua index 152e6a1d..83c724d2 100644 --- a/lua/server/ai/trust_ai.lua +++ b/lua/server/ai/trust_ai.lua @@ -1,15 +1,1091 @@ -- SPDX-License-Identifier: GPL-3.0-or-later - -- Trust AI - ---@class TrustAI: AI -local TrustAI = AI:subclass("TrustAI") +TrustAI = AI:subclass("TrustAI") + +---@param self TrustAI +---@param skill ActiveSkill|ViewAsSkill|Card +local function usePlaySkill(self, skill) + self.use_id = nil + self.use_tos = {} + Self = self.player + self.special_skill = nil + if skill:isInstanceOf(Card) then + local uc = fk.ai_use_play[skill.name] + if type(uc) == "function" then + uc(self, skill) + end + if self.use_id == nil then + if type(skill.special_skills) == "table" then + for _, sn in ipairs(skill.special_skills) do + uc = fk.ai_use_play[sn] + if type(uc) == "function" then + uc(self, skill) + if self.use_id then + break + end + end + end + end + if skill.type == 3 then + if self.player:getEquipment(skill.sub_type) or #self.player:getCardIds("h") <= self.player.hp then + return "" + end + self.use_id = skill.id + elseif skill.is_damage_card and skill.multiple_targets then + if #self.enemies < #self.friends_noself then + return "" + end + self.use_id = skill.id + end + end + elseif skill:isInstanceOf(ViewAsSkill) then + local selected = {} + local cards = + table.map( + self.player:getCardIds("&he"), + function(id) + return Fk:getCardById(id) + end + ) + self:sortValue(cards) + for _, c in ipairs(cards) do + if skill:cardFilter(c.id, selected) then + table.insert(selected, c.id) + end + end + local tc = skill:viewAs(selected) + if tc then + local uc = fk.ai_use_play[tc.name] + if type(uc) == "function" then + uc(self, tc) + if self.use_id then + self.use_id = selected + end + end + end + else + local uc = fk.ai_use_play[skill.name] + if type(uc) == "function" then + uc(self, skill) + end + end + if self.use_id then + if not skill:isInstanceOf(Card) then + self.use_id = + json.encode { + skill = skill.name, + subcards = self.use_id + } + end + return json.encode { + card = self.use_id, + targets = self.use_tos, + special_skill = self.special_skill + } + end + return "" +end + +fk.ai_use_play = {} local trust_cb = {} +trust_cb.AskForUseActiveSkill = function(self, jsonData) + local data = json.decode(jsonData) + local skill = Fk.skills[data[1]] + local prompt = data[2] + local cancelable = data[3] + self:updatePlayers() + local extra_data = json.decode(data[4]) + for k, v in pairs(extra_data) do + skill[k] = v + end + self.use_id = nil + self.use_tos = {} + local ask = fk.ai_use_skill[data[1]] + if type(ask) == "function" then + ask(self, prompt, cancelable, extra_data) + end + if self.use_id then + return json.encode { + card = self.use_id, + targets = self.use_tos + } + end + return "" +end + +fk.ai_use_skill = {} + +fk.ai_use_skill.choose_players_skill = function(self, prompt, cancelable, data) + local ask = fk.ai_choose_players[data.skillName] + if type(ask) == "function" then + ask(self, data.targets, data.min_num, data.num, cancelable) + end + if #self.use_tos > 0 then + if self.use_id then + self.use_id = + json.encode { + skill = data.skillName, + subcards = self.use_id + } + else + self.use_id = + json.encode { + skill = data.skillName, + subcards = {} + } + end + end +end + +fk.ai_choose_players = {} + +fk.ai_use_skill.discard_skill = function(self, prompt, cancelable, data) + local ask = fk.ai_dis_card[data.skillName] + self:assignValue() + if type(ask) == "function" then + ask = ask(self, data.min_num, data.num, data.include_equip, cancelable, data.pattern, prompt) + end + if type(ask) ~= "table" and not cancelable then + local flag = "h" + if data.include_equip then + flag = "he" + end + ask = {} + local cards = + table.map( + self.player:getCardIds(flag), + function(id) + return Fk:getCardById(id) + end + ) + self:sortValue(cards) + for _, c in ipairs(cards) do + table.insert(ask, c.id) + if #ask >= data.min_num then + break + end + end + end + if type(ask) == "table" and #ask >= data.min_num then + self.use_id = + json.encode { + skill = data.skillName, + subcards = ask + } + end +end + +fk.ai_dis_card = {} + +trust_cb.AskForSkillInvoke = function(self, jsonData) + local data = json.decode(jsonData) + local prompt = data[2] + local extra_data = data[3] + local ask = fk.ai_skill_invoke[data[1]] + self:updatePlayers() + if type(ask) == "function" then + return ask(self, extra_data, prompt) and "1" or "" + elseif type(ask) == "boolean" then + return ask and "1" or "" + elseif Fk.skills[data[1]].frequency == 1 then + return "1" + else + return table.random { "1", "" } + end +end + +fk.ai_skill_invoke = {} + +trust_cb.AskForUseCard = function(self, jsonData) + local data = json.decode(jsonData) + local pattern = data[2] + local prompt = data[3] + local cancelable = data[4] + local extra_data = data[5] + self:updatePlayers() + self.use_id = nil + self.use_tos = {} + local exp = Exppattern:Parse(data[2] or data[1]) + self.avail_cards = + table.filter( + self.player:getCardIds("&he"), + function(id) + return exp:match(Fk:getCardById(id)) and not self.player:prohibitUse(Fk:getCardById(id)) + end + ) + Self = self.player + local ask = fk.ai_askuse_card[prompt:split(":")[1]] + if type(ask) == "function" then + ask(self, pattern, prompt, cancelable, extra_data) + else + local cards = + table.map( + self.player:getCardIds("&he"), + function(id) + return Fk:getCardById(id) + end + ) + self:sortValue(cards) + for _, sth in ipairs(self:getActives(pattern)) do + if sth:isInstanceOf(Card) then + if sth.skill:canUse(self.player, sth) and not self.player:prohibitUse(sth) then + local ret = usePlaySkill(self, sth) + if ret ~= "" then + return ret + end + 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(pattern) then + local uc = fk.ai_use_play[tc.name] + if type(uc) == "function" then + uc(self, tc) + if self.use_id then + self.use_id = + json.encode { + skill = sth.name, + subcards = selected + } + break + end + end + end + end + end + end + ask = fk.ai_askuse_card[data[1]] + if self.use_id == nil and type(ask) == "function" then + ask(self, pattern, prompt, cancelable, extra_data) + end + if self.use_id == true then + self.use_id = self.avail_cards[1] + end + if self.use_id then + return json.encode { + card = self.use_id, + targets = self.use_tos + } + end + return "" +end + +fk.ai_askuse_card = {} +fk.ai_nullification = {} + +fk.ai_askuse_card.nullification = function(self, pattern, prompt, cancelable, extra_data) + local effect = self:eventData("CardEffect") + if effect.to then + fk.askNullificationData = effect + fk.askNullification = 1 + elseif effect.from then + fk.askNullification = fk.askNullification + 1 + end + effect = fk.askNullificationData + local positive = fk.askNullification % 2 == 1 + local ask = fk.ai_nullification[effect.card.name] + if type(ask) == "function" then + ask(self, effect.card, self.room:getPlayerById(effect.to), self.room:getPlayerById(effect.from), positive) + end +end + +fk.ai_askuse_card["#AskForPeaches"] = function(self, pattern, prompt, cancelable, extra_data) + local dying = self:eventData("Dying") + local who = self.room:getPlayerById(dying.who) + if who and self:isFriend(who) then + local cards = + table.map( + self.player:getCardIds("&he"), + function(id) + return Fk:getCardById(id) + end + ) + self:sortValue(cards) + for _, sth in ipairs(self:getActives(pattern)) do + if sth:isInstanceOf(Card) then + self.use_id = sth.id + break + else + local selected = {} + for _, c in ipairs(cards) do + if sth.cardFilter(sth, c.id, selected) then + table.insert(selected, c.id) + end + end + local tc = sth.viewAs(sth, selected) + if tc and tc:matchPattern(pattern) then + self.use_id = + json.encode { + skill = sth.name, + subcards = selected + } + break + end + end + end + end +end + +fk.ai_askuse_card["#AskForPeachesSelf"] = fk.ai_askuse_card["#AskForPeaches"] + +fk.ai_card = {} +fk.cardValue = {} + +function TrustAI:assignValue(assign) + assign = assign or { "slash", "peach", "jink", "nullification" } + for v, p in ipairs(assign) do + local kept = {} + v = fk.ai_card[p] + v = v and v.value or 3 + for _, sth in ipairs(self:getActives(p)) do + if sth:isInstanceOf(Card) then + fk.cardValue[sth.id] = self:getValue(sth, kept) + else + fk.cardValue[sth.name] = self:getValue(sth, kept) + v + end + table.insert(kept, sth) + end + self.keptCv = nil + end +end + +function TrustAI:getValue(card, kept) + local v = fk.ai_card[card.name] + v = v and v.value or 0 + if kept then + if card:isInstanceOf(Card) then + if self.keptCv == nil then + self.keptCv = v + end + return v - #kept * 0.25 + else + return (self.keptCv or v) - #kept * 0.25 + end + elseif card:isInstanceOf(Card) then + return fk.cardValue[card.id] or v + else + return fk.cardValue[card.name] or v + end + return v +end + +function TrustAI:getPriority(card) + local v = card and fk.ai_card[card.name] + v = v and v.priority or 0 + if card:isInstanceOf(Card) then + if card:isInstanceOf(Armor) then + v = v + 7 + elseif card:isInstanceOf(Weapon) then + v = v + 3 + elseif card:isInstanceOf(OffensiveRide) then + v = v + 6 + elseif card:isInstanceOf(DefensiveRide) then + v = v + 4 + end + v = v + (13 - card.number) / 100 + v = v + card.suit / 100 + end + return v +end + +fk.compareFunc = { + hp = function(p) + return p.hp + end, + maxHp = function(p) + return p.maxHp + end, + hand = function(p) + return #p:getHandlyIds(true) + end, + equip = function(p) + return #p:getCardIds("e") + end, + maxcards = function(p) + return p.hp + end, + skill = function(p) + return #p:getAllSkills() + end, + defense = function(p) + return p.hp + #p:getHandlyIds(true) + end +} + +function TrustAI:sort(players, key, inverse) + key = key or "defense" + local func = fk.compareFunc[key] + if func == nil then + func = fk.compareFunc.defense + end + local function compare_func(a, b) + return func(a) < func(b) + end + table.sort(players, compare_func) + if inverse then + players = table.reverse(players) + end +end + +function TrustAI:sortValue(cards, inverse) + local function compare_func(a, b) + return self:getValue(a) < self:getValue(b) + end + table.sort(cards, compare_func) + if inverse then + cards = table.reverse(cards) + end +end + +function TrustAI:sortPriority(cards, inverse) + local function compare_func(a, b) + local va = a and self:getPriority(a) or 0 + local vb = b and self:getPriority(b) or 0 + if va == vb then + va = a and self:getValue(a) or 0 + vb = b and self:getValue(b) or 0 + end + return va > vb + end + table.sort(cards, compare_func) + if inverse then + cards = table.reverse(cards) + end +end + +---@param self TrustAI +trust_cb.AskForResponseCard = function(self, jsonData) + local data = json.decode(jsonData) + local pattern = data[2] + local prompt = data[3] + local cancelable = data[4] + local extra_data = data[5] + self:updatePlayers() + self.use_id = nil + local ask = fk.ai_response_card[prompt:split(":")[1]] + if type(ask) == "function" then + ask(self, pattern, prompt, cancelable, extra_data) + else + ask = fk.ai_response_card[data[1]] + if type(ask) == "function" then + ask(self, pattern, prompt, cancelable, extra_data) + end + local effect = self:eventData("CardEffect") + if effect and effect.card then + self:setUseId(pattern) + end + end + if self.use_id then + return json.encode { + card = self.use_id, + targets = {} + } + end + return "" +end + +fk.ai_response_card = {} + +function TrustAI:getRetrialCardId(cards, exchange) + local judge = self:eventData("Judge") + local isgood = judge.card:matchPattern(judge.pattern) + local canRetrial = {} + self:sortValue(cards) + if exchange then + for _, c in ipairs(cards) do + if c:matchPattern(judge.pattern) == isgood then + table.insert(canRetrial, c) + end + end + else + if isgood then + if self:isFriend(judge.who) then + return + end + elseif self:isEnemie(judge.who) then + return + end + end + for _, c in ipairs(cards) do + if + self:isFriend(judge.who) and c:matchPattern(judge.pattern) or + self:isEnemie(judge.who) and not c:matchPattern(judge.pattern) + then + table.insert(canRetrial, c) + end + end + if #canRetrial > 0 then + return canRetrial[1].id + end +end + +function TrustAI:getActives(pattern) + local cards = + table.map( + self.player:getCardIds("&he"), + function(id) + return Fk:getCardById(id) + end + ) + local exp = Exppattern:Parse(pattern) + cards = + table.filter( + cards, + function(c) + return exp:match(c) + end + ) + table.insertTable( + cards, + table.filter( + self.player:getAllSkills(), + function(s) + return s:isInstanceOf(ViewAsSkill) and s:enabledAtResponse(self.player, pattern) + end + ) + ) + self:sortPriority(cards) + return cards +end + +function TrustAI:setUseId(pattern) + local cards = + table.map( + self.player:getCardIds("&he"), + function(id) + return Fk:getCardById(id) + end + ) + self:sortValue(cards) + for _, sth in ipairs(self:getActives(pattern)) do + if sth:isInstanceOf(Card) then + self.use_id = sth.id + break + 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(pattern) then + self.use_id = + json.encode { + skill = sth.name, + subcards = selected + } + break + end + end + end +end + +function TrustAI:cardsView(pattern) + local actives = + table.filter( + self.player:getAllSkills(), + function(s) + return s:isInstanceOf(ViewAsSkill) and s:enabledAtResponse(self.player, pattern) + end + ) + return actives +end + +---@param self TrustAI +trust_cb.PlayCard = function(self, jsonData) + local cards = + table.map( + self.player:getHandlyIds(true), + function(id) + return Fk:getCardById(id) + end + ) + cards = + table.filter( + cards, + function(c) + return c.skill:canUse(self.player, c) and not self.player:prohibitUse(c) + end + ) + table.insertTable( + cards, + table.filter( + self.player:getAllSkills(), + function(s) + return s:isInstanceOf(ActiveSkill) and s:canUse(self.player) or + s:isInstanceOf(ViewAsSkill) and s:enabledAtPlay(self.player) + end + ) + ) + if #cards < 1 then + return + end + self:updatePlayers() + self:sortPriority(cards) + for _, sth in ipairs(cards) do + local ret = usePlaySkill(self, sth) + if ret ~= "" then + return ret + end + end + return "" +end + +fk.ai_card_chosen = {} + +trust_cb.AskForCardChosen = function(self, jsonData) + local data = json.decode(jsonData) + local to = self.room:getPlayerById(data[1]) + local chosen = fk.ai_card_chosen[data[3]] + if type(chosen) == "function" then + return chosen(self, to, data[2]) + elseif table.contains(self.friends, to) then + if string.find(data[2], "j") then + local jc = to:getCardIds("j") + if #jc > 0 then + return table.random(jc) + end + end + else + if string.find(data[2], "h") then + local hc = to:getCardIds("h") + if #hc == 1 then + return hc[1] + end + end + if string.find(data[2], "e") then + local ec = to:getCardIds("e") + if #ec > 0 then + return table.random(ec) + end + for c, id in ipairs(to:getCardIds("e")) do + --c = Fk:getCardById(id) + return id + end + end + if string.find(data[2], "h") then + local hc = to:getCardIds("h") + if #hc > 0 then + return table.random(hc) + end + end + end + return "" +end + +fk.ai_role = {} +fk.roleValue = {} + +fk.trick_judge = {} + +fk.trick_judge.indulgence = ".|.|heart" +fk.trick_judge.lightning = ".|.|^spade" +fk.trick_judge.supply_shortage = ".|.|club" + +local function table_clone(self) + local t = {} + for _, r in ipairs(self) do + table.insert(t, r) + end + return t +end + +trust_cb.AskForGuanxing = function(self, jsonData) + local data = json.decode(jsonData) + local cards = + table.map( + data.cards, + function(id) + return Fk:getCardById(id) + end + ) + self:sortValue(cards) + local top = {} + if self.room.current.phase < Player.Play then + local jt = + table.map( + self.room.current:getCardIds("j"), + function(id) + return Fk:getCardById(id) + end + ) + if #jt > 0 then + for i, j in ipairs(table.reverse(jt)) do + local tj = fk.trick_judge[j.name] + if tj then + for _, c in ipairs(table_clone(cards)) do + if c:matchPattern(tj) and #top < data.max_top_cards then + table.insert(top, c.id) + table.removeOne(cards, c) + tj = 1 + break + end + end + end + if tj ~= 1 and #cards > 0 and #top < data.max_top_cards then + table.insert(top, cards[1].id) + table.remove(cards, 1) + end + end + end + self:sortValue(cards, true) + for _, c in ipairs(table_clone(cards)) do + if #top < data.max_top_cards and c.skill:canUse(self.player, c) and usePlaySkill(self, c) ~= "" then + table.insert(top, c.id) + table.removeOne(cards, c) + break + end + end + end + for _, c in ipairs(table_clone(cards)) do + if #top < data.min_top_cards then + table.insert(top, c.id) + table.removeOne(cards, c) + break + end + end + return json.encode { + top, + table.map( + cards, + function(c) + return c.id + end + ) + } +end + function TrustAI:initialize(player) AI.initialize(self, player) self.cb_table = trust_cb + self.player = player + self.room = RoomInstance or ClientInstance + + fk.ai_role[player.id] = "neutral" + fk.roleValue[player.id] = { + lord = 0, + loyalist = 0, + rebel = 0, + renegade = 0 + } + self:updatePlayers() +end + +function TrustAI:isRolePredictable() + return self.room.settings.gameMode ~= "aaa_role_mode" +end + +local function aliveRoles(room) + fk.alive_roles = { + lord = 0, + loyalist = 0, + rebel = 0, + renegade = 0 + } + for _, ap in ipairs(room:getAllPlayers(false)) do + fk.alive_roles[ap.role] = 0 + end + for _, ap in ipairs(room:getAlivePlayers(false)) do + fk.alive_roles[ap.role] = fk.alive_roles[ap.role] + 1 + end + return fk.alive_roles +end + +function TrustAI:objectiveLevel(to) + if self.player.id == to.id then + return -2 + elseif #self.room:getAlivePlayers(false) < 3 then + return 5 + end + local ars = aliveRoles(self.room) + if self:isRolePredictable() then + fk.ai_role[self.player.id] = self.role + fk.roleValue[self.player.id][self.role] = 666 + if self.role == "renegade" then + fk.explicit_renegade = true + end + for _, p in ipairs(self.room:getAlivePlayers()) do + if + p.role == self.role or p.role == "lord" and self.role == "loyalist" or + p.role == "loyalist" and self.role == "lord" + then + table.insert(self.friends, p) + if p.id ~= self.player.id then + table.insert(self.friends_noself, p) + end + else + table.insert(self.enemies, p) + end + end + elseif self.role == "renegade" then + if to.role == "lord" then + return -1 + elseif ars.rebel < 1 then + return 4 + elseif fk.ai_role[to.id] == "loyalist" then + return ars.lord + ars.loyalist - ars.rebel + elseif fk.ai_role[to.id] == "rebel" then + local r = ars.rebel - ars.lord + ars.loyalist + if r >= 0 then + return 3 + else + return r + end + end + elseif self.role == "lord" or self.role == "loyalist" then + if fk.ai_role[to.id] == "rebel" then + return 5 + elseif to.role == "lord" then + return -2 + elseif ars.rebel < 1 then + if self.role == "lord" then + return fk.explicit_renegade and fk.ai_role[to.id] == "renegade" and 4 or to.hp > 1 and 2 or 0 + elseif fk.explicit_renegade then + return fk.ai_role[to.id] == "renegade" and 4 or -1 + else + return 3 + end + elseif fk.ai_role[to.id] == "loyalist" then + return -2 + elseif fk.ai_role[to.id] == "renegade" then + local r = ars.lord + ars.loyalist - ars.rebel + if r <= 0 then + return r + else + return 3 + end + end + elseif self.role == "rebel" then + if to.role == "lord" then + return 5 + elseif fk.ai_role[to.id] == "loyalist" then + return 4 + elseif fk.ai_role[to.id] == "rebel" then + return -2 + elseif fk.ai_role[to.id] == "renegade" then + local r = ars.rebel - ars.lord + ars.loyalist + if r > 0 then + return 1 + else + return r + end + end + end + return 0 +end + +function TrustAI:updatePlayers(update) + self.role = self.player.role + local neutrality = {} + self.enemies = {} + self.friends = {} + self.friends_noself = {} + + local aps = self.room:getAlivePlayers() + local function compare_func(a, b) + local v1 = fk.roleValue[a.id].rebel + local v2 = fk.roleValue[b.id].rebel + if v1 == v2 then + v1 = fk.roleValue[a.id].renegade + v2 = fk.roleValue[b.id].renegade + end + return v1 > v2 + end + table.sort(aps, compare_func) + fk.explicit_renegade = false + local ars = aliveRoles(self.room) + local rebel, renegade, loyalist = 0, 0, 0 + for _, ap in ipairs(aps) do + if ap.role == "lord" then + fk.ai_role[ap.id] = "loyalist" + elseif fk.roleValue[ap.id].rebel > 50 and ars.rebel > rebel then + rebel = rebel + 1 + fk.ai_role[ap.id] = "rebel" + elseif fk.roleValue[ap.id].renegade > 50 and ars.renegade > renegade then + renegade = renegade + 1 + fk.ai_role[ap.id] = "renegade" + fk.explicit_renegade = fk.roleValue[ap.id].renegade > 100 + elseif fk.roleValue[ap.id].rebel < -50 and ars.loyalist > loyalist then + loyalist = loyalist + 1 + fk.ai_role[ap.id] = "loyalist" + else + fk.ai_role[ap.id] = "neutral" + end + end + + for n, p in ipairs(self.room:getAlivePlayers(false)) do + n = self:objectiveLevel(p) + if n < 0 then + table.insert(self.friends, p) + if p.id ~= self.player.id then + table.insert(self.friends_noself, p) + end + elseif n > 0 then + table.insert(self.enemies, p) + else + table.insert(neutrality, p) + end + end + self:assignValue() + --[[ + if self.enemies<1 and #neutrality>0 + and#self.toUse<3 and self:getOverflow()>0 + then + function compare_func(a,b) + return sgs.getDefense(a) 0 or fk.roleValue[player.id].rebel > 0 and intention < 0 then + fk.roleValue[player.id].renegade = fk.roleValue[player.id].renegade + + intention * 100 / (200 + fk.roleValue[player.id].renegade) + end + local aps = player.room:getAlivePlayers() + local function compare_func(a, b) + local v1 = fk.roleValue[a.id].rebel + local v2 = fk.roleValue[b.id].rebel + if v1 == v2 then + v1 = fk.roleValue[a.id].renegade + v2 = fk.roleValue[b.id].renegade + end + return v1 > v2 + end + table.sort(aps, compare_func) + fk.explicit_renegade = false + local ars = aliveRoles(player.room) + local rebel, renegade, loyalist = 0, 0, 0 + for _, ap in ipairs(aps) do + if ap.role == "lord" then + fk.ai_role[ap.id] = "loyalist" + elseif fk.roleValue[ap.id].rebel > 50 and ars.rebel > rebel then + rebel = rebel + 1 + fk.ai_role[ap.id] = "rebel" + elseif fk.roleValue[ap.id].renegade > 50 and ars.renegade > renegade then + renegade = renegade + 1 + fk.ai_role[ap.id] = "renegade" + fk.explicit_renegade = fk.roleValue[ap.id].renegade > 100 + elseif fk.roleValue[ap.id].rebel < -50 and ars.loyalist > loyalist then + loyalist = loyalist + 1 + fk.ai_role[ap.id] = "loyalist" + else + fk.ai_role[ap.id] = "neutral" + end + end + fk.qWarning( + player.general .. + " " .. + intention .. + " " .. + fk.ai_role[player.id] .. + " rebelValue:" .. fk.roleValue[player.id].rebel .. " renegadeValue:" .. fk.roleValue[player.id].renegade + ) + end +end + +function TrustAI:filterEvent(event, player, data) + if event == fk.TargetSpecified then + local callback = fk.ai_card[data.card.name] + callback = callback and callback.intention + if type(callback) == "function" then + for _, p in ipairs(TargetGroup:getRealTargets(data.tos)) do + p = self.room:getPlayerById(p) + local intention = callback(p.ai, data.card, self.room:getPlayerById(data.from)) + if type(intention) == "number" then + updateIntention(self.room:getPlayerById(data.from), p, intention) + end + end + elseif type(callback) == "number" then + for _, p in ipairs(TargetGroup:getRealTargets(data.tos)) do + p = self.room:getPlayerById(p) + updateIntention(self.room:getPlayerById(data.from), p, callback) + end + end + elseif event == fk.StartJudge then + fk.trick_judge[data.reason] = data.pattern + elseif event == fk.AfterCardsMove then + end +end + +function TrustAI:isWeak(player, getAP) + player = player or self.player + if type(player) == "number" then + player = self.room:getPlayerById(player) + end + return player.hp < 2 or player.hp <= 2 and #player:getCardIds("&h") <= 2 +end + +function TrustAI:isFriend(pid, tid) + if tid then + local bt = self:isFriend(pid) + return bt ~= nil and bt == self:isFriend(tid) + end + if type(pid) == "number" then + pid = self.room:getPlayerById(pid) + end + local ve = self:objectiveLevel(pid) + if ve < 0 then + return true + elseif ve > 0 then + return false + end +end + +function TrustAI:isEnemie(pid, tid) + if tid then + local bt = self:isFriend(pid) + return bt ~= nil and bt ~= self:isFriend(tid) + end + if type(pid) == "number" then + pid = self.room:getPlayerById(pid) + end + local ve = self:objectiveLevel(pid) + if ve > 0 then + return true + elseif ve < 0 then + return false + end +end + +function TrustAI:eventData(game_event) + local event = self.room.logic:getCurrentEvent():findParent(GameEvent[game_event], true) + return event and event.data[1] +end + +for _, n in ipairs(FileIO.ls("packages")) do + if FileIO.isDir("packages/" .. n) and FileIO.exists("packages/" .. n .. "/" .. n .. "_ai.lua") then + dofile("packages/" .. n .. "/" .. n .. "_ai.lua") + end +end +-- 加载两次拓展是为了能够引用,例如属性杀的使用直接套入普通杀的使用 +for _, n in ipairs(FileIO.ls("packages")) do + if FileIO.isDir("packages/" .. n) and FileIO.exists("packages/" .. n .. "/" .. n .. "_ai.lua") then + dofile("packages/" .. n .. "/" .. n .. "_ai.lua") + end end return TrustAI diff --git a/lua/server/events/usecard.lua b/lua/server/events/usecard.lua index c411f9a5..32613bcd 100644 --- a/lua/server/events/usecard.lua +++ b/lua/server/events/usecard.lua @@ -1,15 +1,18 @@ -- SPDX-License-Identifier: GPL-3.0-or-later - local playCardEmotionAndSound = function(room, player, card) if card.type ~= Card.TypeEquip then local anim_path = "./packages/" .. card.package.extensionName .. "/image/anim/" .. card.name if not FileIO.exists(anim_path) then for _, dir in ipairs(FileIO.ls("./packages/")) do anim_path = "./packages/" .. dir .. "/image/anim/" .. card.name - if FileIO.exists(anim_path) then break end + if FileIO.exists(anim_path) then + break + end end end - if FileIO.exists(anim_path) then room:setEmotion(player, anim_path) end + if FileIO.exists(anim_path) then + room:setEmotion(player, anim_path) + end end local soundName @@ -25,13 +28,15 @@ local playCardEmotionAndSound = function(room, player, card) soundName = "./audio/card/common/" .. subTypeStr else - soundName = "./packages/" .. card.package.extensionName .. "/audio/card/" - .. (player.gender == General.Male and "male/" or "female/") .. card.name + soundName = "./packages/" .. card.package.extensionName .. "/audio/card/" .. + (player.gender == General.Male and "male/" or "female/") .. card.name if not FileIO.exists(soundName .. ".mp3") then for _, dir in ipairs(FileIO.ls("./packages/")) do - soundName = "./packages/" .. dir .. "/audio/card/" - .. (player.gender == General.Male and "male/" or "female/") .. card.name - if FileIO.exists(soundName .. ".mp3") then break end + soundName = "./packages/" .. dir .. "/audio/card/" .. + (player.gender == General.Male and "male/" or "female/") .. card.name + if FileIO.exists(soundName .. ".mp3") then + break + end end end end @@ -49,17 +54,19 @@ local sendCardEmotionAndLog = function(room, cardUseEvent) local card = _card ---[[ if not _card:isVirtual() then - local temp = { card = _card } + local temp = { + card = _card + } Fk:filterCard(_card.id, room:getPlayerById(from), temp) card = temp.card end cardUseEvent.card = card - --]] + -- ]] playCardEmotionAndSound(room, room:getPlayerById(from), card) room:doAnimate("Indicate", { from = from, - to = cardUseEvent.tos or Util.DummyTable, + to = cardUseEvent.tos or Util.DummyTable }) local useCardIds = card:isVirtual() and card.subcards or { card.id } @@ -71,23 +78,23 @@ local sendCardEmotionAndLog = function(room, cardUseEvent) if card:isVirtual() or (card ~= _card) then if #useCardIds == 0 then - room:sendLog{ + room:sendLog { type = "#UseV0CardToTargets", from = from, to = to, - arg = card:toLogString(), + arg = card:toLogString() } else - room:sendLog{ + room:sendLog { type = "#UseVCardToTargets", from = from, to = to, card = useCardIds, - arg = card:toLogString(), + arg = card:toLogString() } end else - room:sendLog{ + room:sendLog { type = "#UseCardToTargets", from = from, to = to, @@ -97,74 +104,78 @@ local sendCardEmotionAndLog = function(room, cardUseEvent) for _, t in ipairs(cardUseEvent.tos) do if t[2] then - local temp = {table.unpack(t)} + local temp = { table.unpack(t) } table.remove(temp, 1) - room:sendLog{ + room:sendLog { type = "#CardUseCollaborator", from = t[1], to = temp, - arg = card.name, + arg = card.name } end end elseif cardUseEvent.toCard then if card:isVirtual() or (card ~= _card) then if #useCardIds == 0 then - room:sendLog{ + room:sendLog { type = "#UseV0CardToCard", from = from, arg = cardUseEvent.toCard.name, - arg2 = card:toLogString(), + arg2 = card:toLogString() } else - room:sendLog{ + room:sendLog { type = "#UseVCardToCard", from = from, card = useCardIds, arg = cardUseEvent.toCard.name, - arg2 = card:toLogString(), + arg2 = card:toLogString() } end else - room:sendLog{ + room:sendLog { type = "#UseCardToCard", from = from, card = useCardIds, - arg = cardUseEvent.toCard.name, + arg = cardUseEvent.toCard.name } end else if card:isVirtual() or (card ~= _card) then if #useCardIds == 0 then - room:sendLog{ + room:sendLog { type = "#UseV0Card", from = from, - arg = card:toLogString(), + arg = card:toLogString() } else - room:sendLog{ + room:sendLog { type = "#UseVCard", from = from, card = useCardIds, - arg = card:toLogString(), + arg = card:toLogString() } end else - room:sendLog{ + room:sendLog { type = "#UseCard", from = from, - card = useCardIds, + card = useCardIds } end end - if #useCardIds == 0 then return end + if #useCardIds == 0 then + return + end if cardUseEvent.tos and #cardUseEvent.tos > 0 and #cardUseEvent.tos <= 2 then - local tos = table.map(cardUseEvent.tos, function(e) return e[1] end) + local tos = table.map(cardUseEvent.tos, function(e) + return e[1] + end) room:sendFootnote(useCardIds, { type = "##UseCardTo", from = from, - to = tos, + to = tos }) if card:isVirtual() then room:sendCardVirtName(useCardIds, card.name) @@ -172,7 +183,7 @@ local sendCardEmotionAndLog = function(room, cardUseEvent) else room:sendFootnote(useCardIds, { type = "##UseCard", - from = from, + from = from }) if card:isVirtual() then room:sendCardVirtName(useCardIds, card.name) @@ -185,16 +196,18 @@ GameEvent.functions[GameEvent.UseCard] = function(self) local room = self.room local logic = room.logic - room:moveCardTo(cardUseEvent.card, Card.Processing, nil, fk.ReasonUse) - if cardUseEvent.card.skill then cardUseEvent.card.skill:onUse(room, cardUseEvent) end if logic:trigger(fk.PreCardUse, room:getPlayerById(cardUseEvent.from), cardUseEvent) then + cardUseEvent.breakEvent = true + self.data = { cardUseEvent } logic:breakEvent() end + room:moveCardTo(cardUseEvent.card, Card.Processing, nil, fk.ReasonUse) + sendCardEmotionAndLog(room, cardUseEvent) if not cardUseEvent.extraUse then @@ -205,7 +218,6 @@ GameEvent.functions[GameEvent.UseCard] = function(self) cardUseEvent.responseToEvent.cardsResponded = cardUseEvent.responseToEvent.cardsResponded or {} table.insertIfNeed(cardUseEvent.responseToEvent.cardsResponded, cardUseEvent.card) end - for _, event in ipairs({ fk.AfterCardUseDeclared, fk.AfterCardTargetDeclared, fk.CardUsing }) do if not cardUseEvent.toCard and #TargetGroup:getRealTargets(cardUseEvent.tos) == 0 then break @@ -229,7 +241,7 @@ GameEvent.cleaners[GameEvent.UseCard] = function(self) room:moveCards({ ids = leftRealCardIds, toArea = Card.DiscardPile, - moveReason = fk.ReasonUse, + moveReason = fk.ReasonUse }) end end @@ -238,50 +250,54 @@ GameEvent.functions[GameEvent.RespondCard] = function(self) local cardResponseEvent = table.unpack(self.data) local room = self.room local logic = room.logic + + if logic:trigger(fk.PreCardRespond, room:getPlayerById(cardResponseEvent.from), cardResponseEvent) then + cardResponseEvent.breakEvent = true + self.data = { cardResponseEvent } + logic:breakEvent() + end + local from = cardResponseEvent.customFrom or cardResponseEvent.from local card = cardResponseEvent.card local cardIds = room:getSubcardsByRule(card) if card:isVirtual() then if #cardIds == 0 then - room:sendLog{ + room:sendLog { type = "#ResponsePlayV0Card", from = from, - arg = card:toLogString(), + arg = card:toLogString() } else - room:sendLog{ + room:sendLog { type = "#ResponsePlayVCard", from = from, card = cardIds, - arg = card:toLogString(), + arg = card:toLogString() } end else - room:sendLog{ + room:sendLog { type = "#ResponsePlayCard", from = from, - card = cardIds, + card = cardIds } end room:moveCardTo(card, Card.Processing, nil, fk.ReasonResonpse) if #cardIds > 0 then room:sendFootnote(cardIds, { type = "##ResponsePlayCard", - from = from, + from = from }) if card:isVirtual() then room:sendCardVirtName(cardIds, card.name) end end - - if logic:trigger(fk.PreCardRespond, room:getPlayerById(cardResponseEvent.from), cardResponseEvent) then - logic:breakEvent() + if cardResponseEvent.retrial ~= true then + playCardEmotionAndSound(room, room:getPlayerById(from), card) end - - playCardEmotionAndSound(room, room:getPlayerById(from), card) - logic:trigger(fk.CardResponding, room:getPlayerById(cardResponseEvent.from), cardResponseEvent) + self.data = { cardResponseEvent } end GameEvent.cleaners[GameEvent.RespondCard] = function(self) @@ -295,7 +311,7 @@ GameEvent.cleaners[GameEvent.RespondCard] = function(self) room:moveCards({ ids = realCardIds, toArea = Card.DiscardPile, - moveReason = fk.ReasonResonpse, + moveReason = fk.ReasonResonpse }) end end @@ -315,13 +331,9 @@ GameEvent.functions[GameEvent.CardEffect] = function(self) end end - if - not cardEffectEvent.toCard and - ( - not (room:getPlayerById(cardEffectEvent.to):isAlive() and cardEffectEvent.to) - or #room:deadPlayerFilter(TargetGroup:getRealTargets(cardEffectEvent.tos)) == 0 - ) - then + if not cardEffectEvent.toCard and + (not (room:getPlayerById(cardEffectEvent.to):isAlive() and cardEffectEvent.to) or + #room:deadPlayerFilter(TargetGroup:getRealTargets(cardEffectEvent.tos)) == 0) then logic:breakEvent() end diff --git a/lua/server/gameevent.lua b/lua/server/gameevent.lua index 4c884f96..61e4cfbe 100644 --- a/lua/server/gameevent.lua +++ b/lua/server/gameevent.lua @@ -73,13 +73,6 @@ 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 b0d28f53..429c4ca9 100644 --- a/lua/server/gamelogic.lua +++ b/lua/server/gamelogic.lua @@ -1,5 +1,4 @@ -- SPDX-License-Identifier: GPL-3.0-or-later - ---@class GameLogic: Object ---@field public room Room ---@field public skill_table table @@ -14,421 +13,424 @@ local GameLogic = class("GameLogic") function GameLogic:initialize(room) - self.room = room - self.skill_table = {} -- TriggerEvent --> TriggerSkill[] - self.skill_priority_table = {} - self.refresh_skill_table = {} - self.skills = {} -- skillName[] - self.game_event_stack = Stack:new() - self.all_game_events = {} - self.event_recorder = {} - self.current_event_id = 0 + self.room = room + self.skill_table = {} -- TriggerEvent --> TriggerSkill[] + self.skill_priority_table = {} + self.refresh_skill_table = {} + self.skills = {} -- skillName[] + self.game_event_stack = Stack:new() + self.all_game_events = {} + self.event_recorder = {} + self.current_event_id = 0 - self.role_table = { - { "lord" }, - { "lord", "rebel" }, - { "lord", "rebel", "renegade" }, - { "lord", "loyalist", "rebel", "renegade" }, - { "lord", "loyalist", "rebel", "rebel", "renegade" }, - { "lord", "loyalist", "rebel", "rebel", "rebel", "renegade" }, - { "lord", "loyalist", "loyalist", "rebel", "rebel", "rebel", "renegade" }, - { "lord", "loyalist", "loyalist", "rebel", "rebel", "rebel", "rebel", "renegade" }, - } + self.role_table = {{"lord"}, {"lord", "rebel"}, {"lord", "rebel", "renegade"}, + {"lord", "loyalist", "rebel", "renegade"}, {"lord", "loyalist", "rebel", "rebel", "renegade"}, + {"lord", "loyalist", "rebel", "rebel", "rebel", "renegade"}, + {"lord", "loyalist", "loyalist", "rebel", "rebel", "rebel", "renegade"}, + {"lord", "loyalist", "loyalist", "rebel", "rebel", "rebel", "rebel", "renegade"}} end function GameLogic:run() - -- default logic - local room = self.room - table.shuffle(self.room.players) - self:assignRoles() - self.room.game_started = true - room:doBroadcastNotify("StartGame", "") - room:adjustSeats() + -- default logic + local room = self.room + table.shuffle(self.room.players) + self:assignRoles() + self.room.game_started = true + room:doBroadcastNotify("StartGame", "") + room:adjustSeats() - self:chooseGenerals() + self:chooseGenerals() - self:buildPlayerCircle() - self:broadcastGeneral() - self:prepareDrawPile() - self:attachSkillToPlayers() - self:prepareForStart() + self:buildPlayerCircle() + self:broadcastGeneral() + self:prepareDrawPile() + self:attachSkillToPlayers() + self:prepareForStart() - self:action() + self:action() end local function execGameEvent(type, ...) - local event = GameEvent:new(type, ...) - local _, ret = event:exec() - return ret + local event = GameEvent:new(type, ...) + local _, ret = event:exec() + return ret end - function GameLogic:assignRoles() - local room = self.room - local n = #room.players - local roles = self.role_table[n] - table.shuffle(roles) + local room = self.room + local n = #room.players + local roles = self.role_table[n] + table.shuffle(roles) - for i = 1, n do - local p = room.players[i] - p.role = roles[i] - if p.role == "lord" then - p.role_shown = true - room:broadcastProperty(p, "role") - else - room:notifyProperty(p, p, "role") + for i = 1, n do + local p = room.players[i] + p.role = roles[i] + if p.role == "lord" then + p.role_shown = true + room:broadcastProperty(p, "role") + else + room:notifyProperty(p, p, "role") + end end - end end function GameLogic:chooseGenerals() - local room = self.room - local generalNum = room.settings.generalNum - local n = room.settings.enableDeputy and 2 or 1 - local lord = room:getLord() - local lord_generals = {} + local room = self.room + local generalNum = room.settings.generalNum + local n = room.settings.enableDeputy and 2 or 1 + local lord = room:getLord() + local lord_generals = {} - if lord ~= nil then - room.current = lord - local generals = {} - local lordlist = {} - local lordpools = {} - if room.settings.gameMode == "aaa_role_mode" then - for _, general in pairs(Fk:getAllGenerals()) do - if (not general.hidden and not general.total_hidden) and - table.find(general.skills, function(s) - return s.lordSkill - end) and - not table.find(lordlist, function(g) - return g.trueName == general.trueName - end) then - table.insert(lordlist, general) + if lord ~= nil then + room.current = lord + local generals = {} + local lordlist = {} + local lordpools = {} + if room.settings.gameMode == "aaa_role_mode" then + for _, general in pairs(Fk:getAllGenerals()) do + if (not general.hidden and not general.total_hidden) and table.find(general.skills, function(s) + return s.lordSkill + end) and not table.find(lordlist, function(g) + return g.trueName == general.trueName + end) then + table.insert(lordlist, general) + end + end + lordlist = table.random(lordlist, 3) or {} end - end - lordlist = table.random(lordlist, 3) or {} - end - table.insertTable(generals, Fk:getGeneralsRandomly(generalNum, Fk:getAllGenerals(), nil, function(g) - return table.contains(table.map(lordlist, function(g) return g.trueName end), g.trueName) - end)) - for i = 1, #generals do - generals[i] = generals[i].name - end - lordpools = table.simpleClone(generals) - table.insertTable(lordpools, table.map(lordlist, function(g) return g.name end)) - lord_generals = room:askForGeneral(lord, lordpools, n) - local lord_general, deputy - if type(lord_generals) == "table" then - deputy = lord_generals[2] - lord_general = lord_generals[1] - else - lord_general = lord_generals - lord_generals = {lord_general} + table.insertTable(generals, Fk:getGeneralsRandomly(generalNum, Fk:getAllGenerals(), nil, function(g) + return table.contains(table.map(lordlist, function(g) + return g.trueName + end), g.trueName) + end)) + for i = 1, #generals do + generals[i] = generals[i].name + end + lordpools = table.simpleClone(generals) + table.insertTable(lordpools, table.map(lordlist, function(g) + return g.name + end)) + lord_generals = room:askForGeneral(lord, lordpools, n) + local lord_general, deputy + if type(lord_generals) == "table" then + deputy = lord_generals[2] + lord_general = lord_generals[1] + else + lord_general = lord_generals + lord_generals = {lord_general} + end + + room:setPlayerGeneral(lord, lord_general, true) + room:askForChooseKingdom({lord}) + room:broadcastProperty(lord, "general") + room:broadcastProperty(lord, "kingdom") + room:setDeputyGeneral(lord, deputy) + room:broadcastProperty(lord, "deputyGeneral") end - room:setPlayerGeneral(lord, lord_general, true) - room:askForChooseKingdom({lord}) - room:broadcastProperty(lord, "general") - room:broadcastProperty(lord, "kingdom") - room:setDeputyGeneral(lord, deputy) - room:broadcastProperty(lord, "deputyGeneral") - end - - local nonlord = room:getOtherPlayers(lord, true) - local generals = Fk:getGeneralsRandomly(#nonlord * generalNum, nil, lord_generals) - table.shuffle(generals) - for _, p in ipairs(nonlord) do - local arg = {} - for i = 1, generalNum do - table.insert(arg, table.remove(generals, 1).name) + local nonlord = room:getOtherPlayers(lord, true) + local generals = Fk:getGeneralsRandomly(#nonlord * generalNum, nil, lord_generals) + table.shuffle(generals) + for _, p in ipairs(nonlord) do + local arg = {} + for i = 1, generalNum do + table.insert(arg, table.remove(generals, 1).name) + end + p.request_data = json.encode {arg, n} + p.default_reply = table.random(arg, n) end - p.request_data = json.encode{ arg, n } - p.default_reply = table.random(arg, n) - end - room:notifyMoveFocus(nonlord, "AskForGeneral") - room:doBroadcastRequest("AskForGeneral", nonlord) + room:notifyMoveFocus(nonlord, "AskForGeneral") + room:doBroadcastRequest("AskForGeneral", nonlord) - for _, p in ipairs(nonlord) do - if p.general == "" and p.reply_ready then - local generals = json.decode(p.client_reply) - local general = generals[1] - local deputy = generals[2] - room:setPlayerGeneral(p, general, true, true) - room:setDeputyGeneral(p, deputy) - else - room:setPlayerGeneral(p, p.default_reply[1], true, true) - room:setDeputyGeneral(p, p.default_reply[2]) + for _, p in ipairs(nonlord) do + if p.general == "" and p.reply_ready then + local generals = json.decode(p.client_reply) + local general = generals[1] + local deputy = generals[2] + room:setPlayerGeneral(p, general, true, true) + room:setDeputyGeneral(p, deputy) + else + room:setPlayerGeneral(p, p.default_reply[1], true, true) + room:setDeputyGeneral(p, p.default_reply[2]) + end + p.default_reply = "" end - p.default_reply = "" - end - room:askForChooseKingdom(nonlord) + room:askForChooseKingdom(nonlord) end function GameLogic:buildPlayerCircle() - local room = self.room - local players = room.players - room.alive_players = {table.unpack(players)} - for i = 1, #players - 1 do - players[i].next = players[i + 1] - end - players[#players].next = players[1] + local room = self.room + local players = room.players + room.alive_players = {table.unpack(players)} + for i = 1, #players - 1 do + players[i].next = players[i + 1] + end + players[#players].next = players[1] end function GameLogic:broadcastGeneral() - local room = self.room - local players = room.players + local room = self.room + local players = room.players - for _, p in ipairs(players) do - assert(p.general ~= "") - local general = Fk.generals[p.general] - local deputy = Fk.generals[p.deputyGeneral] - p.maxHp = p:getGeneralMaxHp() - p.hp = deputy and math.floor((deputy.hp + general.hp) / 2) or general.hp - p.shield = math.min(general.shield + (deputy and deputy.shield or 0), 5) - -- TODO: setup AI here + for _, p in ipairs(players) do + assert(p.general ~= "") + local general = Fk.generals[p.general] + local deputy = Fk.generals[p.deputyGeneral] + p.maxHp = p:getGeneralMaxHp() + p.hp = deputy and math.floor((deputy.hp + general.hp) / 2) or general.hp + p.shield = math.min(general.shield + (deputy and deputy.shield or 0), 5) + -- TODO: setup AI here - if p.role ~= "lord" then - room:broadcastProperty(p, "general") - room:broadcastProperty(p, "kingdom") - room:broadcastProperty(p, "deputyGeneral") - elseif #players >= 5 then - p.maxHp = p.maxHp + 1 - p.hp = p.hp + 1 + if p.role ~= "lord" then + room:broadcastProperty(p, "general") + room:broadcastProperty(p, "kingdom") + room:broadcastProperty(p, "deputyGeneral") + elseif #players >= 5 then + p.maxHp = p.maxHp + 1 + p.hp = p.hp + 1 + end + room:broadcastProperty(p, "maxHp") + room:broadcastProperty(p, "hp") + room:broadcastProperty(p, "shield") end - room:broadcastProperty(p, "maxHp") - room:broadcastProperty(p, "hp") - room:broadcastProperty(p, "shield") - end end function GameLogic:prepareDrawPile() - local room = self.room - local allCardIds = Fk:getAllCardIds() + local room = self.room + local allCardIds = Fk:getAllCardIds() - for i = #allCardIds, 1, -1 do - if Fk:getCardById(allCardIds[i]).is_derived then - local id = allCardIds[i] - table.removeOne(allCardIds, id) - table.insert(room.void, id) - room:setCardArea(id, Card.Void, nil) + for i = #allCardIds, 1, -1 do + if Fk:getCardById(allCardIds[i]).is_derived then + local id = allCardIds[i] + table.removeOne(allCardIds, id) + table.insert(room.void, id) + room:setCardArea(id, Card.Void, nil) + end end - end - table.shuffle(allCardIds) - room.draw_pile = allCardIds - for _, id in ipairs(room.draw_pile) do - self.room:setCardArea(id, Card.DrawPile, nil) - end + table.shuffle(allCardIds) + room.draw_pile = allCardIds + for _, id in ipairs(room.draw_pile) do + self.room:setCardArea(id, Card.DrawPile, nil) + end end function GameLogic:attachSkillToPlayers() - local room = self.room - local players = room.players + local room = self.room + local players = room.players - local addRoleModSkills = function(player, skillName) - local skill = Fk.skills[skillName] - if skill.lordSkill and (player.role ~= "lord" or #room.players < 5) then - return - end + local addRoleModSkills = function(player, skillName) + local skill = Fk.skills[skillName] + if skill.lordSkill and (player.role ~= "lord" or #room.players < 5) then + return + end - if #skill.attachedKingdom > 0 and not table.contains(skill.attachedKingdom, player.kingdom) then - return - end + if #skill.attachedKingdom > 0 and not table.contains(skill.attachedKingdom, player.kingdom) then + return + end - room:handleAddLoseSkills(player, skillName, nil, false) - end - for _, p in ipairs(room.alive_players) do - local skills = Fk.generals[p.general].skills - for _, s in ipairs(skills) do - addRoleModSkills(p, s.name) - end - for _, sname in ipairs(Fk.generals[p.general].other_skills) do - addRoleModSkills(p, sname) + room:handleAddLoseSkills(player, skillName, nil, false) end + for _, p in ipairs(room.alive_players) do + local skills = Fk.generals[p.general].skills + for _, s in ipairs(skills) do + addRoleModSkills(p, s.name) + end + for _, sname in ipairs(Fk.generals[p.general].other_skills) do + addRoleModSkills(p, sname) + end - local deputy = Fk.generals[p.deputyGeneral] - if deputy then - skills = deputy.skills - for _, s in ipairs(skills) do - addRoleModSkills(p, s.name) - end - for _, sname in ipairs(deputy.other_skills) do - addRoleModSkills(p, sname) - end + local deputy = Fk.generals[p.deputyGeneral] + if deputy then + skills = deputy.skills + for _, s in ipairs(skills) do + addRoleModSkills(p, s.name) + end + for _, sname in ipairs(deputy.other_skills) do + addRoleModSkills(p, sname) + end + end end - end end function GameLogic:prepareForStart() - local room = self.room - local players = room.players + local room = self.room + local players = room.players - self:addTriggerSkill(GameRule) - for _, trig in ipairs(Fk.global_trigger) do - self:addTriggerSkill(trig) - end + self:addTriggerSkill(GameRule) + for _, trig in ipairs(Fk.global_trigger) do + self:addTriggerSkill(trig) + end - self.room:sendLog{ type = "$GameStart" } + self.room:sendLog{ + type = "$GameStart" + } end function GameLogic:action() - self:trigger(fk.GamePrepared) - local room = self.room + self:trigger(fk.GamePrepared) + local room = self.room - execGameEvent(GameEvent.DrawInitial) + execGameEvent(GameEvent.DrawInitial) - while true do - execGameEvent(GameEvent.Round) - if room.game_finished then break end - end + while true do + execGameEvent(GameEvent.Round) + if room.game_finished then + break + end + end end ---@param skill TriggerSkill function GameLogic:addTriggerSkill(skill) - if skill == nil or table.contains(self.skills, skill.name) then - return - end - - table.insert(self.skills, skill.name) - - for _, event in ipairs(skill.refresh_events) do - if self.refresh_skill_table[event] == nil then - self.refresh_skill_table[event] = {} - end - table.insert(self.refresh_skill_table[event], skill) - end - - for _, event in ipairs(skill.events) do - if self.skill_table[event] == nil then - self.skill_table[event] = {} - end - table.insert(self.skill_table[event], skill) - - if self.skill_priority_table[event] == nil then - self.skill_priority_table[event] = {} + if skill == nil or table.contains(self.skills, skill.name) then + return end - local priority_tab = self.skill_priority_table[event] - local prio = skill.priority_table[event] - if not table.contains(priority_tab, prio) then - for i, v in ipairs(priority_tab) do - if v < prio then - table.insert(priority_tab, i, prio) - break + table.insert(self.skills, skill.name) + + for _, event in ipairs(skill.refresh_events) do + if self.refresh_skill_table[event] == nil then + self.refresh_skill_table[event] = {} end - end - - if not table.contains(priority_tab, prio) then - table.insert(priority_tab, prio) - end + table.insert(self.refresh_skill_table[event], skill) end - if not table.contains(self.skill_priority_table[event], - skill.priority_table[event]) then + for _, event in ipairs(skill.events) do + if self.skill_table[event] == nil then + self.skill_table[event] = {} + end + table.insert(self.skill_table[event], skill) - table.insert(self.skill_priority_table[event], - skill.priority_table[event]) - end - end + if self.skill_priority_table[event] == nil then + self.skill_priority_table[event] = {} + end - if skill.visible then - if (Fk.related_skills[skill.name] == nil) then return end - for _, s in ipairs(Fk.related_skills[skill.name]) do - if (s.class == TriggerSkill) then - self:addTriggerSkill(s) - end + local priority_tab = self.skill_priority_table[event] + local prio = skill.priority_table[event] + if not table.contains(priority_tab, prio) then + for i, v in ipairs(priority_tab) do + if v < prio then + table.insert(priority_tab, i, prio) + break + end + end + + if not table.contains(priority_tab, prio) then + table.insert(priority_tab, prio) + end + end + + if not table.contains(self.skill_priority_table[event], skill.priority_table[event]) then + + table.insert(self.skill_priority_table[event], skill.priority_table[event]) + end + end + + if skill.visible then + if (Fk.related_skills[skill.name] == nil) then + return + end + for _, s in ipairs(Fk.related_skills[skill.name]) do + if (s.class == TriggerSkill) then + self:addTriggerSkill(s) + end + end end - end end ---@param event Event ---@param target ServerPlayer|nil ---@param data any|nil function GameLogic:trigger(event, target, data, refresh_only) - local room = self.room - local broken = false - local skills = self.skill_table[event] or {} - local skills_to_refresh = self.refresh_skill_table[event] or Util.DummyTable - local _target = room.current -- for iteration - local player = _target - - if #skills_to_refresh > 0 then repeat do - -- refresh skills. This should not be broken - for _, skill in ipairs(skills_to_refresh) do - if skill:canRefresh(event, target, player, data) then - skill:refresh(event, target, player, data) - end - end - player = player.next - end until player == _target end - - if #skills == 0 or refresh_only then return end - - local prio_tab = self.skill_priority_table[event] - local prev_prio = math.huge - - for _, prio in ipairs(prio_tab) do - if broken then break end - if prio >= prev_prio then - -- continue - goto trigger_loop_continue + local room = self.room + local broken = false + local skills = self.skill_table[event] or {} + local skills_to_refresh = self.refresh_skill_table[event] or Util.DummyTable + local _target = room.current -- for iteration + local player = _target + if #skills_to_refresh > 0 then + repeat + do + -- refresh skills. This should not be broken + for _, skill in ipairs(skills_to_refresh) do + if skill:canRefresh(event, target, player, data) then + skill:refresh(event, target, player, data) + end + end + player = player.next + end + until player == _target end - repeat do - 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 + if #skills == 0 or refresh_only then + return + end - local skill_names = table.map(table.filter(skills, filter_func), Util.NameMapper) + local prio_tab = self.skill_priority_table[event] + local prev_prio = math.huge - while #skill_names > 0 do - local skill_name = prio <= 0 and table.random(skill_names) or - room:askForChoice(player, skill_names, "trigger", "#choose-trigger") + for _, prio in ipairs(prio_tab) do + if broken then + break + end + if prio >= prev_prio then + -- continue + goto trigger_loop_continue + end - local skill = skill_name == "game_rule" and GameRule - or Fk.skills[skill_name] + repeat + do + local triggerables = table.filter(skills, function(skill) + return skill.priority_table[event] == prio and skill:triggerable(event, target, player, data) + end) - table.insert(invoked_skills, skill) - broken = skill:trigger(event, target, player, data) - skill_names = table.map(table.filter(skills, filter_func), Util.NameMapper) + local skill_names = table.map(triggerables, function(skill) + return skill.name + end) - broken = broken or (event == fk.AskForPeaches - and room:getPlayerById(data.who).hp > 0) + while #skill_names > 0 do + local skill_name = prio <= 0 and table.random(skill_names) or + room:askForChoice(player, skill_names, "trigger", "#choose-trigger") - if broken then break end - end + local skill = skill_name == "game_rule" and GameRule or Fk.skills[skill_name] - if broken then break end + local len = #skills + broken = skill:trigger(event, target, player, data) - player = player.next - end until player == _target + 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)) - prev_prio = prio - ::trigger_loop_continue:: - end + broken = broken or (event == fk.AskForPeaches and room:getPlayerById(data.who).hp > 0) - return broken + if broken then + break + end + table.removeOne(skill_names, skill_name) + end + if broken then + break + end + player = player.next + end + until player == _target + + prev_prio = prio + ::trigger_loop_continue:: + end + _target.ai:filterEvent(event, target, data) + return broken end ---@return GameEvent 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 + return self.game_event_stack.t[self.game_event_stack.p] end -- 在指定历史范围中找至多n个符合条件的事件 @@ -438,85 +440,90 @@ end ---@param scope integer @ 查询历史范围,只能是当前阶段/回合/轮次 ---@return GameEvent[] @ 找到的符合条件的所有事件,最多n个但不保证有n个 function GameLogic:getEventsOfScope(eventType, n, func, scope) - scope = scope or Player.HistoryTurn - local event = self:getCurrentEvent() - local start_event ---@type GameEvent - if scope == Player.HistoryGame then - start_event = self.all_game_events[1] - elseif scope == Player.HistoryRound then - start_event = event:findParent(GameEvent.Round, true) - elseif scope == Player.HistoryTurn then - start_event = event:findParent(GameEvent.Turn, true) - elseif scope == Player.HistoryPhase then - start_event = event:findParent(GameEvent.Phase, true) - end + scope = scope or Player.HistoryTurn + local event = self:getCurrentEvent() + local start_event ---@type GameEvent + if scope == Player.HistoryGame then + start_event = self.all_game_events[1] + elseif scope == Player.HistoryRound then + start_event = event:findParent(GameEvent.Round, true) + elseif scope == Player.HistoryTurn then + start_event = event:findParent(GameEvent.Turn, true) + elseif scope == Player.HistoryPhase then + start_event = event:findParent(GameEvent.Phase, true) + end - return start_event:searchEvents(eventType, n, func) + return start_event:searchEvents(eventType, n, func) end function GameLogic:dumpEventStack(detailed) - local top = self:getCurrentEvent() - local i = self.game_event_stack.p - local inspect = p - if not top then return end - - print("===== Start of event stack dump =====") - if not detailed then print("") end - - repeat - local printable_data - if type(top.data) ~= "table" then - printable_data = top.data - else - printable_data = table.cloneWithoutClass(top.data) + local top = self:getCurrentEvent() + local i = self.game_event_stack.p + local inspect = p + if not top then + return end + print("===== Start of event stack dump =====") if not detailed then - print("Stack level #" .. i .. ": " .. tostring(top)) - else - print("\nStack level #" .. i .. ":") - inspect{ - eventId = GameEvent:translate(top.event), - data = printable_data or "nil", - } + print("") end - top = top.parent - i = i - 1 - until not top + repeat + local printable_data + if type(top.data) ~= "table" then + printable_data = top.data + else + printable_data = table.cloneWithoutClass(top.data) + end - print("\n===== End of event stack dump =====") + if not detailed then + print("Stack level #" .. i .. ": " .. tostring(top)) + else + print("\nStack level #" .. i .. ":") + inspect { + eventId = GameEvent:translate(top.event), + data = printable_data or "nil" + } + end + + top = top.parent + i = i - 1 + until not top + + print("\n===== End of event stack dump =====") end function GameLogic:dumpAllEvents(from, to) - from = from or 1 - to = to or #self.all_game_events - assert(from <= to) + from = from or 1 + to = to or #self.all_game_events + assert(from <= to) - local indent = 0 - local tab = " " - for i = from, to, 1 do - local v = self.all_game_events[i] - if type(v) ~= "table" then - indent = math.max(indent - 1, 0) - -- v = "End" - -- print(tab:rep(indent) .. string.format("#%d: %s", i, v)) - else - print(tab:rep(indent) .. string.format("%s", tostring(v))) - if v.id ~= v.end_id then - indent = indent + 1 - end + local indent = 0 + local tab = " " + for i = from, to, 1 do + local v = self.all_game_events[i] + if type(v) ~= "table" then + indent = math.max(indent - 1, 0) + -- v = "End" + -- print(tab:rep(indent) .. string.format("#%d: %s", i, v)) + else + print(tab:rep(indent) .. string.format("%s", tostring(v))) + if v.id ~= v.end_id then + indent = indent + 1 + end + end end - end end function GameLogic:breakEvent(ret) - coroutine.yield("__breakEvent", ret) + self.room.breakEvent = true + coroutine.yield("__breakEvent", ret) end function GameLogic:breakTurn() - local event = self:getCurrentEvent():findParent(GameEvent.Turn) - event:shutdown() + local event = self:getCurrentEvent():findParent(GameEvent.Turn) + event:shutdown() end return GameLogic diff --git a/lua/server/room.lua b/lua/server/room.lua index 721cc1ef..ac3b15e9 100644 --- a/lua/server/room.lua +++ b/lua/server/room.lua @@ -1,5 +1,4 @@ -- SPDX-License-Identifier: GPL-3.0-or-later - --- Room是fk游戏逻辑运行的主要场所,同时也提供了许多API函数供编写技能使用。 --- --- 一个房间中只有一个Room实例,保存在RoomInstance全局变量中。 @@ -56,7 +55,8 @@ dofile "lua/server/ai/init.lua" gameevent.lua (游戏事件的执行逻辑,以及各种事件的执行方法) game_rule.lua (基础游戏规则,包括执行阶段、决胜负等) aux_skills.lua (某些交互方法是套壳askForUseActiveSkill,就是在这定义的) -]]---------------------------------------------------------------------- +]] +---------------------------------------------------------------------- ------------------------------------------------------------------------ -- 构造函数 @@ -84,7 +84,7 @@ function Room:initialize(_room) self.owner_map = {} self.status_skills = {} for class, skills in pairs(Fk.global_status_skill) do - self.status_skills[class] = {table.unpack(skills)} + self.status_skills[class] = { table.unpack(skills) } end self.request_queue = {} self.request_self = {} @@ -107,10 +107,13 @@ end function Room:resume() -- 如果还没运行的话就先创建自己的主协程 if not self.main_co then - self.main_co = coroutine.create(function() - self.tag["_general_pile"] = Fk:getAllGenerals() - self:run() - end) + self.main_co = + coroutine.create( + function() + self.tag["_general_pile"] = Fk:getAllGenerals() + self:run() + end + ) end local ret, err_msg, rest_time = true, true, nil @@ -174,8 +177,7 @@ function Room:isReady() if p._splayer:thinking() then ret = false -- 烧条烧光了的话就把thinking设为false - rest = p.request_timeout * 1000 - (os.getms() - - p.request_start) / 1000 + rest = p.request_timeout * 1000 - (os.getms() - p.request_start) / 1000 if rest <= 0 or p.serverplayer:getState() ~= fk.Player_Online then p._splayer:setThinking(false) @@ -193,7 +195,9 @@ function Room:isReady() end function Room:checkNoHuman(chkOnly) - if #self.players == 0 then return end + if #self.players == 0 then + return + end for _, p in ipairs(self.players) do -- TODO: trust @@ -217,7 +221,6 @@ function Room:__gc() self.room:checkAbandoned() end --]] - --- 正式在这个房间中开始游戏。 --- --- 当这个函数返回之后,整个Room线程也宣告结束。 @@ -232,7 +235,9 @@ function Room:run() local mode = Fk.game_modes[self.settings.gameMode] self.logic = (mode.logic and mode.logic() or GameLogic):new(self) - if mode.rule then self.logic:addTriggerSkill(mode.rule) end + if mode.rule then + self.logic:addTriggerSkill(mode.rule) + end self.logic:run() end @@ -275,33 +280,36 @@ end ---@param id integer @ 玩家的id ---@return ServerPlayer @ 这个id对应的ServerPlayer实例 function Room:getPlayerById(id) - if not id then return nil end - assert(type(id) == "number") - + --assert(type(id) == "number") for _, p in ipairs(self.players) do if p.id == id then return p end end - return nil end --- 将房间中的玩家按照座位顺序重新排序。 ---@param playerIds integer[] @ 玩家id列表,这个数组会被这个函数排序 function Room:sortPlayersByAction(playerIds, isTargetGroup) - table.sort(playerIds, function(prev, next) - local prevSeat = self:getPlayerById(isTargetGroup and prev[1] or prev).seat - local nextSeat = self:getPlayerById(isTargetGroup and next[1] or next).seat + table.sort( + playerIds, + function(prev, next) + local prevSeat = self:getPlayerById(isTargetGroup and prev[1] or prev).seat + local nextSeat = self:getPlayerById(isTargetGroup and next[1] or next).seat - return prevSeat < nextSeat - end) + return prevSeat < nextSeat + end + ) if - self.current and - table.find(isTargetGroup and TargetGroup:getRealTargets(playerIds) or playerIds, function(id) - return self:getPlayerById(id).seat >= self.current.seat - end) + self.current and + table.find( + isTargetGroup and TargetGroup:getRealTargets(playerIds) or playerIds, + function(id) + return self:getPlayerById(id).seat >= self.current.seat + end + ) then while self:getPlayerById(isTargetGroup and playerIds[1][1] or playerIds[1]).seat < self.current.seat do local toPlayerId = table.remove(playerIds, 1) @@ -330,15 +338,13 @@ function Room:getAllPlayers(sortBySeat) if not self.game_started then return { table.unpack(self.players) } end - if sortBySeat == nil or sortBySeat then - local current = self.current - local temp = current.next - local ret = {current} - while temp ~= current do + if sortBySeat ~= false and self.current then + local temp = self.current.next + local ret = { self.current } + while temp ~= self.current do table.insert(ret, temp) temp = temp.next end - return ret else return { table.unpack(self.players) } @@ -349,7 +355,7 @@ end ---@param sortBySeat bool ---@return ServerPlayer[] function Room:getAlivePlayers(sortBySeat) - if sortBySeat == nil or sortBySeat then + if sortBySeat ~= false and self.current then local current = self.current local temp = current.next @@ -357,14 +363,13 @@ function Room:getAlivePlayers(sortBySeat) if temp == nil then return { table.unpack(self.players) } end - local ret = {current} + local ret = { current } while temp ~= current do if not temp.dead then table.insert(ret, temp) end temp = temp.next end - return ret else return { table.unpack(self.alive_players) } @@ -398,9 +403,13 @@ end ---@return ServerPlayer | nil @ 主公 function Room:getLord() local lord = self.players[1] - if lord.role == "lord" then return lord end + if lord.role == "lord" then + return lord + end for _, p in ipairs(self.players) do - if p.role == "lord" then return p end + if p.role == "lord" then + return p + end end return nil @@ -447,11 +456,7 @@ end ---@param value any @ 要设为的值,其实也可以设为字符串 function Room:setPlayerMark(player, mark, value) player:setMark(mark, value) - self:doBroadcastNotify("SetPlayerMark", json.encode{ - player.id, - mark, - value - }) + self:doBroadcastNotify("SetPlayerMark", json.encode { player.id, mark, value }) end --- 将一名玩家的mark标记增加count个。 @@ -485,11 +490,7 @@ end function Room:setCardMark(card, mark, value) card:setMark(mark, value) if not card:isVirtual() then - self:doBroadcastNotify("SetCardMark", json.encode{ - card.id, - mark, - value - }) + self:doBroadcastNotify("SetCardMark", json.encode { card.id, mark, value }) end end @@ -553,7 +554,9 @@ end ---@param changeKingdom bool ---@param noBroadcast bool function Room:setPlayerGeneral(player, general, changeKingdom, noBroadcast) - if Fk.generals[general] == nil then return end + if Fk.generals[general] == nil then + return + end player.general = general player.gender = Fk.generals[general].gender self:notifyProperty(player, player, "general") @@ -572,7 +575,9 @@ end ---@param player ServerPlayer ---@param general string function Room:setDeputyGeneral(player, general) - if Fk.generals[general] == nil then return end + if Fk.generals[general] == nil then + return + end player.deputyGeneral = general self:notifyProperty(player, player, "deputyGeneral") end @@ -590,23 +595,25 @@ function Room:changeHero(player, new_general, full, isDeputy, sendLog, maxHpChan if not isDeputy and (new.kingdom == "god" or new.subkingdom) then local allKingdoms = {} if new.kingdom == "god" then - allKingdoms = {"wei", "shu", "wu", "qun", "jin"} + allKingdoms = { "wei", "shu", "wu", "qun", "jin" } elseif new.subkingdom then allKingdoms = { new.kingdom, new.subkingdom } end kingdom = self:askForChoice(player, allKingdoms, "AskForKingdom", "#ChooseInitialKingdom") end - execGameEvent(GameEvent.ChangeProperty, - { - from = player, - general = not isDeputy and new_general or "", - deputyGeneral = isDeputy and new_general or "", - gender = isDeputy and player.gender or new.gender, - kingdom = kingdom, - sendLog = sendLog, - results = {}, - }) + execGameEvent( + GameEvent.ChangeProperty, + { + from = player, + general = not isDeputy and new_general or "", + deputyGeneral = isDeputy and new_general or "", + gender = isDeputy and player.gender or new.gender, + kingdom = kingdom, + sendLog = sendLog, + results = {} + } + ) maxHpChange = (maxHpChange == nil) and true or maxHpChange if maxHpChange then @@ -621,16 +628,20 @@ end ---@param kingdom string @ 要变更的势力 ---@param sendLog bool @ 是否发Log function Room:changeKingdom(player, kingdom, sendLog) - if kingdom == player.kingdom then return end + if kingdom == player.kingdom then + return + end sendLog = sendLog or false - execGameEvent(GameEvent.ChangeProperty, - { - from = player, - kingdom = kingdom, - sendLog = sendLog, - results = {}, - }) + execGameEvent( + GameEvent.ChangeProperty, + { + from = player, + kingdom = kingdom, + sendLog = sendLog, + results = {} + } + ) end ------------------------------------------------------------------------ @@ -651,11 +662,7 @@ end ---@param player ServerPlayer @ 拥有那个属性的玩家 ---@param property string @ 属性名称 function Room:notifyProperty(p, player, property) - p:doNotify("PropertyUpdate", json.encode{ - player.id, - property, - player[property], - }) + p:doNotify("PropertyUpdate", json.encode { player.id, property, player[property] }) end --- 向多名玩家广播一条消息。 @@ -676,12 +683,11 @@ end ---@param wait bool @ 是否要等待答复,默认为true ---@return string | nil @ 收到的答复,如果wait为false的话就返回nil function Room:doRequest(player, command, jsonData, wait) - if wait == nil then wait = true end self.request_queue = {} self.race_request_list = nil player:doRequest(command, jsonData, self.timeout) - if wait then + if wait ~= false then local ret = player:waitForReply(self.timeout) player.serverplayer:setBusy(false) player.serverplayer:setThinking(false) @@ -735,12 +741,11 @@ function Room:doRaceRequest(command, players, jsonData) p:doRequest(command, jsonData or p.request_data) end + local elapsed = 0 + local canceled_players = {} + local ret = nil local remainTime = self.timeout local currentTime = os.time() - local elapsed = 0 - local winner - local canceled_players = {} - local ret while true do elapsed = os.time() - currentTime if remainTime - elapsed <= 0 then @@ -749,29 +754,25 @@ function Room:doRaceRequest(command, players, jsonData) for _, p in ipairs(players) do p:waitForReply(0) if p.reply_ready == true then - winner = p + ret = p break end - if p.reply_cancel then - table.removeOne(players, p) table.insertIfNeed(canceled_players, p) + table.removeOne(players, p) elseif p.id > 0 then -- 骗过调度器让他以为自己尚未就绪 p.request_timeout = remainTime - elapsed p.serverplayer:setThinking(true) end end - if winner then + if ret then self:doBroadcastNotify("CancelRequest", "") - ret = winner break end - - if player_len == #canceled_players then + if #canceled_players >= player_len then break end - coroutine.yield("__handleRequest", (remainTime - elapsed) * 1000) end @@ -783,14 +784,12 @@ function Room:doRaceRequest(command, players, jsonData) return ret end - --- 延迟一段时间。 --- --- 这个函数不应该在请求处理协程中使用。 ---@param ms integer @ 要延迟的毫秒数 function Room:delay(ms) - local start = os.getms() - self.delay_start = start + self.delay_start = os.getms() self.delay_duration = ms self.in_delay = true coroutine.yield("__handleRequest", ms) @@ -801,7 +800,9 @@ end ---@param card_moves CardsMoveStruct[] @ 要告知的移牌信息列表 ---@param forceVisible bool @ 是否让所有牌对告知目标可见 function Room:notifyMoveCards(players, card_moves, forceVisible) - if players == nil or players == {} then players = self.players end + if players == nil or players == {} then + players = self.players + end for _, p in ipairs(players) do local arg = table.clone(card_moves) for _, move in ipairs(arg) do @@ -821,25 +822,35 @@ function Room:notifyMoveCards(players, card_moves, forceVisible) end end - local function containArea(area, relevant) --处理区的处理? - local areas = relevant - and {Card.PlayerEquip, Card.PlayerJudge, Card.DiscardPile, Card.Processing, Card.PlayerHand, Card.PlayerSpecial} - or {Card.PlayerEquip, Card.PlayerJudge, Card.DiscardPile, Card.Processing} + local function containArea(area, relevant) -- 处理区的处理? + local areas = + relevant and + { + Card.PlayerEquip, + Card.PlayerJudge, + Card.DiscardPile, + Card.Processing, + Card.PlayerHand, + Card.PlayerSpecial + } or + { Card.PlayerEquip, Card.PlayerJudge, Card.DiscardPile, Card.Processing } return table.contains(areas, area) end -- forceVisible make the move visible -- if move is relevant to player's hands or equips, it should be open - -- cards move from/to equip/judge/discard/processing should be open + -- cards move from/to equip/judge/discard/processing should be open - if not (move.moveVisible or forceVisible or containArea(move.toArea, move.to and p.isBuddy and p:isBuddy(move.to))) then + if + not (move.moveVisible or forceVisible or containArea(move.toArea, move.to and p.isBuddy and p:isBuddy(move.to))) + then for _, info in ipairs(move.moveInfo) do if not containArea(info.fromArea, move.from == p.id) then - info.cardId = -1 + info.cardId = -1 + end end end end - end p:doNotify("MoveCards", json.encode(arg)) end end @@ -851,7 +862,7 @@ end ---@param command string @ 烧条的提示文字 function Room:notifyMoveFocus(players, command) if (players.class) then - players = {players} + players = { players } end local ids = {} @@ -868,10 +879,7 @@ function Room:notifyMoveFocus(players, command) end end - self:doBroadcastNotify("MoveFocus", json.encode{ - ids, - command - }) + self:doBroadcastNotify("MoveFocus", json.encode { ids, command }) end --- 向战报中发送一条log。 @@ -881,11 +889,11 @@ function Room:sendLog(log) end function Room:sendFootnote(ids, log) - self:doBroadcastNotify("SetCardFootnote", json.encode{ ids, log }) + self:doBroadcastNotify("SetCardFootnote", json.encode { ids, log }) end function Room:sendCardVirtName(ids, name) - self:doBroadcastNotify("SetCardVirtName", json.encode{ ids, name }) + self:doBroadcastNotify("SetCardVirtName", json.encode { ids, name }) end --- 播放某种动画效果给players看。 @@ -904,10 +912,13 @@ end ---@param player ServerPlayer @ 被播放动画的那个角色 ---@param name string @ emotion名字,可以是一个路径 function Room:setEmotion(player, name) - self:doAnimate("Emotion", { - player = player.id, - emotion = name - }) + self:doAnimate( + "Emotion", + { + player = player.id, + emotion = name + } + ) end --- 在一张card上播放一段emotion动效。 @@ -916,11 +927,14 @@ end ---@param cid integer @ 被播放动效的那个牌的id ---@param name string @ emotion名字,可以是一个路径 function Room:setCardEmotion(cid, name) - self:doAnimate("Emotion", { - player = cid, - emotion = name, - is_card = true, - }) + self:doAnimate( + "Emotion", + { + player = cid, + emotion = name, + is_card = true + } + ) end --- 播放一个全屏大动画。可以自己指定qml文件路径和额外的信息。 @@ -928,10 +942,13 @@ end ---@param extra_data any @ 要传递的额外信息 function Room:doSuperLightBox(path, extra_data) path = path or "RoomElement/SuperLightBox.qml" - self:doAnimate("SuperLightBox", { - path = path, - data = extra_data, - }) + self:doAnimate( + "SuperLightBox", + { + path = path, + data = extra_data + } + ) end --- 基本上是个不常用函数就是了 @@ -945,20 +962,26 @@ end ---@param skill_name nil @ 技能名 ---@param index integer | nil @ 语音编号,默认为-1(也就是随机播放) function Room:broadcastSkillInvoke(skill_name, index) - print 'Room:broadcastSkillInvoke deprecated; use SPlayer:broadcastSkillInvoke' + print "Room:broadcastSkillInvoke deprecated; use SPlayer:broadcastSkillInvoke" index = index or -1 - self:sendLogEvent("PlaySkillSound", { - name = skill_name, - i = index - }) + self:sendLogEvent( + "PlaySkillSound", + { + name = skill_name, + i = index + } + ) end --- 播放一段音频。 ---@param path string @ 音频文件路径 function Room:broadcastPlaySound(path) - self:sendLogEvent("PlaySound", { - name = path, - }) + self:sendLogEvent( + "PlaySound", + { + name = path + } + ) end --- 在player的脸上播放技能发动的特效。 @@ -971,7 +994,9 @@ function Room:notifySkillInvoked(player, skill_name, skill_type) local bigAnim = false if not skill_type then local skill = Fk.skills[skill_name] - if not skill then skill_type = "" end + if not skill then + skill_type = "" + end if skill.frequency == Skill.Limited or skill.frequency == Skill.Wake then bigAnim = true @@ -980,25 +1005,33 @@ function Room:notifySkillInvoked(player, skill_name, skill_type) skill_type = skill.anim_type end - if skill_type == "big" then bigAnim = true end + if skill_type == "big" then + bigAnim = true + end - self:sendLog{ + self:sendLog { type = "#InvokeSkill", from = player.id, - arg = skill_name, + arg = skill_name } if not bigAnim then - self:doAnimate("InvokeSkill", { - name = skill_name, - player = player.id, - skill_type = skill_type, - }) + self:doAnimate( + "InvokeSkill", + { + name = skill_name, + player = player.id, + skill_type = skill_type + } + ) else - self:doAnimate("InvokeUltSkill", { - name = skill_name, - player = player.id, - }) + self:doAnimate( + "InvokeUltSkill", + { + name = skill_name, + player = player.id + } + ) self:delay(2000) end end @@ -1011,10 +1044,13 @@ function Room:doIndicate(source, targets) for _, id in ipairs(targets) do table.insert(target_group, { id }) end - self:doAnimate("Indicate", { - from = source, - to = target_group, - }) + self:doAnimate( + "Indicate", + { + from = source, + to = target_group + } + ) end ------------------------------------------------------------------------ @@ -1043,8 +1079,8 @@ 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, json.encode(extra_data)} + self:notifyMoveFocus(player, extra_data.skillName or skill_name) -- for display skill name instead of command name + local data = { skill_name, prompt, cancelable, json.encode(extra_data) } Fk.currentResponseReason = extra_data.skillName local result = self:doRequest(player, command, json.encode(data)) @@ -1068,11 +1104,14 @@ function Room:askForUseActiveSkill(player, skill_name, prompt, cancelable, extra end if skill:isInstanceOf(ActiveSkill) then - skill:onUse(self, { - from = player.id, - cards = selected_cards, - tos = targets, - }) + skill:onUse( + self, + { + from = player.id, + cards = selected_cards, + tos = targets + } + ) end return true, { @@ -1097,37 +1136,49 @@ Room.askForUseViewAsSkill = Room.askForUseActiveSkill ---@param skipDiscard bool @ 是否跳过弃牌(即只询问选择可以弃置的牌) ---@param no_indicate bool @ 是否不显示指示线 ---@return integer[] @ 弃掉的牌的id列表,可能是空的 -function Room:askForDiscard(player, minNum, maxNum, includeEquip, skillName, cancelable, pattern, prompt, skipDiscard, no_indicate) +function Room:askForDiscard( + player, + minNum, + maxNum, + includeEquip, + skillName, + cancelable, + pattern, + prompt, + skipDiscard, + no_indicate) cancelable = (cancelable == nil) and true or cancelable no_indicate = no_indicate or false pattern = pattern or "." - local canDiscards = table.filter( - player:getCardIds{ Player.Hand, includeEquip and Player.Equip or nil }, function(id) - local checkpoint = true - local card = Fk:getCardById(id) + local canDiscards = + table.filter( + player:getCardIds { Player.Hand, includeEquip and Player.Equip or nil }, + function(id) + local checkpoint = true + local card = Fk:getCardById(id) - local status_skills = Fk:currentRoom().status_skills[ProhibitSkill] or Util.DummyTable - for _, skill in ipairs(status_skills) do - if skill:prohibitDiscard(player, card) then - return false - end - end - if skillName == "game_rule" then - status_skills = Fk:currentRoom().status_skills[MaxCardsSkill] or Util.DummyTable - for _, skill in ipairs(status_skills) do - if skill:excludeFrom(player, card) then - return false + local status_skills = Fk:currentRoom().status_skills[ProhibitSkill] or Util.DummyTable + for _, skill in ipairs(status_skills) do + if skill:prohibitDiscard(player, card) then + return false + end + end + if skillName == "game_rule" then + status_skills = Fk:currentRoom().status_skills[MaxCardsSkill] or Util.DummyTable + for _, skill in ipairs(status_skills) do + if skill:excludeFrom(player, card) then + return false + end + end end - end - end - if pattern ~= "" then - checkpoint = checkpoint and (Exppattern:Parse(pattern):match(card)) - end - return checkpoint - end - ) + if pattern ~= "" then + checkpoint = checkpoint and (Exppattern:Parse(pattern):match(card)) + end + return checkpoint + end + ) -- maxNum = math.min(#canDiscards, maxNum) -- minNum = math.min(#canDiscards, minNum) @@ -1145,7 +1196,7 @@ function Room:askForDiscard(player, minNum, maxNum, includeEquip, skillName, can min_num = minNum, include_equip = includeEquip, skillName = skillName, - pattern = pattern, + pattern = pattern } local prompt = prompt or ("#AskForDiscard:::" .. maxNum .. ":" .. minNum) local _, ret = self:askForUseActiveSkill(player, "discard_skill", prompt, cancelable, data, no_indicate) @@ -1153,7 +1204,9 @@ function Room:askForDiscard(player, minNum, maxNum, includeEquip, skillName, can if ret then toDiscard = ret.cards else - if cancelable then return {} end + if cancelable then + return {} + end toDiscard = table.random(canDiscards, minNum) end @@ -1214,7 +1267,17 @@ end ---@param expand_pile string|nil @ 可选私人牌堆名称 ---@param no_indicate bool @ 是否不显示指示线 ---@return integer[] @ 选择的牌的id列表,可能是空的 -function Room:askForCard(player, minNum, maxNum, includeEquip, skillName, cancelable, pattern, prompt, expand_pile, no_indicate) +function Room:askForCard( + player, + minNum, + maxNum, + includeEquip, + skillName, + cancelable, + pattern, + prompt, + expand_pile, + no_indicate) if minNum < 1 then return nil end @@ -1229,14 +1292,16 @@ function Room:askForCard(player, minNum, maxNum, includeEquip, skillName, cancel include_equip = includeEquip, skillName = skillName, pattern = pattern, - expand_pile = expand_pile, + expand_pile = expand_pile } local prompt = prompt or ("#AskForCard:::" .. maxNum .. ":" .. minNum) local _, ret = self:askForUseActiveSkill(player, "choose_cards_skill", prompt, cancelable, data, no_indicate) if ret then chosenCards = ret.cards else - if cancelable then return {} end + if cancelable then + return {} + end local hands = player:getCardIds(Player.Hand) if includeEquip then table.insertTable(hands, player:getCardIds(Player.Equip)) @@ -1252,7 +1317,6 @@ function Room:askForCard(player, minNum, maxNum, includeEquip, skillName, cancel end --- 询问玩家选择1张牌和若干名角色。 ---- --- 返回两个值,第一个是选择的目标列表,第二个是选择的那张牌的id ---@param player ServerPlayer @ 要询问的玩家 ---@param targets integer[] @ 选择目标的id范围 @@ -1263,7 +1327,16 @@ end ---@param cancelable bool @ 能否点取消 ---@param no_indicate bool @ 是否不显示指示线 ---@return integer[], integer -function Room:askForChooseCardAndPlayers(player, targets, minNum, maxNum, pattern, prompt, skillName, cancelable, no_indicate) +function Room:askForChooseCardAndPlayers( + player, + targets, + minNum, + maxNum, + pattern, + prompt, + skillName, + cancelable, + no_indicate) if maxNum < 1 then return {} end @@ -1271,11 +1344,17 @@ function Room:askForChooseCardAndPlayers(player, targets, minNum, maxNum, patter no_indicate = no_indicate or false pattern = pattern or "." - local pcards = table.filter(player:getCardIds({ Player.Hand, Player.Equip }), function(id) - local c = Fk:getCardById(id) - return c:matchPattern(pattern) - end) - if #pcards == 0 and not cancelable then return {} end + local pcards = + table.filter( + player:getCardIds({ Player.Hand, Player.Equip }), + function(id) + local c = Fk:getCardById(id) + return c:matchPattern(pattern) + end + ) + if #pcards == 0 and not cancelable then + return {} + end local data = { targets = targets, @@ -1307,18 +1386,22 @@ function Room:askForGeneral(player, generals, n, noConvert) self:notifyMoveFocus(player, command) n = n or 1 - if #generals == n then return n == 1 and generals[1] or generals end + 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 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 + if #choices == 1 then + return choices[1] + end return choices end @@ -1329,16 +1412,26 @@ end ---@param players ServerPlayer[]|nil @ 询问目标 function Room:askForChooseKingdom(players) players = players or self.alive_players - local specialKingdomPlayers = table.filter(players, function(p) - return p.kingdom == "god" or Fk.generals[p.general].subkingdom - end) + local specialKingdomPlayers = + table.filter( + players, + function(p) + return p.kingdom == "god" or Fk.generals[p.general].subkingdom + end + ) if #specialKingdomPlayers > 0 then local choiceMap = {} for _, p in ipairs(specialKingdomPlayers) do local allKingdoms = {} if p.kingdom == "god" then - allKingdoms = table.filter({"wei", "shu", "wu", "qun", "jin"}, function(k) return table.contains(Fk.kingdoms, k) end) + allKingdoms = + table.filter( + { "wei", "shu", "wu", "qun", "jin" }, + function(k) + return table.contains(Fk.kingdoms, k) + end + ) else local curGeneral = Fk.generals[p.general] allKingdoms = { curGeneral.kingdom, curGeneral.subkingdom } @@ -1376,9 +1469,8 @@ end function Room:askForCardChosen(chooser, target, flag, reason) local command = "AskForCardChosen" self:notifyMoveFocus(chooser, command) - local data = {target.id, flag, reason} + local data = { target.id, flag, reason } local result = self:doRequest(chooser, command, json.encode(data)) - if result == "" then local areas = {} local handcards @@ -1404,7 +1496,6 @@ function Room:askForCardChosen(chooser, target, flag, reason) if #handcards == 0 then return end result = table.random(handcards) end - return result end @@ -1424,7 +1515,7 @@ function Room:askForCardsChosen(chooser, target, min, max, flag, reason) local command = "AskForCardsChosen" self:notifyMoveFocus(chooser, command) - local data = {target.id, min, max, flag, reason} + local data = { target.id, min, max, flag, reason } local result = self:doRequest(chooser, command, json.encode(data)) local ret @@ -1434,9 +1525,15 @@ function Room:askForCardsChosen(chooser, target, min, max, flag, reason) local areas = {} local handcards if type(flag) == "string" then - if string.find(flag, "h") then table.insert(areas, Player.Hand) end - if string.find(flag, "e") then table.insert(areas, Player.Equip) end - if string.find(flag, "j") then table.insert(areas, Player.Judge) end + if string.find(flag, "h") then + table.insert(areas, Player.Hand) + end + if string.find(flag, "e") then + table.insert(areas, Player.Equip) + end + if string.find(flag, "j") then + table.insert(areas, Player.Judge) + end handcards = target:getCardIds(areas) else handcards = {} @@ -1444,11 +1541,19 @@ function Room:askForCardsChosen(chooser, target, min, max, flag, reason) table.insertTable(handcards, t[2]) end end - if #handcards == 0 then return {} end + if #handcards == 0 then + return {} + end ret = table.random(handcards, math.min(min, #handcards)) end - local new_ret = table.filter(ret, function(id) return id ~= -1 end) + local new_ret = + table.filter( + ret, + function(id) + return id ~= -1 + end + ) local hand_num = #ret - #new_ret if hand_num > 0 then table.insertTable(new_ret, table.random(target:getCardIds(Player.Hand), hand_num)) @@ -1457,33 +1562,6 @@ 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[] @ 可选选项列表 @@ -1493,16 +1571,26 @@ end ---@param all_choices string[]|nil @ 所有选项(不可选变灰) ---@return string @ 选择的选项 function Room:askForChoice(player, choices, skill_name, prompt, detailed, all_choices) - if #choices == 1 and not all_choices then return choices[1] end - assert(not all_choices or table.every(choices, function(c) return table.contains(all_choices, c) end)) + if #choices == 1 and not all_choices then + return choices[1] + end + assert( + not all_choices or + table.every( + choices, + function(c) + return table.contains(all_choices, c) + end + ) + ) 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{ - choices, all_choices, skill_name, prompt, detailed - }) - if result == "" then result = choices[1] end + local result = self:doRequest(player, command, json.encode { choices, all_choices, skill_name, prompt, detailed }) + if result == "" then + result = choices[1] + end return result end @@ -1516,12 +1604,14 @@ 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 + local result = self:doRequest(player, command, json.encode { skill_name, prompt, data }) + if result ~= "" then + invoked = true + end return invoked end ---为使用牌增减目标 +-- 为使用牌增减目标 ---@param player ServerPlayer @ 执行的玩家 ---@param targets ServerPlayer[] @ 可选的目标范围 ---@param num integer @ 可选的目标数 @@ -1538,12 +1628,23 @@ function Room:askForAddTarget(player, targets, num, can_minus, distance_limited, local room = player.room local tos = {} local orig_tos = table.simpleClone(AimGroup:getAllTargets(data.tos)) - if can_minus and #orig_tos > 1 then --默认不允许减目标至0 - tos = table.map(table.filter(targets, function(p) - return table.contains(AimGroup:getAllTargets(data.tos), p.id) end), Util.IdMapper) + if can_minus and #orig_tos > 1 then -- 默认不允许减目标至0 + tos = + table.map( + table.filter( + targets, + function(p) + return table.contains(AimGroup:getAllTargets(data.tos), p.id) + end + ), + Util.IdMapper + ) end for _, p in ipairs(targets) do - if not table.contains(AimGroup:getAllTargets(data.tos), p.id) and not room:getPlayerById(data.from):isProhibited(p, data.card) then + if + not table.contains(AimGroup:getAllTargets(data.tos), p.id) and + not room:getPlayerById(data.from):isProhibited(p, data.card) + then if data.card.skill:modTargetFilter(p.id, orig_tos, data.from, data.card, distance_limited) then table.insertIfNeed(tos, p.id) end @@ -1551,18 +1652,35 @@ function Room:askForAddTarget(player, targets, num, can_minus, distance_limited, end if #tos > 0 then tos = room:askForChoosePlayers(player, tos, 1, num, prompt, skillName, true) - --借刀……! + -- 借刀……! if data.card.name ~= "collateral" then return tos else local result = {} for _, id in ipairs(tos) do local to = room:getPlayerById(id) - local target = room:askForChoosePlayers(player, table.map(table.filter(room:getOtherPlayers(player), function(v) - return to:inMyAttackRange(v) end), function(p) return p.id end), 1, 1, - "#collateral-choose::"..to.id..":"..data.card:toLogString(), "collateral_skill", true) + local target = + room:askForChoosePlayers( + player, + table.map( + table.filter( + room:getOtherPlayers(player), + function(v) + return to:inMyAttackRange(v) + end + ), + function(p) + return p.id + end + ), + 1, + 1, + "#collateral-choose::" .. to.id .. ":" .. data.card:toLogString(), + "collateral_skill", + true + ) if #target > 0 then - table.insert(result, {id, target[1]}) + table.insert(result, { id, target[1] }) end end if #result > 0 then @@ -1601,7 +1719,10 @@ function Room:askForGuanxing(player, cards, top_limit, bottom_limit, customNotif assert(bottom_limit[1] <= bottom_limit[2], "limits error: The upper limit should be less than the lower limit") end if #top_limit > 0 and #bottom_limit > 0 then - assert(#cards >= top_limit[1] + bottom_limit[1] and #cards <= top_limit[2] + bottom_limit[2], "limits Error: No enough space") + assert( + #cards >= top_limit[1] + bottom_limit[1] and #cards <= top_limit[2] + bottom_limit[2], + "limits Error: No enough space" + ) end if areaNames then assert(#areaNames == 2, "areaNames error: Should have 2 elements") @@ -1616,7 +1737,7 @@ function Room:askForGuanxing(player, cards, top_limit, bottom_limit, customNotif min_bottom_cards = bottom_limit and bottom_limit[1] or 0, max_bottom_cards = bottom_limit and bottom_limit[2] or #cards, top_area_name = areaNames and areaNames[1] or "Top", - bottom_area_name = areaNames and areaNames[2] or "Bottom", + bottom_area_name = areaNames and areaNames[2] or "Bottom" } local result = self:doRequest(player, command, json.encode(data)) @@ -1632,7 +1753,15 @@ function Room:askForGuanxing(player, cards, top_limit, bottom_limit, customNotif end else top = table.random(cards, top_limit and top_limit[2] or #cards) or Util.DummyTable - bottom = table.shuffle(table.filter(cards, function(id) return not table.contains(top, id) end)) or Util.DummyTable + bottom = + table.shuffle( + table.filter( + cards, + function(id) + return not table.contains(top, id) + end + ) + ) or Util.DummyTable end if not noPut then @@ -1643,15 +1772,18 @@ function Room:askForGuanxing(player, cards, top_limit, bottom_limit, customNotif table.insert(self.draw_pile, bottom[i]) end - self:sendLog{ + self:sendLog { type = "#GuanxingResult", from = player.id, arg = #top, - arg2 = #bottom, + arg2 = #bottom } end - return { top = top, bottom = bottom } + return { + top = top, + bottom = bottom + } end --- 询问玩家任意交换几堆牌堆。 @@ -1673,7 +1805,7 @@ function Room:askForExchange(player, piles, piles_name, customNotify) self:notifyMoveFocus(player, customNotify or command) local data = { piles = piles, - piles_name = piles_name, + piles_name = piles_name } local result = self:doRequest(player, command, json.encode(data)) if result ~= "" then @@ -1683,6 +1815,7 @@ function Room:askForExchange(player, piles, piles_name, customNotify) return piles end end + --- 平时写DIY用不到的函数。 ---@param player ServerPlayer ---@param data string @@ -1695,22 +1828,30 @@ function Room:handleUseCardReply(player, data) local card_data = json.decode(card) local skill = Fk.skills[card_data.skill] local selected_cards = card_data.subcards - if skill.interaction then skill.interaction.data = data.interaction_data end + if skill.interaction then + skill.interaction.data = data.interaction_data + end if skill:isInstanceOf(ActiveSkill) then - self:useSkill(player, skill, function() - self:doIndicate(player.id, targets) - skill:onUse(self, { - from = player.id, - cards = selected_cards, - tos = targets, - }) - end) - return nil + self:useSkill( + player, + skill, + function() + self:doIndicate(player.id, targets) + skill:onUse( + self, + { + from = player.id, + cards = selected_cards, + tos = targets + } + ) + end + ) elseif skill:isInstanceOf(ViewAsSkill) then Self = player local c = skill:viewAs(selected_cards) if c then - local use = {} ---@type CardUseStruct + local use = {} ---@type CardUseStruct use.from = player.id use.tos = {} for _, target in ipairs(targets) do @@ -1732,26 +1873,30 @@ function Room:handleUseCardReply(player, data) if data.special_skill then local skill = Fk.skills[data.special_skill] assert(skill:isInstanceOf(ActiveSkill)) - skill:onUse(self, { - from = player.id, - cards = { card }, - tos = targets, - }) - return nil + skill:onUse( + self, + { + from = player.id, + cards = { card }, + tos = targets + } + ) + else + local use = {} ---@type CardUseStruct + use.from = player.id + use.tos = {} + for _, target in ipairs(targets) do + table.insert(use.tos, { target }) + end + if #use.tos == 0 then + use.tos = nil + end + Fk:filterCard(card, player) + use.card = Fk:getCardById(card) + return use end - local use = {} ---@type CardUseStruct - use.from = player.id - use.tos = {} - for _, target in ipairs(targets) do - table.insert(use.tos, { target }) - end - if #use.tos == 0 then - use.tos = nil - end - Fk:filterCard(card, player) - use.card = Fk:getCardById(card) - return use end + return nil end -- available extra_data: @@ -1769,67 +1914,89 @@ end ---@param event_data CardEffectEvent|nil @ 事件信息 ---@return CardUseStruct | nil @ 返回关于本次使用牌的数据,以便后续处理 function Room:askForUseCard(player, card_name, pattern, prompt, cancelable, extra_data, event_data) - if event_data and (event_data.disresponsive or table.contains(event_data.disresponsiveList or Util.DummyTable, player.id)) then + if + event_data and + (event_data.disresponsive or table.contains(event_data.disresponsiveList or Util.DummyTable, player.id)) + then return nil end if event_data and event_data.prohibitedCardNames and card_name then local splitedCardNames = card_name:split(",") - splitedCardNames = table.filter(splitedCardNames, function(name) - return not table.contains(event_data.prohibitedCardNames, name) - end) + splitedCardNames = + table.filter( + splitedCardNames, + function(name) + return not table.contains(event_data.prohibitedCardNames, name) + end + ) if #splitedCardNames == 0 then return nil end - card_name = table.concat(splitedCardNames, ",") end - if extra_data then if extra_data.bypass_distances then player.room:setPlayerMark(player, MarkEnum.BypassDistancesLimit .. "-tmp", 1) end - if extra_data.bypass_times == nil or extra_data.bypass_times then + if extra_data.bypass_times ~= false then player.room:setPlayerMark(player, MarkEnum.BypassTimesLimit .. "-tmp", 1) end + fk.useMustTargets = extra_data.must_targets end local command = "AskForUseCard" self:notifyMoveFocus(player, card_name) - cancelable = (cancelable == nil) and true or cancelable - extra_data = extra_data or Util.DummyTable + cancelable = cancelable ~= false pattern = pattern or card_name prompt = prompt or "" - local askForUseCardData = { + local useData = { user = player, cardName = card_name, pattern = pattern, - extraData = extra_data, - eventData = event_data, + extraData = extra_data or Util.DummyTable, + eventData = event_data } - self.logic:trigger(fk.AskForCardUse, player, askForUseCardData) - - if askForUseCardData.result and type(askForUseCardData.result) == 'table' then - player.room:setPlayerMark(player, MarkEnum.BypassDistancesLimit .. "-tmp", 0) - player.room:setPlayerMark(player, MarkEnum.BypassTimesLimit .. "-tmp", 0) - return askForUseCardData.result - else - local data = {card_name, pattern, prompt, cancelable, extra_data} + local use = nil + self.logic:trigger(fk.AskForCardUse, player, useData) + if type(useData.result) == "table" then + useData = useData.result + useData.extraUse = extra_data ~= nil + self:useCard(useData) + if useData.nullified then + use = false + elseif useData.breakEvent ~= true then + use = useData + end + end + local data = { card_name, pattern, prompt, cancelable, extra_data or Util.DummyTable } + while use == nil do Fk.currentResponsePattern = pattern local result = self:doRequest(player, command, json.encode(data)) Fk.currentResponsePattern = nil - if result ~= "" then - player.room:setPlayerMark(player, MarkEnum.BypassDistancesLimit .. "-tmp", 0) - player.room:setPlayerMark(player, MarkEnum.BypassTimesLimit .. "-tmp", 0) - return self:handleUseCardReply(player, result) + result = self:handleUseCardReply(player, result) + if result then + result.extraUse = extra_data ~= nil + self:useCard(result) + if result.nullified then + use = false + elseif result.breakEvent ~= true then + use = result + end + else + break + end + else + break end end + fk.useMustTargets = nil player.room:setPlayerMark(player, MarkEnum.BypassDistancesLimit .. "-tmp", 0) player.room:setPlayerMark(player, MarkEnum.BypassTimesLimit .. "-tmp", 0) - return nil + return use end --- 询问一名玩家打出一张牌。 @@ -1840,15 +2007,19 @@ end ---@param cancelable bool @ 能否取消 ---@param extra_data any|nil @ 额外数据 ---@param effectData CardEffectEvent|nil @ 关联的卡牌生效流程 +---@param retrial bool @ 改判打出 ---@return Card | nil @ 打出的牌 -function Room:askForResponse(player, card_name, pattern, prompt, cancelable, extra_data, effectData) - if effectData and (effectData.disresponsive or table.contains(effectData.disresponsiveList or Util.DummyTable, player.id)) then +function Room:askForResponse(player, card_name, pattern, prompt, cancelable, extra_data, effectData, retrial) + if + effectData and + (effectData.disresponsive or table.contains(effectData.disresponsiveList or Util.DummyTable, player.id)) + then return nil end local command = "AskForResponseCard" self:notifyMoveFocus(player, card_name) - cancelable = (cancelable == nil) and true or cancelable + cancelable = cancelable ~= false extra_data = extra_data or Util.DummyTable pattern = pattern or card_name prompt = prompt or "" @@ -1857,27 +2028,52 @@ function Room:askForResponse(player, card_name, pattern, prompt, cancelable, ext user = player, cardName = card_name, pattern = pattern, - extraData = extra_data, + extraData = extra_data } + local response = nil self.logic:trigger(fk.AskForCardResponse, player, eventData) - if eventData.result then - return eventData.result - else - local data = {card_name, pattern, prompt, cancelable, extra_data} - + eventData = { + from = player.id, + card = eventData.result, + skipDrop = true, + retrial = retrial, + responseToEvent = effectData + } + eventData.extraResponse = extra_data ~= nil + self:responseCard(eventData) + if eventData.nullified then + response = false + elseif eventData.breakEvent ~= true then + response = eventData + end + end + local data = { card_name, pattern, prompt, cancelable, extra_data } + while response == nil do Fk.currentResponsePattern = pattern local result = self:doRequest(player, command, json.encode(data)) Fk.currentResponsePattern = nil - if result ~= "" then - local use = self:handleUseCardReply(player, result) - if use then - return use.card + result = self:handleUseCardReply(player, result) + if result then + result.skipDrop = true + result.retrial = retrial + result.responseToEvent = effectData + result.extraResponse = extra_data ~= nil + self:responseCard(result) + if result.nullified then + response = false + elseif result.breakEvent ~= true then + response = result + end + else + break end + else + break end end - return nil + return response end --- 同时询问多名玩家是否使用某一张牌。 @@ -1891,35 +2087,45 @@ end ---@param extra_data any|nil @ 额外信息 ---@return CardUseStruct | nil @ 最终决胜出的卡牌使用信息 function Room:askForNullification(players, card_name, pattern, prompt, cancelable, extra_data) - if #players == 0 then - return nil - end - local command = "AskForUseCard" card_name = card_name or "nullification" - cancelable = (cancelable == nil) and true or cancelable - extra_data = extra_data or Util.DummyTable + cancelable = cancelable ~= false prompt = prompt or "" pattern = pattern or card_name self:notifyMoveFocus(self.alive_players, card_name) self:doBroadcastNotify("WaitForNullification", "") - - local data = {card_name, pattern, prompt, cancelable, extra_data} - - Fk.currentResponsePattern = pattern - local winner = self:doRaceRequest(command, players, json.encode(data)) - - if winner then - local result = winner.client_reply - return self:handleUseCardReply(winner, result) + local use = nil + local data = { card_name, pattern, prompt, cancelable, extra_data or Util.DummyTable } + while use == nil do + Fk.currentResponsePattern = pattern + local winner = self:doRaceRequest(command, players, json.encode(data)) + Fk.currentResponsePattern = nil + if winner then + local reply = self:handleUseCardReply(winner, winner.client_reply) + if reply then + local event = self.logic:getCurrentEvent():findParent(GameEvent.CardEffect, true).data[1] + reply.responseToEvent = event + reply.toCard = event.card + reply.extraUse = extra_data ~= nil + self:useCard(reply) + if reply.nullified then + use = false + elseif reply.breakEvent ~= true then + use = reply + end + else + break + end + else + break + end end - Fk.currentResponsePattern = nil - return nil + return use end -- AG(a.k.a. Amazing Grace) functions --- Popup a box that contains many cards, then ask player to choose one +-- Popup a box that contains many cards, then ask player:delay(1200) to choose one --- 询问玩家从AG中选择一张牌。 ---@param player ServerPlayer @ 要询问的玩家 @@ -1950,7 +2156,7 @@ end function Room:fillAG(player, id_list, disable_ids) id_list = Card:getIdList(id_list) -- disable_ids = Card:getIdList(disable_ids) - player:doNotify("FillAG", json.encode{ id_list, disable_ids }) + player:doNotify("FillAG", json.encode { id_list, disable_ids }) end --- 告诉一些玩家,AG中的牌被taker取走了。 @@ -1958,7 +2164,7 @@ end ---@param id integer @ 被拿走的牌 ---@param notify_list ServerPlayer[]|nil @ 要告知的玩家,默认为全员 function Room:takeAG(taker, id, notify_list) - self:doBroadcastNotify("TakeAG", json.encode{ taker.id, id }, notify_list) + self:doBroadcastNotify("TakeAG", json.encode { taker.id, id }, notify_list) end --- 关闭player那侧显示的AG。 @@ -1966,8 +2172,11 @@ end --- 若不传参(即player为nil),那么关闭所有玩家的AG。 ---@param player ServerPlayer|nil @ 要关闭AG的玩家 function Room:closeAG(player) - if player then player:doNotify("CloseAG", "") - else self:doBroadcastNotify("CloseAG", "") end + if player then + player:doNotify("CloseAG", "") + else + self:doBroadcastNotify("CloseAG", "") + end end -- Show a qml dialog and return qml's ClientInstance.replyToServer @@ -1981,10 +2190,14 @@ end function Room:askForCustomDialog(player, focustxt, qmlPath, extra_data) local command = "CustomDialog" self:notifyMoveFocus(player, focustxt) - return self:doRequest(player, command, json.encode{ - path = qmlPath, - data = extra_data, - }) + return self:doRequest( + player, + command, + json.encode { + path = qmlPath, + data = extra_data + } + ) end ---@param player ServerPlayer @ 移动的操作 @@ -2022,12 +2235,15 @@ function Room:askForMoveCardInBoard(player, targetOne, targetTwo, skillName, fla end if #cards > 0 then - table.sort(cards, function(prev, next) - local prevSubType = Fk:getCardById(prev).sub_type - local nextSubType = Fk:getCardById(next).sub_type + table.sort( + cards, + function(prev, next) + local prevSubType = Fk:getCardById(prev).sub_type + local nextSubType = Fk:getCardById(next).sub_type - return prevSubType < nextSubType - end) + return prevSubType < nextSubType + end + ) for _, id in ipairs(cards) do table.insert(cardsPosition, self:getCardOwner(id) == targetOne and 0 or 1) @@ -2058,7 +2274,8 @@ function Room:askForMoveCardInBoard(player, targetOne, targetTwo, skillName, fla return end - local firstGeneralName = targetOne.general + (targetOne.deputyGeneral ~= "" and ("/" .. targetOne.deputyGeneral) or "") + local firstGeneralName = + targetOne.general + (targetOne.deputyGeneral ~= "" and ("/" .. targetOne.deputyGeneral) or "") local secGeneralName = targetTwo.general + (targetTwo.deputyGeneral ~= "" and ("/" .. targetTwo.deputyGeneral) or "") local data = { @@ -2073,12 +2290,15 @@ function Room:askForMoveCardInBoard(player, targetOne, targetTwo, skillName, fla if result == "" then local randomIndex = math.random(1, #cards) - result = { cardId = cards[randomIndex], pos = cardsPosition[randomIndex] } + result = { + cardId = cards[randomIndex], + pos = cardsPosition[randomIndex] + } else result = json.decode(result) end - local from, to = result.pos == 0 and targetOne, targetTwo or targetTwo, targetOne + local from, to = result.pos == 0 and targetOne, targetTwo and targetTwo or targetOne local cardToMove = self:getCardOwner(result.cardId):getVirualEquip(result.cardId) or Fk:getCardById(result.cardId) self:moveCardTo( cardToMove, @@ -2091,7 +2311,11 @@ function Room:askForMoveCardInBoard(player, targetOne, targetTwo, skillName, fla player.id ) - return { card = cardToMove, from = from.id, to = to.id } + return { + card = cardToMove, + from = from.id, + to = to.id + } end --- 询问一名玩家从targets中选择出若干名玩家来移动场上的牌。 @@ -2113,16 +2337,17 @@ function Room:askForChooseToMoveCardInBoard(player, prompt, skillName, cancelabl local data = { flag = flag, skillName = skillName, - excludeIds = excludeIds, + excludeIds = excludeIds } - local _, ret = self:askForUseActiveSkill( - player, - "choose_players_to_move_card_in_board", - prompt or "", - cancelable, - data, - no_indicate - ) + local _, ret = + self:askForUseActiveSkill( + player, + "choose_players_to_move_card_in_board", + prompt or "", + cancelable, + data, + no_indicate + ) if ret then return ret.targets @@ -2151,8 +2376,7 @@ end ---@param aimEventCollaborators table ---@return boolean local onAim = function(room, cardUseEvent, aimEventCollaborators) - local eventStages = { fk.TargetSpecifying, fk.TargetConfirming, fk.TargetSpecified, fk.TargetConfirmed } - for _, stage in ipairs(eventStages) do + for _, stage in ipairs({ fk.TargetSpecifying, fk.TargetConfirming, fk.TargetSpecified, fk.TargetConfirmed }) do if (not cardUseEvent.tos) or #cardUseEvent.tos == 0 then return false end @@ -2180,7 +2404,7 @@ local onAim = function(room, cardUseEvent, aimEventCollaborators) firstTarget = firstTarget, additionalDamage = cardUseEvent.additionalDamage, additionalRecover = cardUseEvent.additionalRecover, - extra_data = cardUseEvent.extra_data, + extra_data = cardUseEvent.extra_data } local index = 1 @@ -2212,7 +2436,14 @@ local onAim = function(room, cardUseEvent, aimEventCollaborators) firstTarget = false - if room.logic:trigger(stage, (stage == fk.TargetSpecifying or stage == fk.TargetSpecified) and room:getPlayerById(aimStruct.from) or room:getPlayerById(aimStruct.to), aimStruct) then + if + room.logic:trigger( + stage, + (stage == fk.TargetSpecifying or stage == fk.TargetSpecified) and room:getPlayerById(aimStruct.from) or + room:getPlayerById(aimStruct.to), + aimStruct + ) + then return false end AimGroup:removeDeadTargets(room, aimStruct) @@ -2264,7 +2495,7 @@ end function Room:doCardUseEffect(cardUseEvent) ---@type table local aimEventCollaborators = {} - if cardUseEvent.tos and not onAim(self, cardUseEvent, aimEventCollaborators) then + if cardUseEvent.tos and not onAim(self, cardUseEvent, aimEventCollaborators) or cardUseEvent.nullified then return end @@ -2278,11 +2509,13 @@ function Room:doCardUseEffect(cardUseEvent) end if self:getPlayerById(TargetGroup:getRealTargets(cardUseEvent.tos)[1]).dead then - self:moveCards({ - ids = realCardIds, - toArea = Card.DiscardPile, - moveReason = fk.ReasonPutIntoDiscardPile, - }) + self:moveCards( + { + ids = realCardIds, + toArea = Card.DiscardPile, + moveReason = fk.ReasonPutIntoDiscardPile + } + ) else local target = TargetGroup:getRealTargets(cardUseEvent.tos)[1] local existingEquipId = self:getPlayerById(target):getEquipment(cardUseEvent.card.sub_type) @@ -2292,22 +2525,24 @@ function Room:doCardUseEffect(cardUseEvent) ids = { existingEquipId }, from = target, toArea = Card.DiscardPile, - moveReason = fk.ReasonPutIntoDiscardPile, + moveReason = fk.ReasonPutIntoDiscardPile }, { ids = realCardIds, to = target, toArea = Card.PlayerEquip, - moveReason = fk.ReasonUse, + moveReason = fk.ReasonUse } ) else - self:moveCards({ - ids = realCardIds, - to = target, - toArea = Card.PlayerEquip, - moveReason = fk.ReasonUse, - }) + self:moveCards( + { + ids = realCardIds, + to = target, + toArea = Card.PlayerEquip, + moveReason = fk.ReasonUse + } + ) end end @@ -2331,22 +2566,26 @@ function Room:doCardUseEffect(cardUseEvent) self:getPlayerById(target):addVirtualEquip(cardUseEvent.card) end - self:moveCards({ - ids = realCardIds, - to = target, - toArea = Card.PlayerJudge, - moveReason = fk.ReasonUse, - }) + self:moveCards( + { + ids = realCardIds, + to = target, + toArea = Card.PlayerJudge, + moveReason = fk.ReasonUse + } + ) return end end - self:moveCards({ - ids = realCardIds, - toArea = Card.DiscardPile, - moveReason = fk.ReasonPutIntoDiscardPile, - }) + self:moveCards( + { + ids = realCardIds, + toArea = Card.DiscardPile, + moveReason = fk.ReasonPutIntoDiscardPile + } + ) return end @@ -2369,7 +2608,7 @@ function Room:doCardUseEffect(cardUseEvent) additionalRecover = cardUseEvent.additionalRecover, cardsResponded = cardUseEvent.cardsResponded, prohibitedCardNames = cardUseEvent.prohibitedCardNames, - extra_data = cardUseEvent.extra_data, + extra_data = cardUseEvent.extra_data } -- If using card to other card (like jink or nullification), simply effect and return @@ -2441,10 +2680,13 @@ end ---@param cardEffectEvent CardEffectEvent function Room:handleCardEffect(event, cardEffectEvent) if event == fk.PreCardEffect then - if cardEffectEvent.card.skill:aboutToEffect(self, cardEffectEvent) then return end + if cardEffectEvent.card.skill:aboutToEffect(self, cardEffectEvent) then + return + end if - cardEffectEvent.card.trueName == "slash" and - not (cardEffectEvent.unoffsetable or table.contains(cardEffectEvent.unoffsetableList or Util.DummyTable, cardEffectEvent.to)) + cardEffectEvent.card.trueName == "slash" and + not (cardEffectEvent.unoffsetable or + table.contains(cardEffectEvent.unoffsetableList or Util.DummyTable, cardEffectEvent.to)) then local loopTimes = 1 if cardEffectEvent.fixedResponseTimes then @@ -2455,51 +2697,36 @@ function Room:handleCardEffect(event, cardEffectEvent) end end Fk.currentResponsePattern = "jink" - for i = 1, loopTimes do local to = self:getPlayerById(cardEffectEvent.to) - local prompt = "" + local prompt = nil if cardEffectEvent.from then - prompt = "#slash-jink:" .. cardEffectEvent.from .. "::" .. 1 + if loopTimes > 1 then + prompt = "#slash-jinks:" .. cardEffectEvent.from .. "::" .. loopTimes + 1 - i .. ":" .. loopTimes + else + prompt = "#slash-jink:" .. cardEffectEvent.from .. "::1" + end end - - local use = self:askForUseCard( - to, - "jink", - nil, - prompt, - true, - nil, - cardEffectEvent - ) - if use then - use.toCard = cardEffectEvent.card - use.responseToEvent = cardEffectEvent - self:useCard(use) - end - - if not cardEffectEvent.isCancellOut then + if self:askForUseCard(to, "jink", nil, prompt, true, nil, cardEffectEvent) then + cardEffectEvent.isCancellOut = true + else + cardEffectEvent.isCancellOut = false break end - - cardEffectEvent.isCancellOut = i == loopTimes end elseif - cardEffectEvent.card.type == Card.TypeTrick and - not (cardEffectEvent.disresponsive or cardEffectEvent.unoffsetable) and - not table.contains(cardEffectEvent.prohibitedCardNames or Util.DummyTable, "nullification") + cardEffectEvent.card.type == Card.TypeTrick and + not (cardEffectEvent.disresponsive or cardEffectEvent.unoffsetable) and + not table.contains(cardEffectEvent.prohibitedCardNames or Util.DummyTable, "nullification") then local players = {} Fk.currentResponsePattern = "nullification" for _, p in ipairs(self.alive_players) do - local cards = p:getHandlyIds(true) - for _, cid in ipairs(cards) do + for _, cid in ipairs(p:getHandlyIds(true)) do if - Fk:getCardById(cid).trueName == "nullification" and - not ( - table.contains(cardEffectEvent.disresponsiveList or Util.DummyTable, p.id) or - table.contains(cardEffectEvent.unoffsetableList or Util.DummyTable, p.id) - ) + Fk:getCardById(cid).trueName == "nullification" and + not (table.contains(cardEffectEvent.disresponsiveList or Util.DummyTable, p.id) or + table.contains(cardEffectEvent.unoffsetableList or Util.DummyTable, p.id)) then table.insert(players, p) break @@ -2509,13 +2736,10 @@ function Room:handleCardEffect(event, cardEffectEvent) Self = p -- for enabledAtResponse for _, s in ipairs(table.connect(p.player_skills, p._fake_skills)) do if - s.pattern and - Exppattern:Parse("nullification"):matchExp(s.pattern) and - not (s.enabledAtResponse and not s:enabledAtResponse(p)) and - not ( - table.contains(cardEffectEvent.disresponsiveList or Util.DummyTable, p.id) or - table.contains(cardEffectEvent.unoffsetableList or Util.DummyTable, p.id) - ) + s.pattern and Exppattern:Parse("nullification"):matchExp(s.pattern) and + not (s.enabledAtResponse and not s:enabledAtResponse(p)) and + not (table.contains(cardEffectEvent.disresponsiveList or Util.DummyTable, p.id) or + table.contains(cardEffectEvent.unoffsetableList or Util.DummyTable, p.id)) then table.insert(players, p) break @@ -2535,22 +2759,25 @@ function Room:handleCardEffect(event, cardEffectEvent) if #TargetGroup:getRealTargets(cardEffectEvent.tos) > 1 then local parentUseEvent = self.logic:getCurrentEvent():findParent(GameEvent.UseCard) if parentUseEvent then - extra_data = { useEventId = parentUseEvent.id, effectTo = cardEffectEvent.to } + extra_data = { + useEventId = parentUseEvent.id, + effectTo = cardEffectEvent.to + } end end - local use = self:askForNullification(players, nil, nil, prompt, true, extra_data) - if use then - use.toCard = cardEffectEvent.card - use.responseToEvent = cardEffectEvent - self:useCard(use) - end + self:askForNullification(players, nil, nil, prompt, true, extra_data) end Fk.currentResponsePattern = nil elseif event == fk.CardEffecting then if cardEffectEvent.card.skill then - execGameEvent(GameEvent.SkillEffect, function () - cardEffectEvent.card.skill:onEffect(self, cardEffectEvent) - end, self:getPlayerById(cardEffectEvent.from), cardEffectEvent.card.skill) + execGameEvent( + GameEvent.SkillEffect, + function() + cardEffectEvent.card.skill:onEffect(self, cardEffectEvent) + end, + self:getPlayerById(cardEffectEvent.from), + cardEffectEvent.card.skill + ) end end end @@ -2571,22 +2798,36 @@ function Room:useVirtualCard(card_name, subcards, from, tos, skillName, extra) local card = Fk:cloneCard(card_name) card.skillName = skillName - if from:prohibitUse(card) then return false end + if from:prohibitUse(card) then + return false + end - if tos.class then tos = { tos } end + if tos.class then + tos = { tos } + end for i, p in ipairs(tos) do if from:isProhibited(p, card) then table.remove(tos, i) end end - if #tos == 0 then return false end + if #tos == 0 then + return false + end - if subcards then card:addSubcards(Card:getIdList(subcards)) end + if subcards then + card:addSubcards(Card:getIdList(subcards)) + end local use = {} ---@type CardUseStruct use.from = from.id - use.tos = table.map(tos, function(p) return { p.id } end) + use.tos = + table.map( + tos, + function(p) + return { p.id } + end + ) use.card = card use.extraUse = extra self:useCard(use) @@ -2613,25 +2854,29 @@ end function Room:obtainCard(player, cid, unhide, reason) if type(cid) ~= "number" then assert(cid and cid:isInstanceOf(Card)) - cid = cid:isVirtual() and cid.subcards or {cid.id} + cid = cid:isVirtual() and cid.subcards or { cid.id } else - cid = {cid} + cid = { cid } + end + if #cid == 0 then + return end - if #cid == 0 then return end if type(player) == "table" then player = player.id end - self:moveCards({ - ids = cid, - from = self.owner_map[cid[1]], - to = player, - toArea = Card.PlayerHand, - moveReason = reason or fk.ReasonJustMove, - proposer = player, - moveVisible = unhide or false, - }) + self:moveCards( + { + ids = cid, + from = self.owner_map[cid[1]], + to = player, + toArea = Card.PlayerHand, + moveReason = reason or fk.ReasonJustMove, + proposer = player, + moveVisible = unhide or false + } + ) end --- 让玩家摸牌 @@ -2645,7 +2890,7 @@ function Room:drawCards(player, num, skillName, fromPlace) who = player, num = num, skillName = skillName, - fromPlace = fromPlace, + fromPlace = fromPlace } if self.logic:trigger(fk.BeforeDrawCard, player, drawData) then self.logic:breakEvent(false) @@ -2655,14 +2900,16 @@ function Room:drawCards(player, num, skillName, fromPlace) fromPlace = drawData.fromPlace local topCards = self:getNCards(num, fromPlace) - self:moveCards({ - ids = topCards, - to = player.id, - toArea = Card.PlayerHand, - moveReason = fk.ReasonDraw, - proposer = player.id, - skillName = skillName, - }) + self:moveCards( + { + ids = topCards, + to = player.id, + toArea = Card.PlayerHand, + moveReason = fk.ReasonDraw, + proposer = player.id, + skillName = skillName + } + ) return { table.unpack(topCards) } end @@ -2684,32 +2931,37 @@ function Room:moveCardTo(card, to_place, target, reason, skill_name, special_nam local ids = Card:getIdList(card) local to - if table.contains( - {Card.PlayerEquip, Card.PlayerHand, - Card.PlayerJudge, Card.PlayerSpecial}, to_place) then + if table.contains({ Card.PlayerEquip, Card.PlayerHand, Card.PlayerJudge, Card.PlayerSpecial }, to_place) then to = target.id end local movesSplitedByOwner = {} for _, cardId in ipairs(ids) do - local moveFound = table.find(movesSplitedByOwner, function(move) - return move.from == self.owner_map[cardId] - end) + local moveFound = + table.find( + movesSplitedByOwner, + function(move) + return move.from == self.owner_map[cardId] + end + ) if moveFound then table.insert(moveFound.ids, cardId) else - table.insert(movesSplitedByOwner, { - ids = { cardId }, - from = self.owner_map[cardId], - to = to, - toArea = to_place, - moveReason = reason, - skillName = skill_name, - specialName = special_name, - moveVisible = visible, - proposer = proposer, - }) + table.insert( + movesSplitedByOwner, + { + ids = { cardId }, + from = self.owner_map[cardId], + to = to, + toArea = to_place, + moveReason = reason, + skillName = skill_name, + specialName = special_name, + moveVisible = visible, + proposer = proposer + } + ) end end @@ -2737,7 +2989,9 @@ end ---@param player ServerPlayer ---@param num integer @ 变化量 function Room:changeShield(player, num) - if num == 0 then return end + if num == 0 then + return + end player.shield = math.max(player.shield + num, 0) player.shield = math.min(player.shield, 5) self:broadcastProperty(player, "shield") @@ -2802,10 +3056,14 @@ function Room:handleAddLoseSkills(player, skill_names, source_skill, sendlog, no skill_names = skill_names:split("|") end - if sendlog == nil then sendlog = true end + if sendlog == nil then + sendlog = true + end - if #skill_names == 0 then return end - local losts = {} ---@type boolean[] + if #skill_names == 0 then + return + end + local losts = {} ---@type boolean[] local triggers = {} ---@type Skill[] for _, skill in ipairs(skill_names) do if string.sub(skill, 1, 1) == "-" then @@ -2813,13 +3071,10 @@ function Room:handleAddLoseSkills(player, skill_names, source_skill, sendlog, no if player:hasSkill(actual_skill, true, true) then local lost_skills = player:loseSkill(actual_skill, source_skill) for _, s in ipairs(lost_skills) do - self:doBroadcastNotify("LoseSkill", json.encode{ - player.id, - s.name - }) + self:doBroadcastNotify("LoseSkill", json.encode { player.id, s.name }) if sendlog and s.visible then - self:sendLog{ + self:sendLog { type = "#LoseSkill", from = player.id, arg = s.name @@ -2838,13 +3093,10 @@ function Room:handleAddLoseSkills(player, skill_names, source_skill, sendlog, no for _, s in ipairs(got_skills) do -- TODO: limit skill mark - self:doBroadcastNotify("AddSkill", json.encode{ - player.id, - s.name - }) + self:doBroadcastNotify("AddSkill", json.encode { player.id, s.name }) if sendlog and s.visible then - self:sendLog{ + self:sendLog { type = "#AcquireSkill", from = player.id, arg = s.name @@ -2881,7 +3133,9 @@ end ---@param skillName string|nil @ 技能名 ---@param exchange bool @ 是否要替换原有判定牌(即类似鬼道那样) function Room:retrial(card, player, judge, skillName, exchange) - if not card then return end + if not card then + return + end local triggerResponded = self.owner_map[card:getEffectiveId()] == player local isHandcard = (triggerResponded and self:getCardArea(card:getEffectiveId()) == Card.PlayerHand) @@ -2911,12 +3165,12 @@ function Room:retrial(card, player, judge, skillName, exchange) move2.moveReason = fk.ReasonJustMove move2.to = exchange and player.id or nil - self:sendLog{ + self:sendLog { type = "#ChangedJudge", from = player.id, to = { judge.who.id }, card = { card:getEffectiveId() }, - arg = skillName, + arg = skillName } self:moveCards(move1, move2) @@ -2933,18 +3187,20 @@ end ---@param thrower ServerPlayer|nil @ 弃别人牌的人 function Room:throwCard(card_ids, skillName, who, thrower) if type(card_ids) == "number" then - card_ids = {card_ids} + card_ids = { card_ids } end skillName = skillName or "" thrower = thrower or who - self:moveCards({ - ids = card_ids, - from = who.id, - toArea = Card.DiscardPile, - moveReason = fk.ReasonDiscard, - proposer = thrower.id, - skillName = skillName - }) + self:moveCards( + { + ids = card_ids, + from = who.id, + toArea = Card.DiscardPile, + moveReason = fk.ReasonDiscard, + proposer = thrower.id, + skillName = skillName + } + ) end --- 重铸一名角色的牌。 @@ -2953,22 +3209,24 @@ end ---@param skillName string|nil @ 技能名,默认为“重铸” function Room:recastCard(card_ids, who, skillName) if type(card_ids) == "number" then - card_ids = {card_ids} + card_ids = { card_ids } end skillName = skillName or "recast" - self:moveCards({ - ids = card_ids, - from = who.id, - toArea = Card.DiscardPile, - skillName = skillName, - moveReason = fk.ReasonPutIntoDiscardPile, - proposer = who.id - }) - self:sendLog{ + self:moveCards( + { + ids = card_ids, + from = who.id, + toArea = Card.DiscardPile, + skillName = skillName, + moveReason = fk.ReasonPutIntoDiscardPile, + proposer = who.id + } + ) + self:sendLog { type = skillName == "recast" and "#Recast" or "#RecastBySkill", from = who.id, card = card_ids, - arg = skillName, + arg = skillName } self:drawCards(who, #card_ids, skillName) end @@ -3015,8 +3273,12 @@ function Room:swapSeat(a, b) local ai, bi local players = self.players for i, v in ipairs(self.players) do - if v == a then ai = i end - if v == b then bi = i end + if v == a then + ai = i + end + if v == b then + bi = i + end end players[ai] = b @@ -3088,7 +3350,9 @@ end ---@param player ServerPlayer ---@param sendLog bool function Room:revivePlayer(player, sendLog) - if not player.dead then return end + if not player.dead then + return + end self:setPlayerProperty(player, "dead", false) player._splayer:setDied(false) self:setPlayerProperty(player, "dying", false) @@ -3097,7 +3361,10 @@ function Room:revivePlayer(player, sendLog) sendLog = (sendLog == nil) and true or sendLog if sendLog then - self:sendLog { type = "#Revive", from = player.id } + self:sendLog { + type = "#Revive", + from = player.id + } end end @@ -3110,7 +3377,9 @@ local function shouldUpdateWinRate(room) return false end for _, p in ipairs(room.players) do - if p.id < 0 then return false end + if p.id < 0 then + return false + end end return Fk.game_modes[room.settings.gameMode]:countInFunc(room) end @@ -3118,7 +3387,9 @@ end --- 结束一局游戏。 ---@param winner string @ 获胜的身份,空字符串表示平局 function Room:gameOver(winner) - if not self.game_started then return end + if not self.game_started then + return + end self.logic:trigger(fk.GameFinished, nil, winner) self.game_started = false @@ -3149,10 +3420,7 @@ function Room:gameOver(winner) self.room:gameOver() - if table.contains( - { "running", "normal" }, - coroutine.status(self.main_co) - ) then + if table.contains({ "running", "normal" }, coroutine.status(self.main_co)) then coroutine.yield("__handleRequest", "over") else coroutine.close(self.main_co) @@ -3251,16 +3519,23 @@ function Room:canMoveCardInBoard(flag, players, excludeIds) excludeIds = type(excludeIds) == "table" and excludeIds or {} local targets = {} - table.find(players, function(p) - local canMoveTo = table.find(players, function(another) - return p ~= another and p:canMoveCardsInBoardTo(another, flag, excludeIds) - end) + table.find( + players, + function(p) + local canMoveTo = + table.find( + players, + function(another) + return p ~= another and p:canMoveCardsInBoardTo(another, flag, excludeIds) + end + ) - if canMoveTo then - targets = {p.id, canMoveTo.id} + if canMoveTo then + targets = { p.id, canMoveTo.id } + end + return canMoveTo end - return canMoveTo - end) + ) return targets end @@ -3275,7 +3550,7 @@ function Room:printCard(name, suit, number) Fk:_addPrintedCard(cd) table.insert(self.void, cd.id) self:setCardArea(cd.id, Card.Void, nil) - self:doBroadcastNotify("PrintCard", json.encode{ name, suit, number }) + self:doBroadcastNotify("PrintCard", json.encode { name, suit, number }) return cd end @@ -3285,11 +3560,7 @@ function Room:updateQuestSkillState(player, skillName, failed) self:setPlayerMark(player, MarkEnum.QuestSkillPreName .. skillName, failed and "failed" or "succeed") local updateValue = failed and 2 or 1 - self:doBroadcastNotify("UpdateQuestSkillUI", json.encode{ - player.id, - skillName, - updateValue, - }) + self:doBroadcastNotify("UpdateQuestSkillUI", json.encode { player.id, skillName, updateValue }) end function Room:abortPlayerArea(player, playerSlots) @@ -3331,17 +3602,25 @@ function Room:abortPlayerArea(player, playerSlots) return end - self:moveCards({ - ids = cardsToDrop, - from = player.id, - toArea = Card.DiscardPile, - moveReason = fk.ReasonPutIntoDiscardPile, - }) + self:moveCards( + { + ids = cardsToDrop, + from = player.id, + toArea = Card.DiscardPile, + moveReason = fk.ReasonPutIntoDiscardPile + } + ) table.insertTable(player.sealedSlots, slotsToSeal) self:broadcastProperty(player, "sealedSlots") - self.logic:trigger(fk.AreaAborted, player, { slots = slotsSealed }) + self.logic:trigger( + fk.AreaAborted, + player, + { + slots = slotsSealed + } + ) end function Room:resumePlayerArea(player, playerSlots) @@ -3363,7 +3642,13 @@ function Room:resumePlayerArea(player, playerSlots) if #slotsToResume > 0 then self:broadcastProperty(player, "sealedSlots") - self.logic:trigger(fk.AreaResumed, player, { slots = slotsToResume }) + self.logic:trigger( + fk.AreaResumed, + player, + { + slots = slotsToResume + } + ) end end diff --git a/lua/server/room.lua.rej b/lua/server/room.lua.rej new file mode 100644 index 00000000..db77f6ab --- /dev/null +++ b/lua/server/room.lua.rej @@ -0,0 +1,46 @@ +diff a/lua/server/room.lua b/lua/server/room.lua (rejected hunks) +@@ -1482,41 +1482,18 @@ + local result = self:doRequest(chooser, command, json.encode(data)) + + if result == "" then +- local areas = {} +- local handcards ++ local handcards = {} + if type(flag) == "string" then +- if string.find(flag, "h") then +- table.insert(areas, Player.Hand) +- end +- if string.find(flag, "e") then +- table.insert(areas, Player.Equip) +- end +- if string.find(flag, "j") then +- table.insert(areas, Player.Judge) +- end +- handcards = target:getCardIds(areas) ++ handcards = target:getCardIds(flag) + else +- handcards = {} + for _, t in ipairs(flag.card_data) do + table.insertTable(handcards, t[2]) + end + end +- if #handcards == 0 then +- return +- end +- result = handcards[math.random(1, #handcards)] ++ result = handcards[math.random(1, #handcards)] or -1 + else + result = tonumber(result) + end +- +- if result == -1 then +- local handcards = target:getCardIds(Player.Hand) +- if #handcards == 0 then +- return +- end +- result = table.random(handcards) +- end +- + return result + end + diff --git a/lua/server/serverplayer.lua b/lua/server/serverplayer.lua index 4f253350..26dab31b 100644 --- a/lua/server/serverplayer.lua +++ b/lua/server/serverplayer.lua @@ -45,7 +45,7 @@ function ServerPlayer:initialize(_self) self._prelighted_skills = {} self._timewaste_count = 0 - self.ai = RandomAI:new(self) + self.ai = TrustAI:new(self) end ---@param command string @@ -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:prependExitFunc(function() self:gainAnExtraPhase(phase, false) end) + turn:addExitFunc(function() self:gainAnExtraPhase(phase, false) end) return end end @@ -484,6 +484,7 @@ function ServerPlayer:gainAnExtraPhase(phase, delay) arg = phase_name_table[phase], } + GameEvent(GameEvent.Phase, self, self.phase):exec() self.phase = current @@ -579,7 +580,6 @@ 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:prependExitFunc(function() self:gainAnExtraTurn(false) end) + turn:addExitFunc(function() self:gainAnExtraTurn(false) end) return end end @@ -604,32 +604,10 @@ 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 @@ -1013,4 +991,8 @@ function ServerPlayer:removeBuddy(other) self:doNotify("RmBuddy", tostring(other.id)) end +function ServerPlayer:getAI() + return self.ai +end + return ServerPlayer diff --git a/packages/maneuvering/init.lua b/packages/maneuvering/init.lua index 1792f34f..8c2d993d 100644 --- a/packages/maneuvering/init.lua +++ b/packages/maneuvering/init.lua @@ -1,481 +1,448 @@ -- SPDX-License-Identifier: GPL-3.0-or-later - local extension = Package:new("maneuvering", Package.CardPack) local slash = Fk:cloneCard("slash") -local thunderSlashSkill = fk.CreateActiveSkill{ - name = "thunder__slash_skill", - max_phase_use_time = 1, - target_num = 1, - can_use = slash.skill.canUse, - mod_target_filter = slash.skill.modTargetFilter, - target_filter = slash.skill.targetFilter, - on_effect = function(self, room, effect) - local to = effect.to - local from = effect.from +local thunderSlashSkill = fk.CreateActiveSkill { + name = "thunder__slash_skill", + max_phase_use_time = 1, + target_num = 1, + can_use = slash.skill.canUse, + mod_target_filter = slash.skill.modTargetFilter, + target_filter = slash.skill.targetFilter, + on_effect = function(self, room, effect) + local to = effect.to + local from = effect.from - room:damage({ - from = room:getPlayerById(from), - to = room:getPlayerById(to), - card = effect.card, - damage = 1, - damageType = fk.ThunderDamage, - skillName = self.name - }) - end -} -local thunderSlash = fk.CreateBasicCard{ - name = "thunder__slash", - skill = thunderSlashSkill, - is_damage_card = true, -} - -extension:addCards{ - thunderSlash:clone(Card.Club, 5), - thunderSlash:clone(Card.Club, 6), - thunderSlash:clone(Card.Club, 7), - thunderSlash:clone(Card.Club, 8), - thunderSlash:clone(Card.Spade, 4), - thunderSlash:clone(Card.Spade, 5), - thunderSlash:clone(Card.Spade, 6), - thunderSlash:clone(Card.Spade, 7), - thunderSlash:clone(Card.Spade, 8), -} - -local fireSlashSkill = fk.CreateActiveSkill{ - name = "fire__slash_skill", - max_phase_use_time = 1, - target_num = 1, - can_use = slash.skill.canUse, - mod_target_filter = slash.skill.modTargetFilter, - target_filter = slash.skill.targetFilter, - on_effect = function(self, room, effect) - local to = effect.to - local from = effect.from - - room:damage({ - from = room:getPlayerById(from), - to = room:getPlayerById(to), - card = effect.card, - damage = 1, - damageType = fk.FireDamage, - skillName = self.name - }) - end -} -local fireSlash = fk.CreateBasicCard{ - name = "fire__slash", - skill = fireSlashSkill, - is_damage_card = true, -} - -extension:addCards{ - fireSlash:clone(Card.Heart, 4), - fireSlash:clone(Card.Heart, 7), - fireSlash:clone(Card.Heart, 10), - fireSlash:clone(Card.Diamond, 4), - fireSlash:clone(Card.Diamond, 5), -} - -local analepticSkill = fk.CreateActiveSkill{ - name = "analeptic_skill", - max_turn_use_time = 1, - mod_target_filter = function(self, to_select, selected, user, card, distance_limited) - return self:withinTimesLimit(Fk:currentRoom():getPlayerById(to_select), Player.HistoryTurn, card, "analeptic", Fk:currentRoom():getPlayerById(to_select)) and - not table.find(Fk:currentRoom().alive_players, function(p) - return p.dying - end) - end, - can_use = function(self, player, card) - return self:withinTimesLimit(player, Player.HistoryTurn, card, "analeptic", player) - end, - on_use = function(self, room, use) - if not use.tos or #TargetGroup:getRealTargets(use.tos) == 0 then - use.tos = { { use.from } } + room:damage({ + from = room:getPlayerById(from), + to = room:getPlayerById(to), + card = effect.card, + damage = 1, + damageType = fk.ThunderDamage, + skillName = self.name + }) end - - if use.extra_data and use.extra_data.analepticRecover then - use.extraUse = true - end - end, - on_effect = function(self, room, effect) - local to = room:getPlayerById(effect.to) - if effect.extra_data and effect.extra_data.analepticRecover then - room:recover({ - who = to, - num = 1, - recoverBy = room:getPlayerById(effect.from), - card = effect.card, - }) - else - to.drank = to.drank + 1 - room:broadcastProperty(to, "drank") - end - end +} +local thunderSlash = fk.CreateBasicCard { + name = "thunder__slash", + skill = thunderSlashSkill, + is_damage_card = true } -local analepticEffect = fk.CreateTriggerSkill{ - name = "analeptic_effect", - global = true, - priority = 0, -- game rule - events = { fk.PreCardUse, fk.EventPhaseStart }, - can_trigger = function(self, event, target, player, data) - if target ~= player then - return false - end +extension:addCards{thunderSlash:clone(Card.Club, 5), thunderSlash:clone(Card.Club, 6), thunderSlash:clone(Card.Club, 7), + thunderSlash:clone(Card.Club, 8), thunderSlash:clone(Card.Spade, 4), + thunderSlash:clone(Card.Spade, 5), thunderSlash:clone(Card.Spade, 6), + thunderSlash:clone(Card.Spade, 7), thunderSlash:clone(Card.Spade, 8)} - if event == fk.PreCardUse then - return data.card.trueName == "slash" and player.drank > 0 - else - return target.phase == Player.NotActive +local fireSlashSkill = fk.CreateActiveSkill { + name = "fire__slash_skill", + max_phase_use_time = 1, + target_num = 1, + can_use = slash.skill.canUse, + mod_target_filter = slash.skill.modTargetFilter, + target_filter = slash.skill.targetFilter, + on_effect = function(self, room, effect) + local to = effect.to + local from = effect.from + + room:damage({ + from = room:getPlayerById(from), + to = room:getPlayerById(to), + card = effect.card, + damage = 1, + damageType = fk.FireDamage, + skillName = self.name + }) end - end, - on_trigger = function(self, event, target, player, data) - local room = player.room - if event == fk.PreCardUse then - data.additionalDamage = (data.additionalDamage or 0) + player.drank - data.extra_data = data.extra_data or {} - data.extra_data.drankBuff = player.drank - player.drank = 0 - room:broadcastProperty(player, "drank") - else - for _, p in ipairs(room:getAlivePlayers(true)) do - if p.drank > 0 then - p.drank = 0 - room:broadcastProperty(p, "drank") +} +local fireSlash = fk.CreateBasicCard { + name = "fire__slash", + skill = fireSlashSkill, + is_damage_card = true +} + +extension:addCards{fireSlash:clone(Card.Heart, 4), fireSlash:clone(Card.Heart, 7), fireSlash:clone(Card.Heart, 10), + fireSlash:clone(Card.Diamond, 4), fireSlash:clone(Card.Diamond, 5)} + +local analepticSkill = fk.CreateActiveSkill { + name = "analeptic_skill", + max_turn_use_time = 1, + mod_target_filter = function(self, to_select, selected, user, card, distance_limited) + local to = Fk:currentRoom():getPlayerById(to_select) + local from = Fk:currentRoom():getPlayerById(user) + return self:withinTimesLimit(from, Player.HistoryTurn, card, "analeptic", to) and + not table.find(Fk:currentRoom().alive_players, function(p) + return p.dying + end) and not (card and from:isProhibited(to, card)) + end, + can_use = function(self, player, card) + return self:withinTimesLimit(player, Player.HistoryTurn, card, "analeptic", player) and + not player:isProhibited(player, card) + end, + + on_use = function(self, room, use) + if not use.tos or #TargetGroup:getRealTargets(use.tos) == 0 then + use.tos = {{use.from}} + end + + if use.extra_data and use.extra_data.analepticRecover then + use.extraUse = true + end + end, + on_effect = function(self, room, effect) + local to = room:getPlayerById(effect.to) + if effect.extra_data and effect.extra_data.analepticRecover then + room:recover({ + who = to, + num = 1, + recoverBy = room:getPlayerById(effect.from), + card = effect.card + }) + else + to.drank = to.drank + 1 + room:broadcastProperty(to, "drank") + end + end +} + +local analepticEffect = fk.CreateTriggerSkill { + name = "analeptic_effect", + global = true, + priority = 0, -- game rule + events = {fk.PreCardUse, fk.EventPhaseStart}, + can_trigger = function(self, event, target, player, data) + if target ~= player then + return false + end + if event == fk.PreCardUse then + return data.card.trueName == "slash" and player.drank > 0 + else + return target.phase == Player.NotActive + end + end, + on_trigger = function(self, event, target, player, data) + local room = player.room + if event == fk.PreCardUse then + data.additionalDamage = (data.additionalDamage or 0) + player.drank + data.extra_data = data.extra_data or {} + data.extra_data.drankBuff = player.drank + player.drank = 0 + room:broadcastProperty(player, "drank") + else + for _, p in ipairs(room:getAlivePlayers(true)) do + if p.drank > 0 then + p.drank = 0 + room:broadcastProperty(p, "drank") + end + end end - end end - end, } Fk:addSkill(analepticEffect) -local analeptic = fk.CreateBasicCard{ - name = "analeptic", - suit = Card.Spade, - number = 3, - skill = analepticSkill, +local analeptic = fk.CreateBasicCard { + name = "analeptic", + suit = Card.Spade, + number = 3, + skill = analepticSkill } -extension:addCards({ - analeptic, - analeptic:clone(Card.Spade, 9), - analeptic:clone(Card.Club, 3), - analeptic:clone(Card.Club, 9), - analeptic:clone(Card.Diamond, 9), -}) +extension:addCards({analeptic, analeptic:clone(Card.Spade, 9), analeptic:clone(Card.Club, 3), + analeptic:clone(Card.Club, 9), analeptic:clone(Card.Diamond, 9)}) -local recast = fk.CreateActiveSkill{ - name = "recast", - target_num = 0, - on_use = function(self, room, effect) - room:recastCard(effect.cards, room:getPlayerById(effect.from)) - end +local recast = fk.CreateActiveSkill { + name = "recast", + target_num = 0, + on_use = function(self, room, effect) + room:recastCard(effect.cards, room:getPlayerById(effect.from)) + end } Fk:addSkill(recast) -local ironChainCardSkill = fk.CreateActiveSkill{ - name = "iron_chain_skill", - min_target_num = 1, - max_target_num = 2, - mod_target_filter = function(self, to_select, selected, user, card, distance_limited) - return true - end, - target_filter = function() return true end, - on_effect = function(self, room, cardEffectEvent) - local to = room:getPlayerById(cardEffectEvent.to) - to:setChainState(not to.chained) - end, -} - -local ironChain = fk.CreateTrickCard{ - name = "iron_chain", - skill = ironChainCardSkill, - special_skills = { "recast" }, - multiple_targets = true, -} -extension:addCards{ - ironChain:clone(Card.Spade, 11), - ironChain:clone(Card.Spade, 12), - ironChain:clone(Card.Club, 10), - ironChain:clone(Card.Club, 11), - ironChain:clone(Card.Club, 12), - ironChain:clone(Card.Club, 13), -} - -local fireAttackSkill = fk.CreateActiveSkill{ - name = "fire_attack_skill", - target_num = 1, - mod_target_filter = function(self, to_select, selected, user, card, distance_limited) - return not Fk:currentRoom():getPlayerById(to_select):isKongcheng() - end, - target_filter = function(self, to_select) - return self:modTargetFilter(to_select) - end, - on_effect = function(self, room, cardEffectEvent) - local from = room:getPlayerById(cardEffectEvent.from) - local to = room:getPlayerById(cardEffectEvent.to) - if to:isKongcheng() then return end - - local showCard = room:askForCard(to, 1, 1, false, self.name, false, ".|.|.|hand", "#fire_attack-show:" .. from.id)[1] - to:showCards(showCard) - - showCard = Fk:getCardById(showCard) - local cards = room:askForDiscard(from, 1, 1, false, self.name, true, - ".|.|" .. showCard:getSuitString(), "#fire_attack-discard:" .. to.id .. "::" .. showCard:getSuitString()) - if #cards > 0 then - room:damage({ - from = from, - to = to, - card = cardEffectEvent.card, - damage = 1, - damageType = fk.FireDamage, - skillName = self.name - }) +local ironChainCardSkill = fk.CreateActiveSkill { + name = "iron_chain_skill", + min_target_num = 1, + max_target_num = 2, + mod_target_filter = function(self, to_select, selected, user, card, distance_limited) + local to = Fk:currentRoom():getPlayerById(to_select) + local from = Fk:currentRoom():getPlayerById(user) + return not (card and from:isProhibited(to, card)) + end, + target_filter = function(self, to_select, selected, _, card) + if #selected < self:getMaxTargetNum(Self, card) then + return self:modTargetFilter(to_select, selected, Self.id, card) + end + end, + on_effect = function(self, room, cardEffectEvent) + local to = room:getPlayerById(cardEffectEvent.to) + to:setChainState(not to.chained) end - end, -} -local fireAttack = fk.CreateTrickCard{ - name = "fire_attack", - skill = fireAttackSkill, - is_damage_card = true, -} -extension:addCards{ - fireAttack:clone(Card.Heart, 2), - fireAttack:clone(Card.Heart, 3), - fireAttack:clone(Card.Diamond, 12), } -local supplyShortageSkill = fk.CreateActiveSkill{ - name = "supply_shortage_skill", - distance_limit = 1, - mod_target_filter = function(self, to_select, selected, user, card, distance_limited) - local player = Fk:currentRoom():getPlayerById(to_select) - local from = Fk:currentRoom():getPlayerById(user) - return from ~= player and not (distance_limited and not self:withinDistanceLimit(from, false, card, player)) - end, - target_filter = function(self, to_select, selected, _, card) - return #selected == 0 and self:modTargetFilter(to_select, selected, Self.id, card, true) - end, - target_num = 1, - on_effect = function(self, room, effect) - local to = room:getPlayerById(effect.to) - local judge = { - who = to, - reason = "supply_shortage", - pattern = ".|.|spade,heart,diamond", - } - room:judge(judge) - local result = judge.card - if result.suit ~= Card.Club then - to:skip(Player.Draw) +local ironChain = fk.CreateTrickCard { + name = "iron_chain", + skill = ironChainCardSkill, + special_skills = {"recast"}, + multiple_targets = true +} +extension:addCards{ironChain:clone(Card.Spade, 11), ironChain:clone(Card.Spade, 12), ironChain:clone(Card.Club, 10), + ironChain:clone(Card.Club, 11), ironChain:clone(Card.Club, 12), ironChain:clone(Card.Club, 13)} + +local fireAttackSkill = fk.CreateActiveSkill { + name = "fire_attack_skill", + target_num = 1, + mod_target_filter = function(self, to_select, selected, user, card, distance_limited) + local to = Fk:currentRoom():getPlayerById(to_select) + local from = Fk:currentRoom():getPlayerById(user) + return not (card and from:isProhibited(to, card)) and not to:isKongcheng() + end, + target_filter = function(self, to_select, selected, _, card) + if #selected < self:getMaxTargetNum(Self, card) then + return self:modTargetFilter(to_select, selected, Self.id, card) + end + end, + on_effect = function(self, room, cardEffectEvent) + local from = room:getPlayerById(cardEffectEvent.from) + local to = room:getPlayerById(cardEffectEvent.to) + if to:isKongcheng() then + return + end + + local showCard = room:askForCard(to, 1, 1, false, self.name, false, ".|.|.|hand", + "#fire_attack-show:" .. from.id)[1] + to:showCards(showCard) + + showCard = Fk:getCardById(showCard) + local cards = room:askForDiscard(from, 1, 1, false, self.name, true, ".|.|" .. showCard:getSuitString(), + "#fire_attack-discard:" .. to.id .. "::" .. showCard:getSuitString()) + if #cards > 0 then + room:damage({ + from = from, + to = to, + card = cardEffectEvent.card, + damage = 1, + damageType = fk.FireDamage, + skillName = self.name + }) + end end - self:onNullified(room, effect) - end, - on_nullified = function(self, room, effect) - room:moveCards{ - ids = room:getSubcardsByRule(effect.card, { Card.Processing }), - toArea = Card.DiscardPile, - moveReason = fk.ReasonUse - } - end, } -local supplyShortage = fk.CreateDelayedTrickCard{ - name = "supply_shortage", - skill = supplyShortageSkill, -} -extension:addCards{ - supplyShortage:clone(Card.Spade, 10), - supplyShortage:clone(Card.Club, 4), +local fireAttack = fk.CreateTrickCard { + name = "fire_attack", + skill = fireAttackSkill, + is_damage_card = true } +extension:addCards{fireAttack:clone(Card.Heart, 2), fireAttack:clone(Card.Heart, 3), fireAttack:clone(Card.Diamond, 12)} -local gudingSkill = fk.CreateTriggerSkill{ - name = "#guding_blade_skill", - attached_equip = "guding_blade", - frequency = Skill.Compulsory, - events = {fk.DamageCaused}, - can_trigger = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name) and - data.to:isKongcheng() and data.card and data.card.trueName == "slash" and - not data.chain - end, - on_use = function(_, _, _, _, data) - data.damage = data.damage + 1 - end, +local supplyShortageSkill = fk.CreateActiveSkill { + name = "supply_shortage_skill", + distance_limit = 1, + mod_target_filter = function(self, to_select, selected, user, card, distance_limited) + local player = Fk:currentRoom():getPlayerById(to_select) + local from = Fk:currentRoom():getPlayerById(user) + return + user ~= to_select and not (distance_limited and not self:withinDistanceLimit(from, false, card, player)) and + not (card and from:isProhibited(player, card)) + end, + target_filter = function(self, to_select, selected, _, card) + return #selected < 1 and self:modTargetFilter(to_select, selected, Self.id, card, true) + end, + target_num = 1, + on_effect = function(self, room, effect) + local to = room:getPlayerById(effect.to) + local judge = { + who = to, + reason = "supply_shortage", + pattern = ".|.|club" + } + room:judge(judge) + if judge.card.suit ~= Card.Club then + to:skip(Player.Draw) + end + self:onNullified(room, effect) + end, + on_nullified = function(self, room, effect) + room:moveCards{ + ids = room:getSubcardsByRule(effect.card, {Card.Processing}), + toArea = Card.DiscardPile, + moveReason = fk.ReasonUse + } + end +} +local supplyShortage = fk.CreateDelayedTrickCard { + name = "supply_shortage", + skill = supplyShortageSkill +} +extension:addCards{supplyShortage:clone(Card.Spade, 10), supplyShortage:clone(Card.Club, 4)} + +local gudingSkill = fk.CreateTriggerSkill { + name = "#guding_blade_skill", + attached_equip = "guding_blade", + frequency = Skill.Compulsory, + events = {fk.DamageCaused}, + can_trigger = function(self, event, target, player, data) + return target == player and player:hasSkill(self.name) and data.to:isKongcheng() and data.card and + data.card.trueName == "slash" and not data.chain + end, + on_use = function(_, _, _, _, data) + data.damage = data.damage + 1 + end } Fk:addSkill(gudingSkill) -local gudingBlade = fk.CreateWeapon{ - name = "guding_blade", - suit = Card.Spade, - number = 1, - attack_range = 2, - equip_skill = gudingSkill, +local gudingBlade = fk.CreateWeapon { + name = "guding_blade", + suit = Card.Spade, + number = 1, + attack_range = 2, + equip_skill = gudingSkill } extension:addCard(gudingBlade) -local fanSkill = fk.CreateTriggerSkill{ - name = "#fan_skill", - attached_equip = "fan", - events = { fk.AfterCardUseDeclared }, - can_trigger = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name) and data.card.name == "slash" - end, - on_use = function(_, _, _, _, data) - local card = Fk:cloneCard("fire__slash") - card.skillName = "fan" - card:addSubcard(data.card) - data.card = card - end, +local fanSkill = fk.CreateTriggerSkill { + name = "#fan_skill", + attached_equip = "fan", + events = {fk.AfterCardUseDeclared}, + can_trigger = function(self, event, target, player, data) + return target == player and player:hasSkill(self.name) and data.card.name == "slash" + end, + on_use = function(_, _, _, _, data) + local card = Fk:cloneCard("fire__slash") + card.skillName = "fan" + card:addSubcard(data.card) + data.card = card + end } Fk:addSkill(fanSkill) -local fan = fk.CreateWeapon{ - name = "fan", - suit = Card.Diamond, - number = 1, - attack_range = 4, - equip_skill = fanSkill, +local fan = fk.CreateWeapon { + name = "fan", + suit = Card.Diamond, + number = 1, + attack_range = 4, + equip_skill = fanSkill } extension:addCard(fan) -local vineSkill = fk.CreateTriggerSkill{ - name = "#vine_skill", - attached_equip = "vine", - mute = true, - frequency = Skill.Compulsory, +local vineSkill = fk.CreateTriggerSkill { + name = "#vine_skill", + attached_equip = "vine", + mute = true, + frequency = Skill.Compulsory, - events = {fk.PreCardEffect, fk.DamageInflicted}, - can_trigger = function(self, event, target, player, data) - if event == fk.DamageInflicted then - return target == player and player:hasSkill(self.name) and - data.damageType == fk.FireDamage + events = {fk.PreCardEffect, fk.DamageInflicted}, + can_trigger = function(self, event, target, player, data) + if event == fk.DamageInflicted then + return target == player and player:hasSkill(self.name) and data.damageType == fk.FireDamage + end + local effect = data ---@type CardEffectEvent + return player.id == effect.to and player:hasSkill(self.name) and + (effect.card.name == "slash" or effect.card.name == "savage_assault" or effect.card.name == + "archery_attack") + end, + on_use = function(self, event, target, player, data) + local room = player.room + if event == fk.DamageInflicted then + room:broadcastPlaySound("./packages/maneuvering/audio/card/vineburn") + room:setEmotion(player, "./packages/maneuvering/image/anim/vineburn") + data.damage = data.damage + 1 + else + room:broadcastPlaySound("./packages/maneuvering/audio/card/vine") + room:setEmotion(player, "./packages/maneuvering/image/anim/vine") + return true + end end - local effect = data ---@type CardEffectEvent - return player.id == effect.to and player:hasSkill(self.name) and - (effect.card.name == "slash" or effect.card.name == "savage_assault" or - effect.card.name == "archery_attack") - end, - on_use = function(self, event, target, player, data) - local room = player.room - if event == fk.DamageInflicted then - room:broadcastPlaySound("./packages/maneuvering/audio/card/vineburn") - room:setEmotion(player, "./packages/maneuvering/image/anim/vineburn") - data.damage = data.damage + 1 - else - room:broadcastPlaySound("./packages/maneuvering/audio/card/vine") - room:setEmotion(player, "./packages/maneuvering/image/anim/vine") - return true - end - end, } Fk:addSkill(vineSkill) -local vine = fk.CreateArmor{ - name = "vine", - equip_skill = vineSkill, -} -extension:addCards{ - vine:clone(Card.Spade, 2), - vine:clone(Card.Club, 2), +local vine = fk.CreateArmor { + name = "vine", + equip_skill = vineSkill } +extension:addCards{vine:clone(Card.Spade, 2), vine:clone(Card.Club, 2)} -local silverLionSkill = fk.CreateTriggerSkill{ - name = "#silver_lion_skill", - attached_equip = "silver_lion", - frequency = Skill.Compulsory, - events = {fk.DamageInflicted}, - can_trigger = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name) and data.damage > 1 - end, - on_use = function(_, _, _, _, data) - data.damage = 1 - end, +local silverLionSkill = fk.CreateTriggerSkill { + name = "#silver_lion_skill", + attached_equip = "silver_lion", + frequency = Skill.Compulsory, + events = {fk.DamageInflicted}, + can_trigger = function(self, event, target, player, data) + return target == player and player:hasSkill(self.name) and data.damage > 1 + end, + on_use = function(_, _, _, _, data) + data.damage = 1 + end } Fk:addSkill(silverLionSkill) -local silverLion = fk.CreateArmor{ - name = "silver_lion", - suit = Card.Club, - number = 1, - equip_skill = silverLionSkill, - on_uninstall = function(self, room, player) - Armor.onUninstall(self, room, player) - if player:isAlive() and player:isWounded() and self.equip_skill:isEffectable(player) then - room:broadcastPlaySound("./packages/maneuvering/audio/card/silver_lion") - room:setEmotion(player, "./packages/maneuvering/image/anim/silver_lion") - room:recover{ - who = player, - num = 1, - skillName = self.name - } +local silverLion = fk.CreateArmor { + name = "silver_lion", + suit = Card.Club, + number = 1, + equip_skill = silverLionSkill, + on_uninstall = function(self, room, player) + Armor.onUninstall(self, room, player) + if player:isAlive() and player:isWounded() and self.equip_skill:isEffectable(player) then + room:broadcastPlaySound("./packages/maneuvering/audio/card/silver_lion") + room:setEmotion(player, "./packages/maneuvering/image/anim/silver_lion") + room:recover{ + who = player, + num = 1, + skillName = self.name + } + end end - end, } extension:addCard(silverLion) -local huaLiu = fk.CreateDefensiveRide{ - name = "hualiu", - suit = Card.Diamond, - number = 13, +local huaLiu = fk.CreateDefensiveRide { + name = "hualiu", + suit = Card.Diamond, + number = 13 } -extension:addCards({ - huaLiu, -}) +extension:addCards({huaLiu}) -extension:addCards{ - Fk:cloneCard("jink", Card.Heart, 8), - Fk:cloneCard("jink", Card.Heart, 9), - Fk:cloneCard("jink", Card.Heart, 11), - Fk:cloneCard("jink", Card.Heart, 12), - Fk:cloneCard("jink", Card.Diamond, 6), - Fk:cloneCard("jink", Card.Diamond, 7), - Fk:cloneCard("jink", Card.Diamond, 8), - Fk:cloneCard("jink", Card.Diamond, 10), - Fk:cloneCard("jink", Card.Diamond, 11), - - Fk:cloneCard("peach", Card.Heart, 5), - Fk:cloneCard("peach", Card.Heart, 6), - Fk:cloneCard("peach", Card.Diamond, 2), - Fk:cloneCard("peach", Card.Diamond, 3), - - Fk:cloneCard("nullification", Card.Heart, 1), - Fk:cloneCard("nullification", Card.Heart, 13), - Fk:cloneCard("nullification", Card.Spade, 13), -} +extension:addCards{Fk:cloneCard("jink", Card.Heart, 8), Fk:cloneCard("jink", Card.Heart, 9), + Fk:cloneCard("jink", Card.Heart, 11), Fk:cloneCard("jink", Card.Heart, 12), + Fk:cloneCard("jink", Card.Diamond, 6), Fk:cloneCard("jink", Card.Diamond, 7), + Fk:cloneCard("jink", Card.Diamond, 8), Fk:cloneCard("jink", Card.Diamond, 10), + Fk:cloneCard("jink", Card.Diamond, 11), Fk:cloneCard("peach", Card.Heart, 5), + Fk:cloneCard("peach", Card.Heart, 6), Fk:cloneCard("peach", Card.Diamond, 2), + Fk:cloneCard("peach", Card.Diamond, 3), Fk:cloneCard("nullification", Card.Heart, 1), + Fk:cloneCard("nullification", Card.Heart, 13), Fk:cloneCard("nullification", Card.Spade, 13)} Fk:loadTranslationTable{ - ["maneuvering"] = "军争", + ["maneuvering"] = "军争", - ["thunder__slash"] = "雷杀", - [":thunder__slash"] = "基本牌
时机:出牌阶段
目标:攻击范围内的一名角色
效果:对目标角色造成1点雷电伤害。", - ["fire__slash"] = "火杀", - [":fire__slash"] = "基本牌
时机:出牌阶段
目标:攻击范围内的一名角色
效果:对目标角色造成1点火焰伤害。", - ["analeptic"] = "酒", - [":analeptic"] = "基本牌
时机:出牌阶段/你处于濒死状态时
目标:你
效果:目标角色本回合使用的下一张【杀】将要造成的伤害+1/目标角色回复1点体力。", - ["iron_chain"] = "铁锁连环", - [":iron_chain"] = "锦囊牌
时机:出牌阶段
目标:一至两名角色
效果:横置或重置目标角色的武将牌。", - ["_normal_use"] = "正常使用", - ["recast"] = "重铸", - [":recast"] = "你可以将此牌置入弃牌堆,然后摸一张牌。", - ["fire_attack"] = "火攻", - ["fire_attack_skill"] = "火攻", - [":fire_attack"] = "锦囊牌
时机:出牌阶段
目标:一名有手牌的角色
效果:目标角色展示一张手牌,然后你可以弃置一张与所展示牌花色相同的手牌令其受到1点火焰伤害。", - ["#fire_attack-show"] = "%src 对你使用了火攻,请展示一张手牌", - ["#fire_attack-discard"] = "你可弃置一张 %arg 手牌,对 %src 造成1点火属性伤害", - ["supply_shortage"] = "兵粮寸断", - [":supply_shortage"] = "延时锦囊牌
时机:出牌阶段
目标:距离1的一名其他角色
效果:将此牌置于目标角色判定区内。其判定阶段进行判定:若结果不为梅花,其跳过摸牌阶段。然后将【兵粮寸断】置入弃牌堆。", - ["guding_blade"] = "古锭刀", - [":guding_blade"] = "装备牌·武器
攻击范围:2
武器技能:锁定技。每当你使用【杀】对目标角色造成伤害时,若该角色没有手牌,此伤害+1。", - ["fan"] = "朱雀羽扇", - [":fan"] = "装备牌·武器
攻击范围:4
武器技能:你可以将一张普通【杀】当火【杀】使用。", - ["#fan_skill"] = "朱雀羽扇", - ["vine"] = "藤甲", - [":vine"] = "装备牌·防具
防具技能:锁定技。【南蛮入侵】、【万箭齐发】和普通【杀】对你无效。每当你受到火焰伤害时,此伤害+1。", - ["silver_lion"] = "白银狮子", - [":silver_lion"] = "装备牌·防具
防具技能:锁定技。每当你受到伤害时,若此伤害大于1点,防止多余的伤害。每当你失去装备区里的【白银狮子】后,你回复1点体力。", - ["hualiu"] = "骅骝", - [":hualiu"] = "装备牌·坐骑
坐骑技能:其他角色与你的距离+1。", + ["thunder__slash"] = "雷杀", + [":thunder__slash"] = "基本牌
时机:出牌阶段
目标:攻击范围内的一名角色
效果:对目标角色造成1点雷电伤害。", + ["fire__slash"] = "火杀", + [":fire__slash"] = "基本牌
时机:出牌阶段
目标:攻击范围内的一名角色
效果:对目标角色造成1点火焰伤害。", + ["analeptic"] = "酒", + [":analeptic"] = "基本牌
时机:出牌阶段/你处于濒死状态时
目标:你
效果:目标角色本回合使用的下一张【杀】将要造成的伤害+1/目标角色回复1点体力。", + ["iron_chain"] = "铁锁连环", + [":iron_chain"] = "锦囊牌
时机:出牌阶段
目标:一至两名角色
效果:横置或重置目标角色的武将牌。", + ["_normal_use"] = "正常使用", + ["recast"] = "重铸", + [":recast"] = "你可以将此牌置入弃牌堆,然后摸一张牌。", + ["fire_attack"] = "火攻", + ["fire_attack_skill"] = "火攻", + [":fire_attack"] = "锦囊牌
时机:出牌阶段
目标:一名有手牌的角色
效果:目标角色展示一张手牌,然后你可以弃置一张与所展示牌花色相同的手牌令其受到1点火焰伤害。", + ["#fire_attack-show"] = "%src 对你使用了火攻,请展示一张手牌", + ["#fire_attack-discard"] = "你可弃置一张 %arg 手牌,对 %src 造成1点火属性伤害", + ["supply_shortage"] = "兵粮寸断", + [":supply_shortage"] = "延时锦囊牌
时机:出牌阶段
目标:距离1的一名其他角色
效果:将此牌置于目标角色判定区内。其判定阶段进行判定:若结果不为梅花,其跳过摸牌阶段。然后将【兵粮寸断】置入弃牌堆。", + ["guding_blade"] = "古锭刀", + [":guding_blade"] = "装备牌·武器
攻击范围:2
武器技能:锁定技。每当你使用【杀】对目标角色造成伤害时,若该角色没有手牌,此伤害+1。", + ["fan"] = "朱雀羽扇", + [":fan"] = "装备牌·武器
攻击范围:4
武器技能:你可以将一张普通【杀】当火【杀】使用。", + ["#fan_skill"] = "朱雀羽扇", + ["vine"] = "藤甲", + [":vine"] = "装备牌·防具
防具技能:锁定技。【南蛮入侵】、【万箭齐发】和普通【杀】对你无效。每当你受到火焰伤害时,此伤害+1。", + ["silver_lion"] = "白银狮子", + [":silver_lion"] = "装备牌·防具
防具技能:锁定技。每当你受到伤害时,若此伤害大于1点,防止多余的伤害。每当你失去装备区里的【白银狮子】后,你回复1点体力。", + ["hualiu"] = "骅骝", + [":hualiu"] = "装备牌·坐骑
坐骑技能:其他角色与你的距离+1。" } return extension diff --git a/packages/maneuvering/maneuvering_ai.lua b/packages/maneuvering/maneuvering_ai.lua new file mode 100644 index 00000000..ce89f11d --- /dev/null +++ b/packages/maneuvering/maneuvering_ai.lua @@ -0,0 +1,140 @@ +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 -- 使用优先值 +} + +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_dis_card.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:isEnemie(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_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_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/game_rule.lua b/packages/standard/game_rule.lua index ca4a826a..006f8552 100644 --- a/packages/standard/game_rule.lua +++ b/packages/standard/game_rule.lua @@ -10,7 +10,7 @@ local function rewardAndPunish(killer, victim) end end -GameRule = fk.CreateTriggerSkill{ +GameRule = fk.CreateTriggerSkill { name = "game_rule", events = { fk.GamePrepared, @@ -37,71 +37,96 @@ GameRule = fk.CreateTriggerSkill{ end switch(event, { - [fk.AskForPeaches] = function() - local dyingPlayer = room:getPlayerById(data.who) - while dyingPlayer.hp < 1 do - local cardNames = {"peach"} - local prompt = "#AskForPeaches:" .. dyingPlayer.id .. "::" .. tostring(1 - dyingPlayer.hp) - if player == dyingPlayer then - table.insert(cardNames, "analeptic") - prompt = "#AskForPeachesSelf:::" .. tostring(1 - dyingPlayer.hp) - end + [fk.AskForPeaches] = function() + local dyingPlayer = room:getPlayerById(data.who) + while dyingPlayer.hp < 1 do + local cardNames = { "peach" } + local prompt = "#AskForPeaches:" .. data.who .. "::" .. tostring(1 - dyingPlayer.hp) + if player == dyingPlayer then + table.insert(cardNames, "analeptic") + prompt = "#AskForPeachesSelf:::" .. tostring(1 - dyingPlayer.hp) + end - cardNames = table.filter(cardNames, function (cardName) - local cardCloned = Fk:cloneCard(cardName) - return not (player:prohibitUse(cardCloned) or player:isProhibited(dyingPlayer, cardCloned)) - end) - if #cardNames == 0 then return end - - local peach_use = room:askForUseCard(player, "peach", table.concat(cardNames, ",") , prompt) - if not peach_use then break end - peach_use.tos = { {dyingPlayer.id} } - if peach_use.card.trueName == "analeptic" then - peach_use.extra_data = peach_use.extra_data or {} - peach_use.extra_data.analepticRecover = true + cardNames = table.filter(cardNames, function(cardName) + local cardCloned = Fk:cloneCard(cardName) + return not (player:prohibitUse(cardCloned) or player:isProhibited(dyingPlayer, cardCloned)) + end) + if #cardNames < 1 then return end + room:notifyMoveFocus(player, "peach") + local useData = { + user = player, + cardName = "peach", + pattern = table.concat(cardNames, ","), + extraData = Util.DummyTable + } + room.logic:trigger(fk.AskForCardUse, player, useData) + if type(useData.result) == "table" then + useData = useData.result + useData.tos = { { data.who } } + if useData.card.trueName == "analeptic" then + useData.extra_data = useData.extra_data or {} + useData.extra_data.analepticRecover = true + end + room:useCard(useData) + end + useData = { "peach", table.concat(cardNames, ","), prompt, true, Util.DummyTable } + while dyingPlayer.hp < 1 do + Fk.currentResponsePattern = table.concat(cardNames, ",") + local result = room:doRequest(player, "AskForUseCard", json.encode(useData)) + Fk.currentResponsePattern = nil + if result ~= "" then + result = room:handleUseCardReply(player, result) + result.tos = { { data.who } } + if result.card.trueName == "analeptic" then + result.extra_data = result.extra_data or {} + result.extra_data.analepticRecover = true + end + room:useCard(result) + else + return + end + end end - room:useCard(peach_use) - end - end, - [fk.AskForPeachesDone] = function() - if player.hp < 1 and not data.ignoreDeath then - ---@type DeathStruct - local deathData = { - who = player.id, - damage = data.damage, - } - room:killPlayer(deathData) - end - end, - [fk.GameOverJudge] = function() - local winner = Fk.game_modes[room.settings.gameMode]:getWinner(player) - if winner ~= "" then - room:gameOver(winner) - return true - end - end, - [fk.BuryVictim] = function() - player:bury() - if room.tag["SkipNormalDeathProcess"] then - return false - end - local damage = data.damage - if damage and damage.from then - local killer = damage.from - rewardAndPunish(killer, player); - end - end, - default = function() - print("game_rule: Event=" .. event) - room:askForSkillInvoke(player, "rule") - end, + end, + [fk.AskForPeachesDone] = function() + if player.hp < 1 and not data.ignoreDeath then + ---@type DeathStruct + local deathData = { + who = player.id, + damage = data.damage, + } + room:killPlayer(deathData) + end + end, + [fk.GameOverJudge] = function() + local winner = Fk.game_modes[room.settings.gameMode]:getWinner(player) + if winner ~= "" then + room:gameOver(winner) + return true + end + end, + [fk.BuryVictim] = function() + player:bury() + if room.tag["SkipNormalDeathProcess"] then + return false + end + local damage = data.damage + if damage and damage.from then + local killer = damage.from + rewardAndPunish(killer, player); + end + end, + default = function() + print("game_rule: Event=" .. event) + room:askForSkillInvoke(player, "rule") + end, }) return false end, } -local fastchat_m = fk.CreateActiveSkill{ name = "fastchat_m" } -local fastchat_f = fk.CreateActiveSkill{ name = "fastchat_f" } +local fastchat_m = fk.CreateActiveSkill { name = "fastchat_m" } +local fastchat_f = fk.CreateActiveSkill { name = "fastchat_f" } Fk:addSkill(fastchat_m) Fk:addSkill(fastchat_f) diff --git a/packages/standard/init.lua b/packages/standard/init.lua index cf7b4daa..62d9c5a1 100644 --- a/packages/standard/init.lua +++ b/packages/standard/init.lua @@ -1,25 +1,25 @@ -- SPDX-License-Identifier: GPL-3.0-or-later - local extension = Package:new("standard") extension.metadata = require "packages.standard.metadata" dofile "packages/standard/game_rule.lua" dofile "packages/standard/aux_skills.lua" -local jianxiong = fk.CreateTriggerSkill{ +local jianxiong = fk.CreateTriggerSkill { name = "jianxiong", anim_type = "masochism", - events = {fk.Damaged}, + events = { fk.Damaged }, can_trigger = function(self, event, target, player, data) local room = target.room return target == player and player:hasSkill(self.name) and data.card and - table.every(data.card:isVirtual() and data.card.subcards or {data.card.id}, function(id) return room:getCardArea(id) == Card.Processing end) + table.every(data.card:isVirtual() and data.card.subcards or { data.card.id }, function(id) + return room:getCardArea(id) == Card.Processing + end) end, on_use = function(self, event, target, player, data) player.room:obtainCard(player.id, data.card, true, fk.ReasonJustMove) - end, + end } - -local hujia = fk.CreateViewAsSkill{ +local hujia = fk.CreateViewAsSkill { name = "hujia$", anim_type = "defensive", pattern = "jink", @@ -38,14 +38,14 @@ local hujia = fk.CreateViewAsSkill{ return false end, enabled_at_response = function(self, player) - return not table.every(Fk:currentRoom().alive_players, function(p) + return player:getMark("hujia-failed-phase") == 0 and not table.every(Fk:currentRoom().alive_players, function(p) return p == player or p.kingdom ~= "wei" end) - end, + end } -local hujiaResponse = fk.CreateTriggerSkill{ +local hujiaResponse = fk.CreateTriggerSkill { name = "#hujiaResponse", - events = {fk.PreCardUse, fk.PreCardRespond}, + events = { fk.PreCardUse, fk.PreCardRespond }, mute = true, priority = 10, can_trigger = function(self, event, target, player, data) @@ -58,34 +58,34 @@ local hujiaResponse = fk.CreateTriggerSkill{ end, on_use = function(self, event, target, player, data) local room = player.room - for _, p in ipairs(room:getOtherPlayers(player)) do - if p.kingdom == "wei" then - local cardResponded = room:askForResponse(p, "jink", "jink", "#hujia-ask:" .. player.id, true) - if cardResponded then - room:responseCard({ - from = p.id, - card = cardResponded, - skipDrop = true, - }) - - data.card = cardResponded - return false - end + local weis = table.filter(room:getOtherPlayers(player), function(p) + return p.kingdom == "wei" + end) + room:doIndicate(player.id, table.map(weis, function(p) + return p.id + end)) + for _, p in ipairs(weis) do + local cardResponded = room:askForResponse(p, "jink", "jink", "#hujia-ask:" .. player.id, true) + if cardResponded then --[[ + room:responseCard({ + from = p.id, + card = cardResponded, + skipDrop = true + })--]] + data.card = cardResponded.card + return false end end - - if event == fk.PreCardUse and player.phase == Player.Play then - room:setPlayerMark(player, "hujia-failed-phase", 1) - end + room:setPlayerMark(player, "hujia-failed-phase", 1) return true end, - refresh_events = {fk.CardUsing}, + refresh_events = { fk.CardUsing, fk.CardResponding }, can_refresh = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name, true) and player:getMark("hujia-failed-phase") > 0 + return player:hasSkill(self.name, true) and player:getMark("hujia-failed-phase") > 0 end, on_refresh = function(self, event, target, player, data) player.room:setPlayerMark(player, "hujia-failed-phase", 0) - end, + end } hujia:addRelatedSkill(hujiaResponse) @@ -93,31 +93,31 @@ local caocao = General:new(extension, "caocao", "wei", 4) caocao:addSkill(jianxiong) caocao:addSkill(hujia) -local guicai = fk.CreateTriggerSkill{ +local guicai = fk.CreateTriggerSkill { name = "guicai", anim_type = "control", - events = {fk.AskForRetrial}, + events = { fk.AskForRetrial }, can_trigger = function(self, event, target, player, data) return player:hasSkill(self.name) and not player:isKongcheng() end, on_cost = function(self, event, target, player, data) local room = player.room local prompt = "#guicai-ask::" .. target.id - local card = room:askForResponse(player, self.name, ".|.|.|hand", prompt, true) - if card ~= nil then - self.cost_data = card + local Response = room:askForResponse(player, self.name, ".|.|.|hand", prompt, true, nil, nil, true) + if Response then + self.cost_data = Response.card return true end end, on_use = function(self, event, target, player, data) local room = player.room room:retrial(self.cost_data, player, data, self.name) - end, + end } -local fankui = fk.CreateTriggerSkill{ +local fankui = fk.CreateTriggerSkill { name = "fankui", anim_type = "masochism", - events = {fk.Damaged}, + events = { fk.Damaged }, can_trigger = function(self, event, target, player, data) if target == player and player:hasSkill(self.name) and data.from and not data.from.dead then if data.from == player then @@ -130,7 +130,7 @@ local fankui = fk.CreateTriggerSkill{ on_use = function(self, event, target, player, data) local room = player.room local from = data.from - local flag = from == player and "e" or "he" + local flag = from == player and "e" or "he" local card = room:askForCardChosen(player, from, flag, self.name) room:obtainCard(player.id, card, false, fk.ReasonPrey) end @@ -139,51 +139,58 @@ local simayi = General:new(extension, "simayi", "wei", 3) simayi:addSkill(guicai) simayi:addSkill(fankui) -local ganglie = fk.CreateTriggerSkill{ +local ganglie = fk.CreateTriggerSkill { name = "ganglie", anim_type = "masochism", - events = {fk.Damaged}, + events = { fk.Damaged }, can_trigger = function(self, event, target, player, data) return target == player and player:hasSkill(self.name) end, on_use = function(self, event, target, player, data) local room = player.room local from = data.from - if from and not from.dead then room:doIndicate(player.id, {from.id}) end + if from and not from.dead then + room:doIndicate(player.id, { from.id }) + end local judge = { who = player, reason = self.name, - pattern = ".|.|^heart", + pattern = ".|.|^heart" } room:judge(judge) if judge.card.suit ~= Card.Heart and from and not from.dead then local discards = room:askForDiscard(from, 2, 2, false, self.name, true) if #discards == 0 then - room:damage{ + room:damage { from = player, to = from, damage = 1, - skillName = self.name, + skillName = self.name } end end - end, + end } local xiahoudun = General:new(extension, "xiahoudun", "wei", 4) xiahoudun:addSkill(ganglie) -local tuxi = fk.CreateTriggerSkill{ +local tuxi = fk.CreateTriggerSkill { name = "tuxi", anim_type = "control", - events = {fk.EventPhaseStart}, + events = { fk.EventPhaseStart }, can_trigger = function(self, event, target, player, data) return target == player and player:hasSkill(self.name) and player.phase == Player.Draw and - table.find(player.room:getOtherPlayers(player), function(p) return not p:isKongcheng() end) + table.find(player.room:getOtherPlayers(player), function(p) + return not p:isKongcheng() + end) end, on_cost = function(self, event, target, player, data) local room = player.room local targets = table.map(table.filter(room:getOtherPlayers(player), function(p) - return not p:isKongcheng() end), function (p) return p.id end) + return not p:isKongcheng() + end), function(p) + return p.id + end) local result = room:askForChoosePlayers(player, targets, 1, 2, "#tuxi-ask", self.name) if #result > 0 then @@ -194,7 +201,9 @@ local tuxi = fk.CreateTriggerSkill{ on_use = function(self, event, target, player, data) local room = player.room for _, id in ipairs(self.cost_data) do - if player.dead then return end + if player.dead then + return + end local p = room:getPlayerById(id) if not p.dead then local c = room:askForCardChosen(player, p, "h", self.name) @@ -202,29 +211,29 @@ local tuxi = fk.CreateTriggerSkill{ end end return true - end, + end } local zhangliao = General:new(extension, "zhangliao", "wei", 4) zhangliao:addSkill(tuxi) -local luoyi = fk.CreateTriggerSkill{ +local luoyi = fk.CreateTriggerSkill { name = "luoyi", anim_type = "offensive", - events = {fk.DrawNCards}, + events = { fk.DrawNCards }, can_trigger = function(self, event, target, player, data) return target == player and player:hasSkill(self.name) and data.n > 0 end, on_use = function(self, event, target, player, data) data.n = data.n - 1 - end, + end } -local luoyi_trigger = fk.CreateTriggerSkill{ +local luoyi_trigger = fk.CreateTriggerSkill { name = "#luoyi_trigger", mute = true, - events = {fk.DamageCaused}, + events = { fk.DamageCaused }, can_trigger = function(self, event, target, player, data) - return target == player and player:usedSkillTimes("luoyi", Player.HistoryTurn) > 0 and - not data.chain and data.card and (data.card.trueName == "slash" or data.card.name == "duel") + return target == player and player:usedSkillTimes("luoyi", Player.HistoryTurn) > 0 and not data.chain and + data.card and (data.card.trueName == "slash" or data.card.name == "duel") end, on_cost = function(self, event, target, player, data) return true @@ -234,37 +243,39 @@ local luoyi_trigger = fk.CreateTriggerSkill{ player:broadcastSkillInvoke("luoyi") room:notifySkillInvoked(player, "luoyi") data.damage = data.damage + 1 - end, + end } local xuchu = General:new(extension, "xuchu", "wei", 4) luoyi:addRelatedSkill(luoyi_trigger) xuchu:addSkill(luoyi) -local tiandu = fk.CreateTriggerSkill{ +local tiandu = fk.CreateTriggerSkill { name = "tiandu", anim_type = "drawcard", - events = {fk.FinishJudge}, + events = { fk.FinishJudge }, can_trigger = function(self, event, target, player, data) return target == player and player:hasSkill(self.name) and player.room:getCardArea(data.card) == Card.Processing end, on_use = function(self, event, target, player, data) player.room:obtainCard(player.id, data.card, true, fk.ReasonJustMove) - end, + end } -local yiji = fk.CreateTriggerSkill{ +local yiji = fk.CreateTriggerSkill { name = "yiji", anim_type = "masochism", - events = {fk.Damaged}, + events = { fk.Damaged }, on_trigger = function(self, event, target, player, data) self.cancel_cost = false for i = 1, data.damage do - if self.cancel_cost then break end + if self.cancel_cost then + break + end self:doCost(event, target, player, data) end end, on_cost = function(self, event, target, player, data) local room = player.room - if room:askForSkillInvoke(player, self.name, data) then + if room:askForSkillInvoke(player, self.name) then return true end self.cancel_cost = true @@ -275,39 +286,54 @@ local yiji = fk.CreateTriggerSkill{ local fakemove = { toArea = Card.PlayerHand, to = player.id, - moveInfo = table.map(ids, function(id) return {cardId = id, fromArea = Card.Void} end), - moveReason = fk.ReasonJustMove, + moveInfo = table.map(ids, function(id) + return { + cardId = id, + fromArea = Card.Void + } + end), + moveReason = fk.ReasonJustMove } - room:notifyMoveCards({player}, {fakemove}) + room:notifyMoveCards({ player }, { fakemove }) for _, id in ipairs(ids) do room:setCardMark(Fk:getCardById(id), "yiji", 1) end - while table.find(ids, function(id) return Fk:getCardById(id):getMark("yiji") > 0 end) do + player.yiji_ids = ids + while table.find(ids, function(id) + return Fk:getCardById(id):getMark("yiji") > 0 + end) do if not room:askForUseActiveSkill(player, "yiji_active", "#yiji-give", true) then for _, id in ipairs(ids) do room:setCardMark(Fk:getCardById(id), "yiji", 0) end - ids = table.filter(ids, function(id) return room:getCardArea(id) ~= Card.PlayerHand end) + ids = table.filter(ids, function(id) + return room:getCardArea(id) ~= Card.PlayerHand + end) fakemove = { from = player.id, toArea = Card.Void, - moveInfo = table.map(ids, function(id) return {cardId = id, fromArea = Card.PlayerHand} end), - moveReason = fk.ReasonGive, + moveInfo = table.map(ids, function(id) + return { + cardId = id, + fromArea = Card.PlayerHand + } + end), + moveReason = fk.ReasonGive } - room:notifyMoveCards({player}, {fakemove}) + room:notifyMoveCards({ player }, { fakemove }) room:moveCards({ fromArea = Card.Void, ids = ids, to = player.id, toArea = Card.PlayerHand, moveReason = fk.ReasonGive, - skillName = self.name, + skillName = self.name }) end end - end, + end } -local yiji_active = fk.CreateActiveSkill{ +local yiji_active = fk.CreateActiveSkill { name = "yiji_active", mute = true, min_card_num = 1, @@ -321,36 +347,41 @@ local yiji_active = fk.CreateActiveSkill{ on_use = function(self, room, effect) local player = room:getPlayerById(effect.from) local target = room:getPlayerById(effect.tos[1]) - room:doIndicate(player.id, {target.id}) + room:doIndicate(player.id, { target.id }) for _, id in ipairs(effect.cards) do room:setCardMark(Fk:getCardById(id), "yiji", 0) end local fakemove = { from = player.id, toArea = Card.Void, - moveInfo = table.map(effect.cards, function(id) return {cardId = id, fromArea = Card.PlayerHand} end), - moveReason = fk.ReasonGive, + moveInfo = table.map(effect.cards, function(id) + return { + cardId = id, + fromArea = Card.PlayerHand + } + end), + moveReason = fk.ReasonGive } - room:notifyMoveCards({player}, {fakemove}) + room:notifyMoveCards({ player }, { fakemove }) room:moveCards({ fromArea = Card.Void, ids = effect.cards, to = target.id, toArea = Card.PlayerHand, moveReason = fk.ReasonGive, - skillName = self.name, + skillName = self.name }) - end, + end } local guojia = General:new(extension, "guojia", "wei", 3) Fk:addSkill(yiji_active) guojia:addSkill(tiandu) guojia:addSkill(yiji) -local luoshen = fk.CreateTriggerSkill{ +local luoshen = fk.CreateTriggerSkill { name = "luoshen", anim_type = "drawcard", - events = {fk.EventPhaseStart}, + events = { fk.EventPhaseStart }, can_trigger = function(self, event, target, player, data) return target == player and player:hasSkill(self.name) and player.phase == Player.Start end, @@ -360,35 +391,37 @@ local luoshen = fk.CreateTriggerSkill{ local judge = { who = player, reason = self.name, - pattern = ".|.|spade,club", + pattern = ".|.|spade,club" } room:judge(judge) if judge.card.color ~= Card.Black or player.dead or not room:askForSkillInvoke(player, self.name) then break end end - end, + end } -local luoshen_obtain = fk.CreateTriggerSkill{ +local luoshen_obtain = fk.CreateTriggerSkill { name = "#luoshen_obtain", mute = true, frequency = Skill.Compulsory, - events = {fk.FinishJudge}, + events = { fk.FinishJudge }, can_trigger = function(self, event, target, player, data) - return target == player and data.reason == "luoshen" and data.card.color == Card.Black end, + return target == player and data.reason == "luoshen" and data.card.color == Card.Black + end, on_use = function(self, event, target, player, data) player.room:obtainCard(player.id, data.card) - end, + end } luoshen:addRelatedSkill(luoshen_obtain) -local qingguo = fk.CreateViewAsSkill{ +local qingguo = fk.CreateViewAsSkill { name = "qingguo", anim_type = "defensive", pattern = "jink", card_filter = function(self, to_select, selected) - if #selected == 1 then return false end - return Fk:getCardById(to_select).color == Card.Black - and Fk:currentRoom():getCardArea(to_select) == Player.Hand + if #selected == 1 then + return false + end + return Fk:getCardById(to_select).color == Card.Black and Fk:currentRoom():getCardArea(to_select) == Player.Hand end, view_as = function(self, cards) if #cards ~= 1 then @@ -398,13 +431,13 @@ local qingguo = fk.CreateViewAsSkill{ c.skillName = self.name c:addSubcard(cards[1]) return c - end, + end } local zhenji = General:new(extension, "zhenji", "wei", 3, 3, General.Female) zhenji:addSkill(luoshen) zhenji:addSkill(qingguo) -local rende = fk.CreateActiveSkill{ +local rende = fk.CreateActiveSkill { name = "rende", anim_type = "support", card_filter = function(self, to_select, selected) @@ -423,17 +456,17 @@ local rende = fk.CreateActiveSkill{ room:moveCardTo(cards, Player.Hand, target, fk.ReasonGive, self.name, nil, false, player.id) room:addPlayerMark(player, "_rende_cards-phase", #cards) if marks < 2 and marks + #cards >= 2 and player:isWounded() then - room:recover{ + room:recover { who = player, num = 1, recoverBy = player, skillName = self.name } end - end, + end } -local jijiang = fk.CreateViewAsSkill{ +local jijiang = fk.CreateViewAsSkill { name = "jijiang$", anim_type = "offensive", pattern = "slash", @@ -449,59 +482,55 @@ local jijiang = fk.CreateViewAsSkill{ return c end, enabled_at_play = function(self, player) - return player:getMark("jijiang-failed-phase") == 0 and not table.every(Fk:currentRoom().alive_players, function(p) - return p == player or p.kingdom ~= "shu" - end) + return player:getMark("jijiang-failed-phase") == 0 and + not table.every(Fk:currentRoom().alive_players, function(p) + return p == player or p.kingdom ~= "shu" + end) end, enabled_at_response = function(self, player) - return not table.every(Fk:currentRoom().alive_players, function(p) - return p == player or p.kingdom ~= "shu" - end) - end, + return player:getMark("jijiang-failed-phase") == 0 and + not table.every(Fk:currentRoom().alive_players, function(p) + return p == player or p.kingdom ~= "shu" + end) + end } -local jijiangResponse = fk.CreateTriggerSkill{ +local jijiangResponse = fk.CreateTriggerSkill { name = "#jijiangResponse", - events = {fk.PreCardUse, fk.PreCardRespond}, + events = { fk.PreCardUse, fk.PreCardRespond }, mute = true, priority = 10, can_trigger = function(self, event, target, player, data) return target == player and player:hasSkill(self.name, true) and table.contains(data.card.skillNames, "jijiang") end, on_cost = function(self, event, target, player, data) - local room = player.room - room:doIndicate(player.id, TargetGroup:getRealTargets(data.tos)) + player.room:doIndicate(player.id, TargetGroup:getRealTargets(data.tos)) return true end, on_use = function(self, event, target, player, data) local room = player.room - for _, p in ipairs(room:getOtherPlayers(player)) do - if p.kingdom == "shu" then - local cardResponded = room:askForResponse(p, "slash", "slash", "#jijiang-ask:" .. player.id, true) - if cardResponded then - room:responseCard({ - from = p.id, - card = cardResponded, - skipDrop = true, - }) - - data.card = cardResponded - return false - end + local shus = table.filter(room:getOtherPlayers(player), function(p) + return p.kingdom == "shu" + end) + room:doIndicate(player.id, table.map(shus, function(p) + return p.id + end)) + for _, p in ipairs(shus) do + local cardResponded = room:askForResponse(p, "slash", "slash", "#jijiang-ask:" .. player.id, true) + if cardResponded then + data.card = cardResponded.card + return false end end - - if event == fk.PreCardUse and player.phase == Player.Play then - room:setPlayerMark(player, "jijiang-failed-phase", 1) - end + room:setPlayerMark(player, "jijiang-failed-phase", 1) return true end, - refresh_events = {fk.CardUsing}, + refresh_events = { fk.CardUsing, fk.CardResponding }, can_refresh = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name, true) and player:getMark("jijiang-failed-phase") > 0 + return player:hasSkill(self.name, true) and player:getMark("jijiang-failed-phase") > 0 end, on_refresh = function(self, event, target, player, data) player.room:setPlayerMark(player, "jijiang-failed-phase", 0) - end, + end } jijiang:addRelatedSkill(jijiangResponse) @@ -509,12 +538,14 @@ local liubei = General:new(extension, "liubei", "shu", 4) liubei:addSkill(rende) liubei:addSkill(jijiang) -local wusheng = fk.CreateViewAsSkill{ +local wusheng = fk.CreateViewAsSkill { name = "wusheng", anim_type = "offensive", pattern = "slash", card_filter = function(self, to_select, selected) - if #selected == 1 then return false end + if #selected == 1 then + return false + end return Fk:getCardById(to_select).color == Card.Red end, view_as = function(self, cards) @@ -525,62 +556,63 @@ local wusheng = fk.CreateViewAsSkill{ c.skillName = self.name c:addSubcard(cards[1]) return c - end, + end } local guanyu = General:new(extension, "guanyu", "shu", 4) guanyu:addSkill(wusheng) -local paoxiaoAudio = fk.CreateTriggerSkill{ +local paoxiaoAudio = fk.CreateTriggerSkill { name = "#paoxiaoAudio", visible = false, - refresh_events = {fk.CardUsing}, + refresh_events = { fk.CardUsing }, can_refresh = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name) and - data.card.trueName == "slash" and - player:usedCardTimes("slash") > 1 + return target == player and player:hasSkill(self.name) and data.card.trueName == "slash" and + player:usedCardTimes("slash") > 1 end, on_refresh = function(self, event, target, player, data) player:broadcastSkillInvoke("paoxiao") player.room:doAnimate("InvokeSkill", { name = "paoxiao", player = player.id, - skill_type = "offensive", + skill_type = "offensive" }) - end, + end } -local paoxiao = fk.CreateTargetModSkill{ +local paoxiao = fk.CreateTargetModSkill { name = "paoxiao", frequency = Skill.Compulsory, bypass_times = function(self, player, skill, scope) - if player:hasSkill(self.name) and skill.trueName == "slash_skill" - and scope == Player.HistoryPhase then + if player:hasSkill(self.name) and skill.trueName == "slash_skill" and scope == Player.HistoryPhase then return true end - end, + end } paoxiao:addRelatedSkill(paoxiaoAudio) local zhangfei = General:new(extension, "zhangfei", "shu", 4) zhangfei:addSkill(paoxiao) -local guanxing = fk.CreateTriggerSkill{ +local guanxing = fk.CreateTriggerSkill { name = "guanxing", anim_type = "control", - events = {fk.EventPhaseStart}, + events = { fk.EventPhaseStart }, can_trigger = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name) and - player.phase == Player.Start + return target == player and player:hasSkill(self.name) and player.phase == Player.Start end, on_use = function(self, event, target, player, data) local room = player.room room:askForGuanxing(player, room:getNCards(math.min(5, #room.alive_players))) - end, + end } -local kongchengAudio = fk.CreateTriggerSkill{ +local kongchengAudio = fk.CreateTriggerSkill { name = "#kongchengAudio", - refresh_events = {fk.AfterCardsMove}, + refresh_events = { fk.AfterCardsMove }, can_refresh = function(self, event, target, player, data) - if not player:hasSkill(self.name) then return end - if not player:isKongcheng() then return end + if not player:hasSkill(self.name) then + return + end + if not player:isKongcheng() then + return + end for _, move in ipairs(data) do if move.from == player.id then for _, info in ipairs(move.moveInfo) do @@ -594,27 +626,29 @@ local kongchengAudio = fk.CreateTriggerSkill{ on_refresh = function(self, event, target, player, data) player:broadcastSkillInvoke("kongcheng") player.room:notifySkillInvoked(player, "kongcheng", "defensive") - end, + end } -local kongcheng = fk.CreateProhibitSkill{ +local kongcheng = fk.CreateProhibitSkill { name = "kongcheng", frequency = Skill.Compulsory, is_prohibited = function(self, from, to, card) if to:hasSkill(self.name) and to:isKongcheng() then return card.trueName == "slash" or card.trueName == "duel" end - end, + end } kongcheng:addRelatedSkill(kongchengAudio) local zhugeliang = General:new(extension, "zhugeliang", "shu", 3) zhugeliang:addSkill(guanxing) zhugeliang:addSkill(kongcheng) -local longdan = fk.CreateViewAsSkill{ +local longdan = fk.CreateViewAsSkill { name = "longdan", pattern = "slash,jink", card_filter = function(self, to_select, selected) - if #selected == 1 then return false end + if #selected == 1 then + return false + end local _c = Fk:getCardById(to_select) local c if _c.trueName == "slash" then @@ -624,7 +658,8 @@ local longdan = fk.CreateViewAsSkill{ else return false end - return (Fk.currentResponsePattern == nil and Self:canUse(c)) or (Fk.currentResponsePattern and Exppattern:Parse(Fk.currentResponsePattern):match(c)) + return (Fk.currentResponsePattern == nil and Self:canUse(c)) or + (Fk.currentResponsePattern and Exppattern:Parse(Fk.currentResponsePattern):match(c)) end, view_as = function(self, cards) if #cards ~= 1 then @@ -640,69 +675,69 @@ local longdan = fk.CreateViewAsSkill{ c.skillName = self.name c:addSubcard(cards[1]) return c - end, + end } local zhaoyun = General:new(extension, "zhaoyun", "shu", 4) zhaoyun:addSkill(longdan) -local mashu = fk.CreateDistanceSkill{ +local mashu = fk.CreateDistanceSkill { name = "mashu", frequency = Skill.Compulsory, correct_func = function(self, from, to) if from:hasSkill(self.name) then return -1 end - end, + end } -local tieqi = fk.CreateTriggerSkill{ +local tieqi = fk.CreateTriggerSkill { name = "tieqi", anim_type = "offensive", - events = {fk.TargetSpecified}, + events = { fk.TargetSpecified }, can_trigger = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name) and - data.card.trueName == "slash" + player.tieqi_tos = data.tos + return target == player and player:hasSkill(self.name) and data.card.trueName == "slash" end, on_use = function(self, event, target, player, data) local room = player.room local judge = { who = player, reason = self.name, - pattern = ".|.|heart,diamond", + pattern = ".|.|heart,diamond" } room:judge(judge) if judge.card.color == Card.Red then data.disresponsive = true end - end, + end } local machao = General:new(extension, "machao", "shu", 4) machao:addSkill(mashu) machao:addSkill(tieqi) -local jizhi = fk.CreateTriggerSkill{ +local jizhi = fk.CreateTriggerSkill { name = "jizhi", anim_type = "drawcard", - events = {fk.CardUsing}, + events = { fk.CardUsing }, can_trigger = function(self, event, target, player, data) return target == player and player:hasSkill(self.name) and data.card:isCommonTrick() and - (not data.card:isVirtual() or #data.card.subcards == 0) + (not data.card:isVirtual() or #data.card.subcards == 0) end, on_use = function(self, event, target, player, data) player:drawCards(1, self.name) - end, + end } -local qicai = fk.CreateTargetModSkill{ +local qicai = fk.CreateTargetModSkill { name = "qicai", frequency = Skill.Compulsory, bypass_distances = function(self, player, skill, card) return player:hasSkill(self.name) and card and card.type == Card.TypeTrick - end, + end } local huangyueying = General:new(extension, "huangyueying", "shu", 3, 3, General.Female) huangyueying:addSkill(jizhi) huangyueying:addSkill(qicai) -local zhiheng = fk.CreateActiveSkill{ +local zhiheng = fk.CreateActiveSkill { name = "zhiheng", anim_type = "drawcard", can_use = function(self, player) @@ -717,36 +752,32 @@ local zhiheng = fk.CreateActiveSkill{ end } -local jiuyuan = fk.CreateTriggerSkill{ +local jiuyuan = fk.CreateTriggerSkill { name = "jiuyuan$", anim_type = "support", frequency = Skill.Compulsory, - events = {fk.PreHpRecover}, + events = { fk.PreHpRecover }, can_trigger = function(self, event, target, player, data) - return - target == player and - player:hasSkill(self.name) and - data.card and - data.card.trueName == "peach" and - data.recoverBy and - data.recoverBy.kingdom == "wu" and - data.recoverBy ~= player + return target == player and player:hasSkill(self.name) and data.card and data.card.trueName == "peach" and + data.recoverBy and data.recoverBy.kingdom == "wu" and data.recoverBy ~= player end, on_use = function(self, event, target, player, data) data.num = data.num + 1 - end, + end } local sunquan = General:new(extension, "sunquan", "wu", 4) sunquan:addSkill(zhiheng) sunquan:addSkill(jiuyuan) -local qixi = fk.CreateViewAsSkill{ +local qixi = fk.CreateViewAsSkill { name = "qixi", anim_type = "control", pattern = "dismantlement", card_filter = function(self, to_select, selected) - if #selected == 1 then return false end + if #selected == 1 then + return false + end return Fk:getCardById(to_select).color == Card.Black end, view_as = function(self, cards) @@ -757,26 +788,24 @@ local qixi = fk.CreateViewAsSkill{ c.skillName = self.name c:addSubcard(cards[1]) return c - end, + end } local ganning = General:new(extension, "ganning", "wu", 4) ganning:addSkill(qixi) -local keji = fk.CreateTriggerSkill{ +local keji = fk.CreateTriggerSkill { name = "keji", anim_type = "defensive", - events = {fk.EventPhaseChanging}, + events = { fk.EventPhaseChanging }, can_trigger = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name) and - data.to == Player.Discard and - player:usedCardTimes("slash") < 1 and - player:getMark("_keji_played_slash") == 0 + return target == player and player:hasSkill(self.name) and data.to == Player.Discard and + player:usedCardTimes("slash") < 1 and player:getMark("_keji_played_slash") == 0 end, on_use = function(self, event, target, player, data) return true end, - refresh_events = {fk.CardResponding, fk.EventPhaseStart}, + refresh_events = { fk.CardResponding, fk.EventPhaseStart }, can_refresh = function(self, event, target, player, data) if not (target == player and player:hasSkill(self.name)) then return false @@ -799,7 +828,7 @@ local keji = fk.CreateTriggerSkill{ local lvmeng = General:new(extension, "lvmeng", "wu", 4) lvmeng:addSkill(keji) -local kurou = fk.CreateActiveSkill{ +local kurou = fk.CreateActiveSkill { name = "kurou", anim_type = "drawcard", card_filter = function(self, to_select, selected, selected_targets) @@ -816,20 +845,22 @@ local kurou = fk.CreateActiveSkill{ local huanggai = General:new(extension, "huanggai", "wu", 4) huanggai:addSkill(kurou) -local yingzi = fk.CreateTriggerSkill{ +local yingzi = fk.CreateTriggerSkill { name = "yingzi", anim_type = "drawcard", - events = {fk.DrawNCards}, + events = { fk.DrawNCards }, on_use = function(self, event, target, player, data) data.n = data.n + 1 - end, + end } -local fanjian = fk.CreateActiveSkill{ +local fanjian = fk.CreateActiveSkill { name = "fanjian", can_use = function(self, player) return player:usedSkillTimes(self.name, Player.HistoryPhase) == 0 and not player:isKongcheng() end, - card_filter = function() return false end, + card_filter = function() + return false + end, target_filter = function(self, to_select, selected) return #selected == 0 and to_select ~= Self.id end, @@ -837,29 +868,31 @@ local fanjian = fk.CreateActiveSkill{ on_use = function(self, room, effect) local player = room:getPlayerById(effect.from) local target = room:getPlayerById(effect.tos[1]) - local choice = room:askForChoice(target, {"spade", "heart", "club", "diamond"}, self.name) + local choice = room:askForChoice(target, { "spade", "heart", "club", "diamond" }, self.name) local card = room:askForCardChosen(target, player, 'h', self.name) room:obtainCard(target.id, card, true, fk.ReasonPrey) if Fk:getCardById(card):getSuitString() ~= choice then - room:damage{ + room:damage { from = player, to = target, damage = 1, - skillName = self.name, + skillName = self.name } end - end, + end } local zhouyu = General:new(extension, "zhouyu", "wu", 3) zhouyu:addSkill(yingzi) zhouyu:addSkill(fanjian) -local guose = fk.CreateViewAsSkill{ +local guose = fk.CreateViewAsSkill { name = "guose", anim_type = "control", pattern = "indulgence", card_filter = function(self, to_select, selected) - if #selected == 1 then return false end + if #selected == 1 then + return false + end return Fk:getCardById(to_select).suit == Card.Diamond end, view_as = function(self, cards) @@ -870,20 +903,20 @@ local guose = fk.CreateViewAsSkill{ c.skillName = self.name c:addSubcard(cards[1]) return c - end, + end } -local liuli = fk.CreateTriggerSkill{ +local liuli = fk.CreateTriggerSkill { name = "liuli", anim_type = "defensive", - events = {fk.TargetConfirming}, + events = { fk.TargetConfirming }, can_trigger = function(self, event, target, player, data) - local ret = target == player and player:hasSkill(self.name) and - data.card.trueName == "slash" + local ret = target == player and player:hasSkill(self.name) and data.card.trueName == "slash" if ret then local room = player.room local from = room:getPlayerById(data.from) for _, p in ipairs(room.alive_players) do - if p ~= player and p.id ~= data.from and player:inMyAttackRange(p) and not from:isProhibited(p, data.card) then + if p ~= player and p.id ~= data.from and player:inMyAttackRange(p) and + not from:isProhibited(p, data.card) then return true end end @@ -899,10 +932,12 @@ local liuli = fk.CreateTriggerSkill{ table.insert(targets, p.id) end end - if #targets == 0 then return false end + if #targets == 0 then + return false + end local plist, cid = room:askForChooseCardAndPlayers(player, targets, 1, 1, nil, prompt, self.name, true) if #plist > 0 then - self.cost_data = {plist[1], cid} + self.cost_data = { plist[1], cid } return true end end, @@ -913,28 +948,32 @@ local liuli = fk.CreateTriggerSkill{ room:throwCard(self.cost_data[2], self.name, player, player) TargetGroup:removeTarget(data.targetGroup, player.id) TargetGroup:pushTargets(data.targetGroup, to) - end, + end } local daqiao = General:new(extension, "daqiao", "wu", 3, 3, General.Female) daqiao:addSkill(guose) daqiao:addSkill(liuli) -local qianxun = fk.CreateProhibitSkill{ +local qianxun = fk.CreateProhibitSkill { name = "qianxun", frequency = Skill.Compulsory, is_prohibited = function(self, from, to, card) if to:hasSkill(self.name) then return card.name == "indulgence" or card.name == "snatch" end - end, + end } -local lianying = fk.CreateTriggerSkill{ +local lianying = fk.CreateTriggerSkill { name = "lianying", anim_type = "drawcard", - events = {fk.AfterCardsMove}, + events = { fk.AfterCardsMove }, can_trigger = function(self, event, target, player, data) - if not player:hasSkill(self.name) then return end - if not player:isKongcheng() then return end + if not player:hasSkill(self.name) then + return + end + if not player:isKongcheng() then + return + end for _, move in ipairs(data) do if move.from == player.id then for _, info in ipairs(move.moveInfo) do @@ -947,18 +986,20 @@ local lianying = fk.CreateTriggerSkill{ end, on_use = function(self, event, target, player, data) player:drawCards(1, self.name) - end, + end } local luxun = General:new(extension, "luxun", "wu", 3) luxun:addSkill(qianxun) luxun:addSkill(lianying) -local xiaoji = fk.CreateTriggerSkill{ +local xiaoji = fk.CreateTriggerSkill { name = "xiaoji", anim_type = "drawcard", - events = {fk.AfterCardsMove}, + events = { fk.AfterCardsMove }, can_trigger = function(self, event, target, player, data) - if not player:hasSkill(self.name) then return end + if not player:hasSkill(self.name) then + return + end for _, move in ipairs(data) do if move.from == player.id then for _, info in ipairs(move.moveInfo) do @@ -982,7 +1023,9 @@ local xiaoji = fk.CreateTriggerSkill{ end self.cancel_cost = false for i = 1, i do - if self.cancel_cost or not player:hasSkill(self.name) then break end + if self.cancel_cost or not player:hasSkill(self.name) then + break + end self:doCost(event, target, player, data) end end, @@ -994,9 +1037,9 @@ local xiaoji = fk.CreateTriggerSkill{ end, on_use = function(self, event, target, player, data) player:drawCards(2, self.name) - end, + end } -local jieyin = fk.CreateActiveSkill{ +local jieyin = fk.CreateActiveSkill { name = "jieyin", anim_type = "support", can_use = function(self, player) @@ -1007,9 +1050,7 @@ local jieyin = fk.CreateActiveSkill{ end, target_filter = function(self, to_select, selected) local target = Fk:currentRoom():getPlayerById(to_select) - return target:isWounded() and - target.gender == General.Male - and #selected < 1 and to_select ~= Self.id + return target:isWounded() and target.gender == General.Male and #selected < 1 and to_select ~= Self.id end, target_num = 1, card_num = 2, @@ -1036,7 +1077,7 @@ local sunshangxiang = General:new(extension, "sunshangxiang", "wu", 3, 3, Genera sunshangxiang:addSkill(xiaoji) sunshangxiang:addSkill(jieyin) -local qingnang = fk.CreateActiveSkill{ +local qingnang = fk.CreateActiveSkill { name = "qingnang", anim_type = "support", can_use = function(self, player) @@ -1059,14 +1100,16 @@ local qingnang = fk.CreateActiveSkill{ recoverBy = from, skillName = self.name }) - end, + end } -local jijiu = fk.CreateViewAsSkill{ +local jijiu = fk.CreateViewAsSkill { name = "jijiu", anim_type = "support", pattern = "peach", card_filter = function(self, to_select, selected) - if #selected == 1 then return false end + if #selected == 1 then + return false + end return Fk:getCardById(to_select).color == Card.Red end, view_as = function(self, cards) @@ -1083,17 +1126,17 @@ local jijiu = fk.CreateViewAsSkill{ end, enabled_at_response = function(self, player) return player.phase == Player.NotActive - end, + end } local huatuo = General:new(extension, "huatuo", "qun", 3) huatuo:addSkill(qingnang) huatuo:addSkill(jijiu) -local wushuang = fk.CreateTriggerSkill{ +local wushuang = fk.CreateTriggerSkill { name = "wushuang", anim_type = "offensive", frequency = Skill.Compulsory, - events = {fk.TargetSpecified, fk.TargetConfirmed}, + events = { fk.TargetSpecified, fk.TargetConfirmed }, can_trigger = function(self, event, target, player, data) if not player:hasSkill(self.name) then return false @@ -1114,12 +1157,12 @@ local wushuang = fk.CreateTriggerSkill{ data.fixedAddTimesResponsors = data.fixedAddTimesResponsors or {} table.insert(data.fixedAddTimesResponsors, (event == fk.TargetSpecified and data.to or data.from)) end - end, + end } local lvbu = General:new(extension, "lvbu", "qun", 4) lvbu:addSkill(wushuang) -local lijian = fk.CreateActiveSkill{ +local lijian = fk.CreateActiveSkill { name = "lijian", anim_type = "offensive", can_use = function(self, player) @@ -1129,8 +1172,8 @@ local lijian = fk.CreateActiveSkill{ return #selected == 0 end, target_filter = function(self, to_select, selected) - return #selected < 2 and to_select ~= Self.id and - Fk:currentRoom():getPlayerById(to_select).gender == General.Male + return #selected < 2 and to_select ~= Self.id and Fk:currentRoom():getPlayerById(to_select).gender == + General.Male end, target_num = 2, min_card_num = 1, @@ -1143,29 +1186,28 @@ local lijian = fk.CreateActiveSkill{ from = use.tos[2], tos = { { use.tos[1] } }, card = duel, - prohibitedCardNames = { "nullification" }, + prohibitedCardNames = { "nullification" } } room:useCard(new_use) - end, + end } -local biyue = fk.CreateTriggerSkill{ +local biyue = fk.CreateTriggerSkill { name = "biyue", anim_type = "drawcard", - events = {fk.EventPhaseStart}, + events = { fk.EventPhaseStart }, can_trigger = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name) - and player.phase == Player.Finish + return target == player and player:hasSkill(self.name) and player.phase == Player.Finish end, on_use = function(self, event, target, player, data) player:drawCards(1, self.name) - end, + end } local diaochan = General:new(extension, "diaochan", "qun", 3, 3, General.Female) diaochan:addSkill(lijian) diaochan:addSkill(biyue) -local role_mode = fk.CreateGameMode{ - name = "aaa_role_mode", -- just to let it at the top of list +local role_mode = fk.CreateGameMode { + name = "aaa_role_mode", -- just to let it at the top of list minPlayer = 2, maxPlayer = 8, is_counted = function(self, room) @@ -1174,16 +1216,12 @@ local role_mode = fk.CreateGameMode{ surrender_func = function(self, playedTime) local roleCheck = false local roleText = "" - local roleTable = { - { "lord" }, - { "lord", "rebel" }, - { "lord", "rebel", "renegade" }, - { "lord", "loyalist", "rebel", "renegade" }, - { "lord", "loyalist", "rebel", "rebel", "renegade" }, - { "lord", "loyalist", "rebel", "rebel", "rebel", "renegade" }, - { "lord", "loyalist", "loyalist", "rebel", "rebel", "rebel", "renegade" }, - { "lord", "loyalist", "loyalist", "rebel", "rebel", "rebel", "rebel", "renegade" }, - } + local roleTable = { { "lord" }, { "lord", "rebel" }, { "lord", "rebel", "renegade" }, + { "lord", "loyalist", "rebel", "renegade" }, + { "lord", "loyalist", "rebel", "rebel", "renegade" }, + { "lord", "loyalist", "rebel", "rebel", "rebel", "renegade" }, + { "lord", "loyalist", "loyalist", "rebel", "rebel", "rebel", "renegade" }, + { "lord", "loyalist", "loyalist", "rebel", "rebel", "rebel", "rebel", "renegade" } } roleTable = roleTable[#Fk:currentRoom().players] @@ -1230,7 +1268,10 @@ local role_mode = fk.CreateGameMode{ roleText = "left one rebel alive" else if Self.role == "loyalist" then - return { { text = "loyalist never surrender", passed = false } } + return { { + text = "loyalist never surrender", + passed = false + } } else if #Fk:currentRoom().alive_players == 2 then roleCheck = true @@ -1263,27 +1304,30 @@ local role_mode = fk.CreateGameMode{ roleText = "left you alive" end - return { - { text = "time limitation: 5 min", passed = playedTime >= 300 }, - { text = roleText, passed = roleCheck }, - } - end, + return { { + text = "time limitation: 5 min", + passed = playedTime >= 300 + }, { + text = roleText, + passed = roleCheck + } } + end } extension:addGameMode(role_mode) -Fk:loadTranslationTable{ +Fk:loadTranslationTable { ["time limitation: 5 sec"] = "游戏时长达到5秒(测试用)", ["left lord and loyalist alive"] = "仅剩你和主忠方存活", ["left one rebel alive"] = "反贼仅剩你存活且不存在存活内奸", ["left you alive"] = "主忠方仅剩你存活且其他阵营仅剩一方", - ["loyalist never surrender"] = "忠臣永不投降!", + ["loyalist never surrender"] = "忠臣永不投降!" } local anjiang = General(extension, "anjiang", "unknown", 5) anjiang.gender = General.Agender anjiang.total_hidden = true -Fk:loadTranslationTable{ - ["anjiang"] = "暗将", +Fk:loadTranslationTable { + ["anjiang"] = "暗将" } -- load translations of this package diff --git a/packages/standard/standard_ai.lua b/packages/standard/standard_ai.lua new file mode 100644 index 00000000..af9b0a20 --- /dev/null +++ b/packages/standard/standard_ai.lua @@ -0,0 +1,227 @@ +fk.ai_use_play.rende = function(self, skill) + for _, p in ipairs(self.friends_noself) do + if p.kingdom == "shu" and #self.player:getCardIds("h") >= self.player.hp then + self.use_id = {} + for _, cid in ipairs(self.player:getCardIds("h")) do + if #self.use_id < #self.player:getCardIds("h") / 2 then + table.insert(self.use_id, cid) + end + end + self.use_tos = { p.id } + return + end + end + for _, p in ipairs(self.friends_noself) do + if #self.player:getCardIds("h") >= self.player.hp then + self.use_id = {} + for _, cid in ipairs(self.player:getCardIds("h")) do + if #self.use_id < #self.player:getCardIds("h") / 2 then + table.insert(self.use_id, cid) + end + end + self.use_tos = { p.id } + return + end + end +end + +fk.ai_card.jijiang = { priority = 10 } + +fk.ai_use_play.lijian = function(self, skill) + local c = Fk:cloneCard("duel") + c.skillName = "lijian" + for _, p in ipairs(self.enemies) do + for _, pt in ipairs(self.enemies) do + if p.gender == General.Male and pt.gender == General.Male and p.id ~= pt.id and + c.skill:targetFilter(pt.id, {}, p.id, c) then + self.use_id = { self.player:getCardIds("he")[1] } + self.use_tos = { pt.id, p.id } + break + end + end + end + for _, p in ipairs(self.friends_noself) do + for _, pt in ipairs(self.enemies) do + if p.gender == General.Male and pt.gender == General.Male and p.id ~= pt.id and + c.skill:targetFilter(pt.id, {}, p.id, c) then + self.use_id = { self.player:getCardIds("he")[1] } + self.use_tos = { pt.id, p.id } + break + end + end + end +end + +fk.ai_card.lijian = { priority = 2 } + +fk.ai_use_play.zhiheng = function(self, skill) + local card_ids = {} + for _, h in ipairs(self.player:getCardIds("he")) do + if #card_ids < #self.player:getCardIds("he") / 2 then + table.insert(card_ids, h) + end + end + if #card_ids > 0 then + self.use_id = card_ids + end +end + +fk.ai_use_play.kurou = function(self, skill) + if #self:getActives("peach") + self.player.hp > 1 then + local slash = Fk:cloneCard("slash") + if slash.skill:canUse(self.player, slash) and not self.player:prohibitUse(slash) then + fk.ai_use_play.slash(self, slash) + if self.use_id then + self.use_id = {} + self.use_tos = {} + end + end + end +end + +fk.ai_use_play.fanjian = function(self, skill) + for _, p in ipairs(self.enemies) do + if #self.player:getCardIds("h") > 0 then + self.use_id = {} + table.insert(self.use_tos, p.id) + break + end + end +end + +fk.ai_use_play.jieyin = function(self, skill) + for cs, p in ipairs(self.friends_noself) do + cs = self.player:getCardIds("h") + if #cs > 1 and p.gender == General.Male and p:isWounded() then + self.use_id = { cs[1], cs[2] } + table.insert(self.use_tos, p.id) + break + end + end +end + +fk.ai_use_play.qingnang = function(self, skill) + for cs, p in ipairs(self.friends) do + cs = self.player:getCardIds("h") + if #cs > 0 and p:isWounded() then + self.use_id = { cs[1] } + table.insert(self.use_tos, p.id) + break + end + end +end + +fk.ai_skill_invoke.jianxiong = true + +fk.ai_card.hujia = { priority = 10 } + +fk.ai_response_card["#hujia-ask"] = function(self, pattern, prompt, cancelable, data) + local to = self.room:getPlayerById(tonumber(prompt:split(":")[2])) + if to and self:isFriend(to) then + self:setUseId(pattern) + end +end + +fk.ai_response_card["#jijiang-ask"] = fk.ai_response_card["#hujia-ask"] + +fk.ai_skill_invoke.fankui = function(self, data, prompt) + local damage = self:eventData("Damage") + return damage and damage.from and not self:isFriend(damage.from) +end + +fk.ai_response_card["#guicai-ask"] = function(self, pattern, prompt, cancelable, data) + local cards = table.map(self.player:getHandlyIds(true), function(id) + return Fk:getCardById(id) + end) + local id = self:getRetrialCardId(cards) + if id then + self.use_id = id + end +end + +fk.ai_skill_invoke.ganglie = function(self, data, prompt) + local damage = self:eventData("Damage") + return damage and damage.from and not self:isFriend(damage.from) +end + +fk.ai_skill_invoke.luoyi = function(self, data, prompt) + for _, p in ipairs(self.enemies) do + if #self:getActives("slash") > 0 and not self:isWeak() then + return true + end + end +end + +fk.ai_skill_invoke.tiandu = true + +fk.ai_skill_invoke.yiji = true + +fk.ai_skill_invoke.luoshen = true + +fk.ai_skill_invoke.guanxing = true + +fk.ai_skill_invoke.tieqi = function(self, data, prompt) + local use = self:eventData("UseCard") + for _, p in ipairs(TargetGroup:getRealTargets(use.tos)) do + p = self.room:getPlayerById(p) + if self:isEnemie(p) then + return true + end + end +end + +fk.ai_skill_invoke.jizhi = true + +fk.ai_skill_invoke.keji = true + +fk.ai_skill_invoke.yingzi = true + +fk.ai_skill_invoke.lianying = true + +fk.ai_skill_invoke.xiaoji = true + +fk.ai_skill_invoke.biyue = true + +fk.ai_choose_players.tuxi = function(self, targets, min_num, num, cancelable) + for _, pid in ipairs(targets) do + local p = self.room:getPlayerById(pid) + if self:isEnemie(p) and #self.use_tos < num then + table.insert(self.use_tos, pid) + end + end +end + +fk.ai_use_skill.yiji_active = function(self, prompt, cancelable, data) + for _, p in ipairs(self.friends_noself) do + for c, cid in ipairs(self.player.yiji_ids) do + c = Fk:getCardById(cid) + if c:getMark("yiji") > 0 and c.skill:canUse(p, c) then + self.use_tos = { p.id } + self.use_id = json.encode { + skill = "yiji_active", + subcards = { cid } + } + return + end + end + end +end + +fk.ai_choose_players.liuli = function(self, targets, min_num, num, cancelable) + for _, pid in ipairs(targets) do + local p = self.room:getPlayerById(pid) + if self:isEnemie(p) and #self.use_tos < num and #self.player:getCardIds("he") > 0 then + table.insert(self.use_tos, pid) + self.use_id = { self.player:getCardIds("he")[1] } + return + end + end + for _, pid in ipairs(targets) do + local p = self.room:getPlayerById(pid) + if not self:isFriend(p) and #self.use_tos < num and #self.player:getCardIds("he") > 0 then + table.insert(self.use_tos, pid) + self.use_id = { self.player:getCardIds("he")[1] } + return + end + end +end diff --git a/packages/standard_cards/i18n/zh_CN.lua b/packages/standard_cards/i18n/zh_CN.lua index 8bd9ec4d..763cbc3d 100644 --- a/packages/standard_cards/i18n/zh_CN.lua +++ b/packages/standard_cards/i18n/zh_CN.lua @@ -1,6 +1,6 @@ -- SPDX-License-Identifier: GPL-3.0-or-later -Fk:loadTranslationTable{ +Fk:loadTranslationTable { ["standard_cards"] = "标+EX", ["unknown_card"] = '未知牌', @@ -50,13 +50,12 @@ Fk:loadTranslationTable{ ["slash"] = "杀", [":slash"] = "基本牌
时机:出牌阶段
目标:攻击范围内的一名其他角色
效果:对目标角色造成1点伤害。", ["#slash-jink"] = "%src 对你使用了杀,请使用 %arg 张闪", + ["#slash-jinks"] = "%src 对你使用了杀,需 %arg2 张闪,你还需使用 %arg 张闪", ["jink"] = "闪", [":jink"] = "基本牌
时机:【杀】对你生效时
目标:此【杀】
效果:抵消此【杀】的效果。", - ["peach"] = "桃", [":peach"] = "基本牌
时机:出牌阶段/一名角色处于濒死状态时
目标:已受伤的你/处于濒死状态的角色
效果:目标角色回复1点体力。", - ["dismantlement"] = "过河拆桥", [":dismantlement"] = "锦囊牌
时机:出牌阶段
目标:一名区域内有牌的其他角色。
效果:你弃置目标角色区域内的一张牌。", ["dismantlement_skill"] = "过河拆桥", @@ -67,26 +66,19 @@ Fk:loadTranslationTable{ ["duel"] = "决斗", [":duel"] = "锦囊牌
时机:出牌阶段
目标:一名其他角色
效果:由目标角色开始,你与其轮流:打出一张【杀】,否则受到对方的1点伤害并结束此牌结算。", - ["collateral"] = "借刀杀人", [":collateral"] = "锦囊牌
时机:出牌阶段
目标:装备区内有武器牌且攻击范围内有【杀】的合法目标的一名其他角色A(你需要选择一名A攻击范围内的【杀】的合法目标B)
效果:A须对B使用一张【杀】,否则你获得A装备区内的武器牌。", ["#collateral-slash"] = "借刀杀人:你需对 %dest 使用【杀】,否则 %src 获得你的武器", - ["ex_nihilo"] = "无中生有", [":ex_nihilo"] = "锦囊牌
时机:出牌阶段
目标:你
效果:目标角色摸两张牌。", - ["nullification"] = "无懈可击", [":nullification"] = "锦囊牌
时机:锦囊牌对目标角色生效前,或一张【无懈可击】生效前
目标:该锦囊牌
效果:抵消该锦囊牌对该角色产生的效果,或抵消另一张【无懈可击】产生的效果。", - ["savage_assault"] = "南蛮入侵", [":savage_assault"] = "锦囊牌
时机:出牌阶段
目标:所有其他角色
效果:每名目标角色须打出一张【杀】,否则受到1点伤害。", - ["archery_attack"] = "万箭齐发", [":archery_attack"] = "锦囊牌
时机:出牌阶段
目标:所有其他角色
效果:每名目标角色须打出一张【闪】,否则受到1点伤害。", - ["god_salvation"] = "桃园结义", [":god_salvation"] = "锦囊牌
时机:出牌阶段
目标:所有角色
效果:每名目标角色回复1点体力。", - ["amazing_grace"] = "五谷丰登", [":amazing_grace"] = "锦囊牌
时机:出牌阶段
目标:所有角色
效果:你亮出牌堆顶等于角色数的牌,每名目标角色获得其中一张牌,然后将其余的牌置入弃牌堆。", ["amazing_grace_skill"] = "五谷选牌", @@ -94,16 +86,12 @@ Fk:loadTranslationTable{ ["lightning"] = "闪电", [":lightning"] = "延时锦囊牌
时机:出牌阶段
目标:你
效果:将此牌置于目标角色判定区内。其判定阶段进行判定:若结果为黑桃2-9,其受到3点雷电伤害并将【闪电】置入弃牌堆,否则将【闪电】移动至其下家判定区内。", - ["indulgence"] = "乐不思蜀", [":indulgence"] = "延时锦囊牌
时机:出牌阶段
目标:一名其他角色
效果:将此牌置于目标角色判定区内。其判定阶段进行判定:若结果不为红桃,其跳过出牌阶段。然后将【乐不思蜀】置入弃牌堆。", - ["crossbow"] = "诸葛连弩", [":crossbow"] = "装备牌·武器
攻击范围:1
武器技能:锁定技。你于出牌阶段内使用【杀】无次数限制。", - ["qinggang_sword"] = "青釭剑", [":qinggang_sword"] = "装备牌·武器
攻击范围:2
武器技能:锁定技。你的【杀】无视目标角色的防具。", - ["ice_sword"] = "寒冰剑", [":ice_sword"] = "装备牌·武器
攻击范围:2
武器技能:每当你使用【杀】对目标角色造成伤害时,若该角色有牌,你可以防止此伤害,然后依次弃置其两张牌。", ["#ice_sword_skill"] = "寒冰剑", @@ -130,7 +118,6 @@ Fk:loadTranslationTable{ ["halberd"] = "方天画戟", [":halberd"] = "装备牌·武器
攻击范围:4
武器技能:锁定技。你使用最后的手牌【杀】可以额外选择至多两名目标。", - ["kylin_bow"] = "麒麟弓", [":kylin_bow"] = "装备牌·武器
攻击范围:5
武器技能:每当你使用【杀】对目标角色造成伤害时,你可以弃置其装备区内的一张坐骑牌。", ["#kylin_bow_skill"] = "麒麟弓", diff --git a/packages/standard_cards/init.lua b/packages/standard_cards/init.lua index 5e9b2a92..b752c50c 100644 --- a/packages/standard_cards/init.lua +++ b/packages/standard_cards/init.lua @@ -1,28 +1,28 @@ -- SPDX-License-Identifier: GPL-3.0-or-later - local extension = Package:new("standard_cards", Package.CardPack) extension.metadata = require "packages.standard_cards.metadata" -local slashSkill = fk.CreateActiveSkill{ +local slashSkill = fk.CreateActiveSkill { name = "slash_skill", max_phase_use_time = 1, target_num = 1, can_use = function(self, player, card) - return - table.find(Fk:currentRoom().alive_players, function(p) - return self:withinTimesLimit(player, Player.HistoryPhase, card, "slash", p) - end) + return table.find(Fk:currentRoom().alive_players, function(p) + return self:withinTimesLimit(player, Player.HistoryPhase, card, "slash", p) + end) end, mod_target_filter = function(self, to_select, selected, user, card, distance_limited) local player = Fk:currentRoom():getPlayerById(to_select) local from = Fk:currentRoom():getPlayerById(user) - return from ~= player and not (distance_limited and not self:withinDistanceLimit(from, true, card, player)) + return + user ~= to_select and not (distance_limited and not self:withinDistanceLimit(from, true, card, player)) and + not (card and from:isProhibited(player, card)) end, target_filter = function(self, to_select, selected, _, card) if #selected < self:getMaxTargetNum(Self, card) then local player = Fk:currentRoom():getPlayerById(to_select) return self:modTargetFilter(to_select, selected, Self.id, card, true) and - (#selected > 0 or self:withinTimesLimit(Self, Player.HistoryPhase, card, "slash", player)) + (#selected > 0 or self:withinTimesLimit(Self, Player.HistoryPhase, card, "slash", player)) end end, on_effect = function(self, room, effect) @@ -40,51 +40,27 @@ local slashSkill = fk.CreateActiveSkill{ end end } -local slash = fk.CreateBasicCard{ +local slash = fk.CreateBasicCard { name = "slash", number = 7, suit = Card.Spade, is_damage_card = true, - skill = slashSkill, + skill = slashSkill } -extension:addCards({ - slash, - slash:clone(Card.Spade, 8), - slash:clone(Card.Spade, 8), - slash:clone(Card.Spade, 9), - slash:clone(Card.Spade, 9), - slash:clone(Card.Spade, 10), - slash:clone(Card.Spade, 10), +extension:addCards({ slash, slash:clone(Card.Spade, 8), slash:clone(Card.Spade, 8), slash:clone(Card.Spade, 9), + slash:clone(Card.Spade, 9), slash:clone(Card.Spade, 10), slash:clone(Card.Spade, 10), - slash:clone(Card.Club, 2), - slash:clone(Card.Club, 3), - slash:clone(Card.Club, 4), - slash:clone(Card.Club, 5), - slash:clone(Card.Club, 6), - slash:clone(Card.Club, 7), - slash:clone(Card.Club, 8), - slash:clone(Card.Club, 8), - slash:clone(Card.Club, 9), - slash:clone(Card.Club, 9), - slash:clone(Card.Club, 10), - slash:clone(Card.Club, 10), - slash:clone(Card.Club, 11), - slash:clone(Card.Club, 11), + slash:clone(Card.Club, 2), slash:clone(Card.Club, 3), slash:clone(Card.Club, 4), + slash:clone(Card.Club, 5), slash:clone(Card.Club, 6), slash:clone(Card.Club, 7), + slash:clone(Card.Club, 8), slash:clone(Card.Club, 8), slash:clone(Card.Club, 9), + slash:clone(Card.Club, 9), slash:clone(Card.Club, 10), slash:clone(Card.Club, 10), + slash:clone(Card.Club, 11), slash:clone(Card.Club, 11), slash:clone(Card.Heart, 10), + slash:clone(Card.Heart, 10), slash:clone(Card.Heart, 11), slash:clone(Card.Diamond, 6), + slash:clone(Card.Diamond, 7), slash:clone(Card.Diamond, 8), slash:clone(Card.Diamond, 9), + slash:clone(Card.Diamond, 10), slash:clone(Card.Diamond, 13) }) - slash:clone(Card.Heart, 10), - slash:clone(Card.Heart, 10), - slash:clone(Card.Heart, 11), - - slash:clone(Card.Diamond, 6), - slash:clone(Card.Diamond, 7), - slash:clone(Card.Diamond, 8), - slash:clone(Card.Diamond, 9), - slash:clone(Card.Diamond, 10), - slash:clone(Card.Diamond, 13), -}) - -local jinkSkill = fk.CreateActiveSkill{ +local jinkSkill = fk.CreateActiveSkill { name = "jink_skill", can_use = function() return false @@ -95,39 +71,26 @@ local jinkSkill = fk.CreateActiveSkill{ end end } -local jink = fk.CreateBasicCard{ +local jink = fk.CreateBasicCard { name = "jink", suit = Card.Heart, number = 2, - skill = jinkSkill, + skill = jinkSkill } -extension:addCards({ - jink, - jink:clone(Card.Heart, 2), - jink:clone(Card.Heart, 13), +extension:addCards({ jink, jink:clone(Card.Heart, 2), jink:clone(Card.Heart, 13), jink:clone(Card.Diamond, 2), + jink:clone(Card.Diamond, 2), jink:clone(Card.Diamond, 3), jink:clone(Card.Diamond, 4), + jink:clone(Card.Diamond, 5), jink:clone(Card.Diamond, 6), jink:clone(Card.Diamond, 7), + jink:clone(Card.Diamond, 8), jink:clone(Card.Diamond, 9), jink:clone(Card.Diamond, 10), + jink:clone(Card.Diamond, 11), jink:clone(Card.Diamond, 11) }) - jink:clone(Card.Diamond, 2), - jink:clone(Card.Diamond, 2), - jink:clone(Card.Diamond, 3), - jink:clone(Card.Diamond, 4), - jink:clone(Card.Diamond, 5), - jink:clone(Card.Diamond, 6), - jink:clone(Card.Diamond, 7), - jink:clone(Card.Diamond, 8), - jink:clone(Card.Diamond, 9), - jink:clone(Card.Diamond, 10), - jink:clone(Card.Diamond, 11), - jink:clone(Card.Diamond, 11), -}) - -local peachSkill = fk.CreateActiveSkill{ +local peachSkill = fk.CreateActiveSkill { name = "peach_skill", mod_target_filter = function(self, to_select) return Fk:currentRoom():getPlayerById(to_select):isWounded() and - not table.find(Fk:currentRoom().alive_players, function(p) - return p.dying - end) + not table.find(Fk:currentRoom().alive_players, function(p) + return p.dying + end) end, can_use = function(self, player, card) return player:isWounded() and not player:isProhibited(player, card) @@ -151,72 +114,65 @@ local peachSkill = fk.CreateActiveSkill{ end end } -local peach = fk.CreateBasicCard{ +local peach = fk.CreateBasicCard { name = "peach", suit = Card.Heart, number = 3, - skill = peachSkill, + skill = peachSkill } -extension:addCards({ - peach, - peach:clone(Card.Heart, 4), - peach:clone(Card.Heart, 6), - peach:clone(Card.Heart, 7), - peach:clone(Card.Heart, 8), - peach:clone(Card.Heart, 9), - peach:clone(Card.Heart, 12), - peach:clone(Card.Heart, 12), -}) +extension:addCards({ peach, peach:clone(Card.Heart, 4), peach:clone(Card.Heart, 6), peach:clone(Card.Heart, 7), + peach:clone(Card.Heart, 8), peach:clone(Card.Heart, 9), peach:clone(Card.Heart, 12), + peach:clone(Card.Heart, 12) }) -local dismantlementSkill = fk.CreateActiveSkill{ +local dismantlementSkill = fk.CreateActiveSkill { name = "dismantlement_skill", target_num = 1, - mod_target_filter = function(self, to_select, selected, user) + mod_target_filter = function(self, to_select, selected, user, card) local player = Fk:currentRoom():getPlayerById(to_select) - return Fk:currentRoom():getPlayerById(user) ~= player and not player:isAllNude() + local from = Fk:currentRoom():getPlayerById(user) + return user ~= to_select and not player:isAllNude() and not (card and from:isProhibited(player, card)) end, - target_filter = function(self, to_select, selected) - if #selected < self:getMaxTargetNum(Self) then - return self:modTargetFilter(to_select, selected, Self.id) + target_filter = function(self, to_select, selected, _, card) + if #selected < self:getMaxTargetNum(Self, card) then + return self:modTargetFilter(to_select, selected, Self.id, card) end end, on_effect = function(self, room, effect) local from = room:getPlayerById(effect.from) local to = room:getPlayerById(effect.to) - if to.dead or to:isAllNude() then return end + if to.dead or to:isAllNude() then + return + end local cid = room:askForCardChosen(from, to, "hej", self.name) - room:throwCard({cid}, self.name, to, from) + room:throwCard({ cid }, self.name, to, from) end } -local dismantlement = fk.CreateTrickCard{ +local dismantlement = fk.CreateTrickCard { name = "dismantlement", suit = Card.Spade, number = 3, - skill = dismantlementSkill, + skill = dismantlementSkill } -extension:addCards({ - dismantlement, - dismantlement:clone(Card.Spade, 4), - dismantlement:clone(Card.Spade, 12), +extension:addCards({ dismantlement, dismantlement:clone(Card.Spade, 4), dismantlement:clone(Card.Spade, 12), + dismantlement:clone(Card.Club, 3), dismantlement:clone(Card.Club, 4), - dismantlement:clone(Card.Club, 3), - dismantlement:clone(Card.Club, 4), + dismantlement:clone(Card.Heart, 12) }) - dismantlement:clone(Card.Heart, 12), -}) - -local snatchSkill = fk.CreateActiveSkill{ +local snatchSkill = fk.CreateActiveSkill { name = "snatch_skill", distance_limit = 1, mod_target_filter = function(self, to_select, selected, user, card, distance_limited) local player = Fk:currentRoom():getPlayerById(to_select) local from = Fk:currentRoom():getPlayerById(user) - return from ~= player and not (player:isAllNude() or (distance_limited and not self:withinDistanceLimit(from, false, card, player))) + return user ~= to_select and + not (player:isAllNude() or + (distance_limited and not self:withinDistanceLimit(from, false, card, player))) and + not (card and from:isProhibited(player, card)) end, target_filter = function(self, to_select, selected, _, card) - if #selected == 0 then + if #selected < self:getMaxTargetNum(Self, card) then return self:modTargetFilter(to_select, selected, Self.id, card, true) end end, @@ -224,35 +180,33 @@ local snatchSkill = fk.CreateActiveSkill{ on_effect = function(self, room, effect) local from = room:getPlayerById(effect.from) local to = room:getPlayerById(effect.to) - if to.dead or to:isAllNude() then return end + if to.dead or to:isAllNude() then + return + end local cid = room:askForCardChosen(from, to, "hej", self.name) room:obtainCard(from, cid, false, fk.ReasonPrey) end } -local snatch = fk.CreateTrickCard{ +local snatch = fk.CreateTrickCard { name = "snatch", suit = Card.Spade, number = 3, - skill = snatchSkill, + skill = snatchSkill } -extension:addCards({ - snatch, - snatch:clone(Card.Spade, 4), - snatch:clone(Card.Spade, 11), +extension:addCards({ snatch, snatch:clone(Card.Spade, 4), snatch:clone(Card.Spade, 11), snatch:clone(Card.Diamond, 3), + snatch:clone(Card.Diamond, 4) }) - snatch:clone(Card.Diamond, 3), - snatch:clone(Card.Diamond, 4), -}) - -local duelSkill = fk.CreateActiveSkill{ +local duelSkill = fk.CreateActiveSkill { name = "duel_skill", - mod_target_filter = function(self, to_select, selected, user) - return user ~= to_select + mod_target_filter = function(self, to_select, selected, user, card) + local player = Fk:currentRoom():getPlayerById(to_select) + local from = Fk:currentRoom():getPlayerById(user) + return user ~= to_select and not (card and from:isProhibited(player, card)) end, - target_filter = function(self, to_select, selected) - if #selected == 0 then - return self:modTargetFilter(to_select, selected, Self.id) + target_filter = function(self, to_select, selected, _, card) + if #selected < self:getMaxTargetNum(Self, card) then + return self:modTargetFilter(to_select, selected, Self.id, card) end end, target_num = 1, @@ -270,7 +224,6 @@ local duelSkill = fk.CreateActiveSkill{ if effect.fixedAddTimesResponsors then canFix = table.contains(effect.fixedAddTimesResponsors, currentResponser.id) end - if canFix then if type(effect.fixedResponseTimes) == 'table' then loopTimes = effect.fixedResponseTimes["slash"] or 1 @@ -279,25 +232,17 @@ local duelSkill = fk.CreateActiveSkill{ end end end - - local cardResponded + local can for i = 1, loopTimes do - cardResponded = room:askForResponse(currentResponser, 'slash', nil, nil, false, nil, effect) - if cardResponded then - room:responseCard({ - from = currentResponser.id, - card = cardResponded, - responseToEvent = effect, - }) + if room:askForResponse(currentResponser, 'slash', nil, nil, false, nil, effect) then else + can = true break end end - - if not cardResponded then + if can then break end - currentTurn = currentTurn % 2 + 1 currentResponser = responsers[currentTurn] end @@ -309,79 +254,90 @@ local duelSkill = fk.CreateActiveSkill{ card = effect.card, damage = 1, damageType = fk.NormalDamage, - skillName = self.name, + skillName = self.name }) end end } -local duel = fk.CreateTrickCard{ +local duel = fk.CreateTrickCard { name = "duel", suit = Card.Spade, number = 1, is_damage_card = true, - skill = duelSkill, + skill = duelSkill } -extension:addCards({ - duel, +extension:addCards({ duel, duel:clone(Card.Club, 1), duel:clone(Card.Diamond, 1) }) - duel:clone(Card.Club, 1), - - duel:clone(Card.Diamond, 1), -}) - -local collateralSkill = fk.CreateActiveSkill{ +local collateralSkill = fk.CreateActiveSkill { name = "collateral_skill", - mod_target_filter = function(self, to_select, selected, user, card, distance_limited) + mod_target_filter = function(self, to_select, selected, user, card) local player = Fk:currentRoom():getPlayerById(to_select) - return user ~= player.id and player:getEquipment(Card.SubtypeWeapon) + local from = Fk:currentRoom():getPlayerById(user) + return user ~= to_select and player:getEquipment(Card.SubtypeWeapon) and + not (card and from:isProhibited(player, card)) end, - target_filter = function(self, to_select, selected) - local player = Fk:currentRoom():getPlayerById(to_select) - if #selected == 0 then - return Self ~= player and player:getEquipment(Card.SubtypeWeapon) - elseif #selected == 1 then - return Fk:currentRoom():getPlayerById(selected[1]):inMyAttackRange(player) + target_filter = function(self, to_select, selected, _, card) + if #selected >= (self:getMaxTargetNum(Self, card) - 1) * 2 then + return false + elseif #selected % 2 == 0 then + return self:modTargetFilter(to_select, selected, Self.id, card) + elseif #selected > 0 then + return Fk:currentRoom():getPlayerById(selected[#selected]):inMyAttackRange( + Fk:currentRoom():getPlayerById(to_select)) end end, target_num = 2, on_use = function(self, room, cardUseEvent) - cardUseEvent.tos = { { cardUseEvent.tos[1][1], cardUseEvent.tos[2][1] } } + -- cardUseEvent.tos = {{cardUseEvent.tos[1][1], cardUseEvent.tos[2][1]}} + local tos = {} + local exclusive = {} + for i, pid in ipairs(TargetGroup:getRealTargets(cardUseEvent.tos)) do + if i % 2 == 1 then + exclusive = { pid } + else + table.insert(exclusive, pid) + table.insert(tos, exclusive) + end + end + cardUseEvent.tos = tos end, on_effect = function(self, room, effect) local to = room:getPlayerById(effect.to) - if to.dead or not to:getEquipment(Card.SubtypeWeapon) then return end - local prompt = "#collateral-slash:"..effect.from..":"..effect.subTargets[1] + if to.dead or not to:getEquipment(Card.SubtypeWeapon) then + return + end + local prompt = "#collateral-slash:" .. effect.from .. ":" .. effect.subTargets[1] if #effect.subTargets > 1 then prompt = nil end - local use = room:askForUseCard(to, "slash", nil, prompt, nil, { must_targets = effect.subTargets }, effect) - if use then - use.extraUse = true - room:useCard(use) + if room:askForUseCard(to, "slash", nil, prompt, nil, { + must_targets = effect.subTargets, + exclusive_targets = effect.subTargets, + bypass_distances = true, + bypass_times = true + }, effect) then else - room:obtainCard(effect.from, - room:getPlayerById(effect.to):getEquipment(Card.SubtypeWeapon), - true, fk.ReasonGive) + room:obtainCard(effect.from, room:getPlayerById(effect.to):getEquipment(Card.SubtypeWeapon), true, + fk.ReasonGive) end end } -local collateral = fk.CreateTrickCard{ +local collateral = fk.CreateTrickCard { name = "collateral", suit = Card.Club, number = 12, - skill = collateralSkill, + skill = collateralSkill } -extension:addCards({ - collateral, - collateral:clone(Card.Club, 13), -}) +extension:addCards({ collateral, collateral:clone(Card.Club, 13) }) -local exNihiloSkill = fk.CreateActiveSkill{ +local exNihiloSkill = fk.CreateActiveSkill { name = "ex_nihilo_skill", mod_target_filter = function(self, to_select, selected, user, card, distance_limited) - return true + local player = Fk:currentRoom():getPlayerById(to_select) + local from = Fk:currentRoom():getPlayerById(user) + return not (card and from:isProhibited(player, card)) end, can_use = function(self, player, card) return not player:isProhibited(player, card) @@ -393,68 +349,57 @@ local exNihiloSkill = fk.CreateActiveSkill{ end, on_effect = function(self, room, effect) local target = room:getPlayerById(effect.to) - if target.dead then return end + if target.dead then + return + end target:drawCards(2, "ex_nihilo") end } -local exNihilo = fk.CreateTrickCard{ +local exNihilo = fk.CreateTrickCard { name = "ex_nihilo", suit = Card.Heart, number = 7, - skill = exNihiloSkill, + skill = exNihiloSkill } -extension:addCards({ - exNihilo, - exNihilo:clone(Card.Heart, 8), - exNihilo:clone(Card.Heart, 9), - exNihilo:clone(Card.Heart, 11), -}) +extension:addCards({ exNihilo, exNihilo:clone(Card.Heart, 8), exNihilo:clone(Card.Heart, 9), + exNihilo:clone(Card.Heart, 11) }) -local nullificationSkill = fk.CreateActiveSkill{ +local nullificationSkill = fk.CreateActiveSkill { name = "nullification_skill", can_use = function() return false end, - on_use = function() RoomInstance:delay(1200) end, + on_use = function() + RoomInstance:delay(1200) + end, on_effect = function(self, room, effect) if effect.responseToEvent then effect.responseToEvent.isCancellOut = true end end } -local nullification = fk.CreateTrickCard{ +local nullification = fk.CreateTrickCard { name = "nullification", suit = Card.Spade, number = 11, - skill = nullificationSkill, + skill = nullificationSkill } -extension:addCards({ - nullification, +extension:addCards({ nullification, nullification:clone(Card.Club, 12), nullification:clone(Card.Club, 13), + nullification:clone(Card.Diamond, 12) }) - nullification:clone(Card.Club, 12), - nullification:clone(Card.Club, 13), - - nullification:clone(Card.Diamond, 12), -}) - -local savageAssaultSkill = fk.CreateActiveSkill{ +local savageAssaultSkill = fk.CreateActiveSkill { name = "savage_assault_skill", can_use = Util.AoeCanUse, on_use = Util.AoeOnUse, mod_target_filter = function(self, to_select, selected, user, card, distance_limited) - return user ~= to_select + local player = Fk:currentRoom():getPlayerById(to_select) + local from = Fk:currentRoom():getPlayerById(user) + return user ~= to_select and not (card and from:isProhibited(player, card)) end, on_effect = function(self, room, effect) - local cardResponded = room:askForResponse(room:getPlayerById(effect.to), 'slash', nil, nil, false, nil, effect) - - if cardResponded then - room:responseCard({ - from = effect.to, - card = cardResponded, - responseToEvent = effect, - }) + if room:askForResponse(room:getPlayerById(effect.to), 'slash', nil, nil, false, nil, effect) then else room:damage({ from = room:getPlayerById(effect.from), @@ -462,42 +407,33 @@ local savageAssaultSkill = fk.CreateActiveSkill{ card = effect.card, damage = 1, damageType = fk.NormalDamage, - skillName = self.name, + skillName = self.name }) end end } -local savageAssault = fk.CreateTrickCard{ +local savageAssault = fk.CreateTrickCard { name = "savage_assault", suit = Card.Spade, number = 7, is_damage_card = true, multiple_targets = true, - skill = savageAssaultSkill, + skill = savageAssaultSkill } -extension:addCards({ - savageAssault, - savageAssault:clone(Card.Spade, 13), - savageAssault:clone(Card.Club, 7), -}) +extension:addCards({ savageAssault, savageAssault:clone(Card.Spade, 13), savageAssault:clone(Card.Club, 7) }) -local archeryAttackSkill = fk.CreateActiveSkill{ +local archeryAttackSkill = fk.CreateActiveSkill { name = "archery_attack_skill", can_use = Util.AoeCanUse, on_use = Util.AoeOnUse, mod_target_filter = function(self, to_select, selected, user, card, distance_limited) - return user ~= to_select + local player = Fk:currentRoom():getPlayerById(to_select) + local from = Fk:currentRoom():getPlayerById(user) + return user ~= to_select and not (card and from:isProhibited(player, card)) end, on_effect = function(self, room, effect) - local cardResponded = room:askForResponse(room:getPlayerById(effect.to), 'jink', nil, nil, false, nil, effect) - - if cardResponded then - room:responseCard({ - from = effect.to, - card = cardResponded, - responseToEvent = effect, - }) + if room:askForResponse(room:getPlayerById(effect.to), 'jink', nil, nil, false, nil, effect) then else room:damage({ from = room:getPlayerById(effect.from), @@ -505,30 +441,30 @@ local archeryAttackSkill = fk.CreateActiveSkill{ card = effect.card, damage = 1, damageType = fk.NormalDamage, - skillName = self.name, + skillName = self.name }) end end } -local archeryAttack = fk.CreateTrickCard{ +local archeryAttack = fk.CreateTrickCard { name = "archery_attack", suit = Card.Heart, number = 1, is_damage_card = true, multiple_targets = true, - skill = archeryAttackSkill, + skill = archeryAttackSkill } -extension:addCards({ - archeryAttack, -}) +extension:addCards({ archeryAttack }) -local godSalvationSkill = fk.CreateActiveSkill{ +local godSalvationSkill = fk.CreateActiveSkill { name = "god_salvation_skill", can_use = Util.GlobalCanUse, on_use = Util.GlobalOnUse, mod_target_filter = function(self, to_select, selected, user, card, distance_limited) - return true + local player = Fk:currentRoom():getPlayerById(to_select) + local from = Fk:currentRoom():getPlayerById(user) + return not (card and from:isProhibited(player, card)) end, about_to_effect = function(self, room, effect) if not room:getPlayerById(effect.to):isWounded() then @@ -544,29 +480,29 @@ local godSalvationSkill = fk.CreateActiveSkill{ num = 1, recoverBy = player, card = effect.card, - skillName = self.name, + skillName = self.name }) end end } -local godSalvation = fk.CreateTrickCard{ +local godSalvation = fk.CreateTrickCard { name = "god_salvation", suit = Card.Heart, number = 1, multiple_targets = true, - skill = godSalvationSkill, + skill = godSalvationSkill } -extension:addCards({ - godSalvation, -}) +extension:addCards({ godSalvation }) -local amazingGraceSkill = fk.CreateActiveSkill{ +local amazingGraceSkill = fk.CreateActiveSkill { name = "amazing_grace_skill", can_use = Util.GlobalCanUse, on_use = Util.GlobalOnUse, mod_target_filter = function(self, to_select, selected, user, card, distance_limited) - return true + local player = Fk:currentRoom():getPlayerById(to_select) + local from = Fk:currentRoom():getPlayerById(user) + return not (card and from:isProhibited(player, card)) end, on_effect = function(self, room, effect) local to = room:getPlayerById(effect.to) @@ -581,10 +517,13 @@ local amazingGraceSkill = fk.CreateActiveSkill{ end } -local amazingGraceAction = fk.CreateTriggerSkill{ +local amazingGraceAction = fk.CreateTriggerSkill { name = "amazing_grace_action", global = true, - priority = { [fk.BeforeCardUseEffect] = 0, [fk.CardUseFinished] = 10 }, -- game rule + priority = { + [fk.BeforeCardUseEffect] = 0, + [fk.CardUseFinished] = 10 + }, -- game rule events = { fk.BeforeCardUseEffect, fk.CardUseFinished }, can_trigger = function(self, event, target, player, data) local frameFilled = data.extra_data and data.extra_data.AGFilled @@ -601,7 +540,7 @@ local amazingGraceAction = fk.CreateTriggerSkill{ room:moveCards({ ids = toDisplay, toArea = Card.Processing, - moveReason = fk.ReasonPut, + moveReason = fk.ReasonPut }) table.forEach(room.players, function(p) @@ -624,34 +563,33 @@ local amazingGraceAction = fk.CreateTriggerSkill{ room:moveCards({ ids = toDiscard, toArea = Card.DiscardPile, - moveReason = fk.ReasonPutIntoDiscardPile, + moveReason = fk.ReasonPutIntoDiscardPile }) end end data.extra_data.AGFilled = nil end - end, + end } Fk:addSkill(amazingGraceAction) -local amazingGrace = fk.CreateTrickCard{ +local amazingGrace = fk.CreateTrickCard { name = "amazing_grace", suit = Card.Heart, number = 3, multiple_targets = true, - skill = amazingGraceSkill, + skill = amazingGraceSkill } -extension:addCards({ - amazingGrace, - amazingGrace:clone(Card.Heart, 4), -}) +extension:addCards({ amazingGrace, amazingGrace:clone(Card.Heart, 4) }) -local lightningSkill = fk.CreateActiveSkill{ +local lightningSkill = fk.CreateActiveSkill { name = "lightning_skill", mod_target_filter = function(self, to_select, selected, user, card, distance_limited) - return true + local player = Fk:currentRoom():getPlayerById(to_select) + local from = Fk:currentRoom():getPlayerById(user) + return not (card and from:isProhibited(player, card)) end, can_use = function(self, player, card) return not player:isProhibited(player, card) @@ -666,20 +604,19 @@ local lightningSkill = fk.CreateActiveSkill{ local judge = { who = to, reason = "lightning", - pattern = ".|2~9|spade", + pattern = ".|.|^spade;.|1,10,11,12,13|spade" } room:judge(judge) - local result = judge.card - if result.suit == Card.Spade and result.number >= 2 and result.number <= 9 then - room:damage{ + if judge.card.suit == Card.Spade and judge.card.number >= 2 and judge.card.number <= 9 then + room:damage { to = to, damage = 3, card = effect.card, damageType = fk.ThunderDamage, - skillName = self.name, + skillName = self.name } - room:moveCards{ + room:moveCards { ids = { effect.cardId }, toArea = Card.DiscardPile, moveReason = fk.ReasonUse @@ -693,41 +630,41 @@ local lightningSkill = fk.CreateActiveSkill{ local nextp = to repeat nextp = nextp:getNextAlive(true) - if nextp == to then break end + if nextp == to then + break + end until not nextp:hasDelayedTrick("lightning") and not nextp:isProhibited(nextp, effect.card) - if effect.card:isVirtual() then nextp:addVirtualEquip(effect.card) end - room:moveCards{ + room:moveCards { ids = room:getSubcardsByRule(effect.card, { Card.Processing }), to = nextp.id, toArea = Card.PlayerJudge, moveReason = fk.ReasonPut } - end, + end } -local lightning = fk.CreateDelayedTrickCard{ +local lightning = fk.CreateDelayedTrickCard { name = "lightning", suit = Card.Spade, number = 1, - skill = lightningSkill, + skill = lightningSkill } -extension:addCards({ - lightning, - lightning:clone(Card.Heart, 12), -}) +extension:addCards({ lightning, lightning:clone(Card.Heart, 12) }) -local indulgenceSkill = fk.CreateActiveSkill{ +local indulgenceSkill = fk.CreateActiveSkill { name = "indulgence_skill", mod_target_filter = function(self, to_select, selected, user, card, distance_limited) - return user ~= to_select + local player = Fk:currentRoom():getPlayerById(to_select) + local from = Fk:currentRoom():getPlayerById(user) + return user ~= to_select and not (card and from:isProhibited(player, card)) end, target_filter = function(self, to_select, selected, _, card) - return #selected == 0 and self:modTargetFilter(to_select, selected, Self.id, card, true) + return #selected < 1 and self:modTargetFilter(to_select, selected, Self.id, card, true) end, target_num = 1, on_effect = function(self, room, effect) @@ -735,74 +672,65 @@ local indulgenceSkill = fk.CreateActiveSkill{ local judge = { who = to, reason = "indulgence", - pattern = ".|.|spade,club,diamond", + pattern = ".|.|heart" } room:judge(judge) - local result = judge.card - if result.suit ~= Card.Heart then + if judge.card.suit ~= Card.Heart then to:skip(Player.Play) end self:onNullified(room, effect) end, on_nullified = function(self, room, effect) - room:moveCards{ + room:moveCards { ids = room:getSubcardsByRule(effect.card, { Card.Processing }), toArea = Card.DiscardPile, moveReason = fk.ReasonUse } - end, + end } -local indulgence = fk.CreateDelayedTrickCard{ +local indulgence = fk.CreateDelayedTrickCard { name = "indulgence", suit = Card.Spade, number = 6, - skill = indulgenceSkill, + skill = indulgenceSkill } -extension:addCards({ - indulgence, - indulgence:clone(Card.Club, 6), - indulgence:clone(Card.Heart, 6), -}) +extension:addCards({ indulgence, indulgence:clone(Card.Club, 6), indulgence:clone(Card.Heart, 6) }) -local crossbowAudio = fk.CreateTriggerSkill{ +local crossbowAudio = fk.CreateTriggerSkill { name = "#crossbowAudio", - refresh_events = {fk.CardUsing}, + refresh_events = { fk.CardUsing }, can_refresh = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name) and player.phase == Player.Play and - data.card.trueName == "slash" and player:usedCardTimes("slash", Player.HistoryPhase) > 1 + return target == player and player:hasSkill(self.name) and player.phase == Player.Play and data.card.trueName == + "slash" and player:usedCardTimes("slash", Player.HistoryPhase) > 1 end, on_refresh = function(self, event, target, player, data) local room = player.room room:broadcastPlaySound("./packages/standard_cards/audio/card/crossbow") room:setEmotion(player, "./packages/standard_cards/image/anim/crossbow") - end, + end } -local crossbowSkill = fk.CreateTargetModSkill{ +local crossbowSkill = fk.CreateTargetModSkill { name = "#crossbow_skill", attached_equip = "crossbow", bypass_times = function(self, player, skill, scope) - if player:hasSkill(self.name) and skill.trueName == "slash_skill" - and scope == Player.HistoryPhase then + if player:hasSkill(self.name) and skill.trueName == "slash_skill" and scope == Player.HistoryPhase then return true end - end, + end } crossbowSkill:addRelatedSkill(crossbowAudio) Fk:addSkill(crossbowSkill) -local crossbow = fk.CreateWeapon{ +local crossbow = fk.CreateWeapon { name = "crossbow", suit = Card.Club, number = 1, attack_range = 1, - equip_skill = crossbowSkill, + equip_skill = crossbowSkill } -extension:addCards({ - crossbow, - crossbow:clone(Card.Diamond, 1), -}) +extension:addCards({ crossbow, crossbow:clone(Card.Diamond, 1) }) fk.MarkArmorNullified = "mark__armor_nullified" @@ -821,14 +749,13 @@ local armorInvalidity = fk.CreateInvaliditySkill { } Fk:addSkill(armorInvalidity) -local qingGangSkill = fk.CreateTriggerSkill{ +local qingGangSkill = fk.CreateTriggerSkill { name = "#qinggang_sword_skill", attached_equip = "qinggang_sword", frequency = Skill.Compulsory, events = { fk.TargetSpecified }, can_trigger = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name) and - data.card and data.card.trueName == "slash" + return target == player and player:hasSkill(self.name) and data.card and data.card.trueName == "slash" end, on_use = function(self, event, target, player, data) local room = player.room @@ -836,7 +763,8 @@ local qingGangSkill = fk.CreateTriggerSkill{ data.extra_data = data.extra_data or {} data.extra_data.qinggangNullified = data.extra_data.qinggangNullified or {} - data.extra_data.qinggangNullified[tostring(data.to)] = (data.extra_data.qinggangNullified[tostring(data.to)] or 0) + 1 + data.extra_data.qinggangNullified[tostring(data.to)] = + (data.extra_data.qinggangNullified[tostring(data.to)] or 0) + 1 end, refresh_events = { fk.CardUseFinished }, @@ -853,64 +781,63 @@ local qingGangSkill = fk.CreateTriggerSkill{ end data.qinggangNullified = nil - end, + end } Fk:addSkill(qingGangSkill) -local qingGang = fk.CreateWeapon{ +local qingGang = fk.CreateWeapon { name = "qinggang_sword", suit = Card.Spade, number = 6, attack_range = 2, - equip_skill = qingGangSkill, + equip_skill = qingGangSkill } -extension:addCards({ - qingGang, -}) +extension:addCards({ qingGang }) -local iceSwordSkill = fk.CreateTriggerSkill{ +local iceSwordSkill = fk.CreateTriggerSkill { name = "#ice_sword_skill", attached_equip = "ice_sword", - events = {fk.DamageCaused}, + events = { fk.DamageCaused }, can_trigger = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name) and (not data.chain) and - data.card and data.card.trueName == "slash" and not data.to:isNude() + return + target == player and player:hasSkill(self.name) and (not data.chain) and data.card and data.card.trueName == + "slash" and not data.to:isNude() end, on_use = function(self, event, target, player, data) local room = player.room local to = data.to for i = 1, 2 do - if player.dead or to.dead or to:isNude() then break end + if player.dead or to.dead or to:isNude() then + break + end local card = room:askForCardChosen(player, to, "he", self.name) - room:throwCard({card}, self.name, to, player) + room:throwCard({ card }, self.name, to, player) end return true end } Fk:addSkill(iceSwordSkill) -local iceSword = fk.CreateWeapon{ +local iceSword = fk.CreateWeapon { name = "ice_sword", suit = Card.Spade, number = 2, attack_range = 2, - equip_skill = iceSwordSkill, + equip_skill = iceSwordSkill } -extension:addCards({ - iceSword, -}) +extension:addCards({ iceSword }) -local doubleSwordsSkill = fk.CreateTriggerSkill{ +local doubleSwordsSkill = fk.CreateTriggerSkill { name = "#double_swords_skill", attached_equip = "double_swords", - events = {fk.TargetSpecified}, + events = { fk.TargetSpecified }, can_trigger = function(self, event, target, player, data) - if target == player and player:hasSkill(self.name) and - data.card and data.card.trueName == "slash" then + if target == player and player:hasSkill(self.name) and data.card and data.card.trueName == "slash" then local target = player.room:getPlayerById(data.to) - return target.gender ~= player.gender and target.gender ~= General.Agender and player.gender ~= General.Agender + return target.gender ~= player.gender and target.gender ~= General.Agender and player.gender ~= + General.Agender end end, on_use = function(self, event, target, player, data) @@ -919,66 +846,120 @@ local doubleSwordsSkill = fk.CreateTriggerSkill{ if to:isKongcheng() then player:drawCards(1, self.name) else - local result = room:askForDiscard(to, 1, 1, false, self.name, true, ".", "#double_swords-invoke:"..player.id) + local result = room:askForDiscard(to, 1, 1, false, self.name, true, ".", + "#double_swords-invoke:" .. player.id) if #result == 0 then player:drawCards(1, self.name) end end - end, + end } Fk:addSkill(doubleSwordsSkill) -local doubleSwords = fk.CreateWeapon{ +local doubleSwords = fk.CreateWeapon { name = "double_swords", suit = Card.Spade, number = 2, attack_range = 2, - equip_skill = doubleSwordsSkill, + equip_skill = doubleSwordsSkill } -extension:addCards({ - doubleSwords, -}) +extension:addCards({ doubleSwords }) -local bladeSkill = fk.CreateTriggerSkill{ +local bladeSkill = fk.CreateTriggerSkill { name = "#blade_skill", attached_equip = "blade", - events = {fk.CardEffectCancelledOut}, + events = { fk.CardEffectCancelledOut }, can_trigger = function(self, event, target, player, data) - return player:hasSkill(self.name) and data.from == player.id and data.card.trueName == "slash" and not player.room:getPlayerById(data.to).dead + return player:hasSkill(self.name) and data.from == player.id and data.card.trueName == "slash" and + not player.room:getPlayerById(data.to).dead end, on_cost = function(self, event, target, player, data) - local room = player.room - local use = room:askForUseCard(player, "slash", nil, "#blade_slash:" .. data.to, - true, { must_targets = {data.to}, exclusive_targets = {data.to}, bypass_distances = true, bypass_times = true }) - if use then - use.extraUse = true + local extra_data = { + must_targets = { data.to }, + exclusive_targets = { data.to }, + bypass_distances = true, + bypass_times = true + } + if extra_data then + if extra_data.bypass_distances then + player.room:setPlayerMark(player, MarkEnum.BypassDistancesLimit .. "", 1) + end + if extra_data.bypass_times ~= false then + player.room:setPlayerMark(player, MarkEnum.BypassTimesLimit .. "-tmp", 1) + end + fk.useMustTargets = extra_data.must_targets + end + local command = "AskForUseCard" + player.room:notifyMoveFocus(player, "slash") + local pattern = "slash" + local prompt = "#blade_slash:" .. data.to + + local useData = { + user = player, + cardName = "slash", + pattern = pattern, + extraData = extra_data + } + local use = nil + if self.cost_data == nil then + player.room.logic:trigger(fk.AskForCardUse, player, useData) + if type(useData.result) == "table" then + useData = useData.result + useData.extraUse = extra_data ~= nil + use = useData + end + end + local usedata = { "slash", pattern, prompt, true, extra_data } + if use == nil then + Fk.currentResponsePattern = pattern + local result = player.room:doRequest(player, command, json.encode(usedata)) + Fk.currentResponsePattern = nil + if result ~= "" then + result = player.room:handleUseCardReply(player, result) + result.extraUse = extra_data ~= nil + use = result + end + end + fk.useMustTargets = nil + player.room:setPlayerMark(player, MarkEnum.BypassDistancesLimit .. "-tmp", 0) + player.room:setPlayerMark(player, MarkEnum.BypassTimesLimit .. "-tmp", 0) + if use + then self.cost_data = use return true end end, on_use = function(self, event, target, player, data) - player.room:useCard(self.cost_data) - end, + while true do + local use = self.cost_data + player.room:useCard(use) + if use.breakEvent and self:onCost(event, target, player, data) then + else + break + end + end + self.cost_data = nil + end } Fk:addSkill(bladeSkill) -local blade = fk.CreateWeapon{ +local blade = fk.CreateWeapon { name = "blade", suit = Card.Spade, number = 5, attack_range = 3, - equip_skill = bladeSkill, + equip_skill = bladeSkill } -extension:addCards({ - blade, -}) +extension:addCards({ blade }) -local spearSkill = fk.CreateViewAsSkill{ +local spearSkill = fk.CreateViewAsSkill { name = "spear_skill", attached_equip = "spear", pattern = "slash", card_filter = function(self, to_select, selected) - if #selected == 2 then return false end + if #selected == 2 then + return false + end return Fk:currentRoom():getCardArea(to_select) ~= Player.Equip end, view_as = function(self, cards) @@ -989,37 +970,37 @@ local spearSkill = fk.CreateViewAsSkill{ c.skillName = "spear" c:addSubcards(cards) return c - end, + end } Fk:addSkill(spearSkill) -local spear = fk.CreateWeapon{ +local spear = fk.CreateWeapon { name = "spear", suit = Card.Spade, number = 12, attack_range = 3, - equip_skill = spearSkill, + equip_skill = spearSkill } -extension:addCards({ - spear, -}) +extension:addCards({ spear }) -local axeSkill = fk.CreateTriggerSkill{ +local axeSkill = fk.CreateTriggerSkill { name = "#axe_skill", attached_equip = "axe", - events = {fk.CardEffectCancelledOut}, + events = { fk.CardEffectCancelledOut }, can_trigger = function(self, event, target, player, data) - return player:hasSkill(self.name) and data.from == player.id and data.card.trueName == "slash" and not player.room:getPlayerById(data.to).dead + return player:hasSkill(self.name) and data.from == player.id and data.card.trueName == "slash" and + not player.room:getPlayerById(data.to).dead end, on_cost = function(self, event, target, player, data) local room = player.room local pattern if player:getEquipment(Card.SubtypeWeapon) then - pattern = ".|.|.|.|.|.|^"..tostring(player:getEquipment(Card.SubtypeWeapon)) + pattern = ".|.|.|.|.|.|^" .. tostring(player:getEquipment(Card.SubtypeWeapon)) else pattern = "." end - local cards = room:askForDiscard(player, 2, 2, true, self.name, true, pattern, "#axe-invoke::"..data.to, true) + player.axe_to = data.to + local cards = room:askForDiscard(player, 2, 2, true, self.name, true, pattern, "#axe-invoke::" .. data.to, true) if #cards > 0 then self.cost_data = cards return true @@ -1029,73 +1010,70 @@ local axeSkill = fk.CreateTriggerSkill{ local room = player.room room:throwCard(self.cost_data, "axe", player, player) return true - end, + end } Fk:addSkill(axeSkill) -local axe = fk.CreateWeapon{ +local axe = fk.CreateWeapon { name = "axe", suit = Card.Diamond, number = 5, attack_range = 3, - equip_skill = axeSkill, + equip_skill = axeSkill } -extension:addCards({ - axe, -}) +extension:addCards({ axe }) -local halberdAudio = fk.CreateTriggerSkill{ +local halberdAudio = fk.CreateTriggerSkill { name = "#halberdAudio", - refresh_events = {fk.CardUsing}, + refresh_events = { fk.CardUsing }, can_refresh = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name) and - data.card.trueName == "slash" and #TargetGroup:getRealTargets(data.tos) > 1 + return target == player and player:hasSkill(self.name) and data.card.trueName == "slash" and + #TargetGroup:getRealTargets(data.tos) > 1 end, on_refresh = function(self, event, target, player, data) local room = player.room room:broadcastPlaySound("./packages/standard_cards/audio/card/halberd") room:setEmotion(player, "./packages/standard_cards/image/anim/halberd") - end, + end } -local halberdSkill = fk.CreateTargetModSkill{ +local halberdSkill = fk.CreateTargetModSkill { name = "#halberd_skill", attached_equip = "halberd", extra_target_func = function(self, player, skill, card) - if player:hasSkill(self.name) and skill.trueName == "slash_skill" then - local cards = card:isVirtual() and card.subcards or {card.id} + if player:hasSkill(self.name) and skill.trueName == "slash_skill" and card then + local cards = card:isVirtual() and card.subcards or { card.id } local handcards = player:getCardIds(Player.Hand) - if #cards == #handcards and table.every(cards, function(id) return table.contains(handcards, id) end) then + if #cards == #handcards and table.every(cards, function(id) + return table.contains(handcards, id) + end) then return 2 end end - end, + end } halberdSkill:addRelatedSkill(halberdAudio) Fk:addSkill(halberdSkill) -local halberd = fk.CreateWeapon{ +local halberd = fk.CreateWeapon { name = "halberd", suit = Card.Diamond, number = 12, attack_range = 4, - equip_skill = halberdSkill, + equip_skill = halberdSkill } -extension:addCards({ - halberd, -}) +extension:addCards({ halberd }) -local kylinBowSkill = fk.CreateTriggerSkill{ +local kylinBowSkill = fk.CreateTriggerSkill { name = "#kylin_bow_skill", attached_equip = "kylin_bow", - events = {fk.DamageCaused}, + events = { fk.DamageCaused }, can_trigger = function(self, event, target, player, data) - local ret = target == player and player:hasSkill(self.name) and - data.card and data.card.trueName == "slash" and (not data.chain) + local ret = target == player and player:hasSkill(self.name) and data.card and data.card.trueName == "slash" and + (not data.chain) if ret then ---@type ServerPlayer local to = data.to - return to:getEquipment(Card.SubtypeDefensiveRide) or - to:getEquipment(Card.SubtypeOffensiveRide) + return to:getEquipment(Card.SubtypeDefensiveRide) or to:getEquipment(Card.SubtypeOffensiveRide) end end, on_use = function(self, event, target, player, data) @@ -1108,7 +1086,9 @@ local kylinBowSkill = fk.CreateTriggerSkill{ if to:getEquipment(Card.SubtypeOffensiveRide) then table.insert(ride_tab, "-1") end - if #ride_tab == 0 then return end + if #ride_tab == 0 then + return + end local choice = room:askForChoice(player, ride_tab, self.name) if choice == "+1" then room:throwCard(to:getEquipment(Card.SubtypeDefensiveRide), self.name, to, player) @@ -1118,32 +1098,31 @@ local kylinBowSkill = fk.CreateTriggerSkill{ end } Fk:addSkill(kylinBowSkill) -local kylinBow = fk.CreateWeapon{ +local kylinBow = fk.CreateWeapon { name = "kylin_bow", suit = Card.Heart, number = 5, attack_range = 5, - equip_skill = kylinBowSkill, + equip_skill = kylinBowSkill } -extension:addCards({ - kylinBow, -}) +extension:addCards({ kylinBow }) -local eightDiagramSkill = fk.CreateTriggerSkill{ +local eightDiagramSkill = fk.CreateTriggerSkill { name = "#eight_diagram_skill", attached_equip = "eight_diagram", - events = {fk.AskForCardUse, fk.AskForCardResponse}, + events = { fk.AskForCardUse, fk.AskForCardResponse }, can_trigger = function(self, event, target, player, data) return target == player and player:hasSkill(self.name) and - (data.cardName == "jink" or (data.pattern and Exppattern:Parse(data.pattern):matchExp("jink|0|nosuit|none"))) + (data.cardName == "jink" or + (data.pattern and Exppattern:Parse(data.pattern):matchExp("jink|0|nosuit|none"))) end, on_use = function(self, event, target, player, data) local room = player.room local judgeData = { who = player, reason = self.name, - pattern = ".|.|heart,diamond", + pattern = ".|.|heart,diamond" } room:judge(judgeData) @@ -1151,7 +1130,7 @@ local eightDiagramSkill = fk.CreateTriggerSkill{ if event == fk.AskForCardUse then data.result = { from = player.id, - card = Fk:cloneCard('jink'), + card = Fk:cloneCard('jink') } data.result.card.skillName = "eight_diagram" @@ -1163,49 +1142,44 @@ local eightDiagramSkill = fk.CreateTriggerSkill{ data.result = Fk:cloneCard('jink') data.result.skillName = "eight_diagram" end - return true end end } Fk:addSkill(eightDiagramSkill) -local eightDiagram = fk.CreateArmor{ +local eightDiagram = fk.CreateArmor { name = "eight_diagram", suit = Card.Spade, number = 2, - equip_skill = eightDiagramSkill, + equip_skill = eightDiagramSkill } -extension:addCards({ - eightDiagram, - eightDiagram:clone(Card.Club, 2), -}) +extension:addCards({ eightDiagram, eightDiagram:clone(Card.Club, 2) }) -local niohShieldSkill = fk.CreateTriggerSkill{ +local niohShieldSkill = fk.CreateTriggerSkill { name = "#nioh_shield_skill", attached_equip = "nioh_shield", frequency = Skill.Compulsory, - events = {fk.PreCardEffect}, + events = { fk.PreCardEffect }, can_trigger = function(self, event, target, player, data) - local effect = data ---@type CardEffectEvent - return player.id == effect.to and player:hasSkill(self.name) and - effect.card.trueName == "slash" and effect.card.color == Card.Black + return player.id == data.to and player:hasSkill(self.name) and data.card.trueName == "slash" and + data.card.color == Card.Black end, - on_use = function() return true end, + on_use = function() + return true + end } Fk:addSkill(niohShieldSkill) -local niohShield = fk.CreateArmor{ +local niohShield = fk.CreateArmor { name = "nioh_shield", suit = Card.Club, number = 2, - equip_skill = niohShieldSkill, + equip_skill = niohShieldSkill } -extension:addCards({ - niohShield, -}) +extension:addCards({ niohShield }) -local horseSkill = fk.CreateDistanceSkill{ +local horseSkill = fk.CreateDistanceSkill { name = "horse_skill", global = true, correct_func = function(self, from, to) @@ -1217,71 +1191,59 @@ local horseSkill = fk.CreateDistanceSkill{ ret = ret + 1 end return ret - end, + end } if not Fk.skills["horse_skill"] then Fk:addSkill(horseSkill) end -local diLu = fk.CreateDefensiveRide{ +local diLu = fk.CreateDefensiveRide { name = "dilu", suit = Card.Club, - number = 5, + number = 5 } -extension:addCards({ - diLu, -}) +extension:addCards({ diLu }) -local jueYing = fk.CreateDefensiveRide{ +local jueYing = fk.CreateDefensiveRide { name = "jueying", suit = Card.Spade, - number = 5, + number = 5 } -extension:addCards({ - jueYing, -}) +extension:addCards({ jueYing }) -local zhuaHuangFeiDian = fk.CreateDefensiveRide{ +local zhuaHuangFeiDian = fk.CreateDefensiveRide { name = "zhuahuangfeidian", suit = Card.Heart, - number = 13, + number = 13 } -extension:addCards({ - zhuaHuangFeiDian, -}) +extension:addCards({ zhuaHuangFeiDian }) -local chiTu = fk.CreateOffensiveRide{ +local chiTu = fk.CreateOffensiveRide { name = "chitu", suit = Card.Heart, - number = 5, + number = 5 } -extension:addCards({ - chiTu, -}) +extension:addCards({ chiTu }) -local daYuan = fk.CreateOffensiveRide{ +local daYuan = fk.CreateOffensiveRide { name = "dayuan", suit = Card.Spade, - number = 13, + number = 13 } -extension:addCards({ - daYuan, -}) +extension:addCards({ daYuan }) -local ziXing = fk.CreateOffensiveRide{ +local ziXing = fk.CreateOffensiveRide { name = "zixing", suit = Card.Diamond, - number = 13, + number = 13 } -extension:addCards({ - ziXing, -}) +extension:addCards({ ziXing }) dofile "packages/standard_cards/i18n/init.lua" diff --git a/packages/standard_cards/standard_cards_ai.lua b/packages/standard_cards/standard_cards_ai.lua new file mode 100644 index 00000000..4404f596 --- /dev/null +++ b/packages/standard_cards/standard_cards_ai.lua @@ -0,0 +1,407 @@ +fk.ai_card.slash = { + intention = 100, -- 身份值 + value = 4, -- 卡牌价值 + priority = 2.5 -- 使用优先值 +} +fk.ai_card.peach = { + intention = -150, + value = 10, + priority = 0.5 +} +fk.ai_card.dismantlement = { + intention = function(self, card, from) + if #self.player.player_cards[Player.Judge] < 1 then + return 80 + elseif fk.ai_role[from.id] == "neutral" then + return 30 + end + end, + value = 3.5, + priority = 10.5 +} +fk.ai_card.snatch = { + intention = function(self, card, from) + if #self.player.player_cards[Player.Judge] < 1 then + return 80 + elseif fk.ai_role[from.id] == "neutral" then + return 30 + end + end, + value = 4.5, + priority = 10.4 +} +fk.ai_card.duel = { + intention = 120, + value = 4.5, + priority = 3.5 +} +fk.ai_card.collateral = { + intention = 20, + value = 3, + priority = 4.5 +} +fk.ai_card.ex_nihilo = { + intention = -200, + value = 8, + priority = 10 +} +fk.ai_card.savage_assault = { + intention = 20, + value = 2, + priority = 4 +} +fk.ai_card.archery_attack = { + intention = 30, + value = 2, + priority = 3 +} +fk.ai_card.god_salvation = { + intention = function(self, card, from) + if self.player.hp ~= self.player.maxHp then + return -45 + end + end, + value = 1.5, + priority = 2 +} +fk.ai_card.amazing_grace = { + intention = -30, + value = 2, + priority = 2 +} +fk.ai_card.indulgence = { + intention = 150, + value = -1, + priority = 2 +} + +local function slashEeffect(slash, to) + for _, s in ipairs(to:getAllSkills()) do + if s.name == "#vine_skill" then + if slash.name == "slash" then + return + end + end + if s.name == "#nioh_shield_skill" then + if slash.color == Card.Black then + return + end + end + end + return true +end + +fk.ai_use_play.slash = 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 slashEeffect(card, p) then + self.use_id = card.id + table.insert(self.use_tos, p.id) + end + end +end + +fk.ai_askuse_card["#slash-jink"] = function(self, pattern, prompt, cancelable, extra_data) + local act = self:getActives(pattern) + if tonumber(prompt:split(":")[4]) > #act then + return + end + local cards = + table.map( + self.player:getCardIds("&he"), + function(id) + return Fk:getCardById(id) + end + ) + self:sortValue(cards) + for _, sth in ipairs(act) do + if sth:isInstanceOf(Card) then + self.use_id = sth.id + break + else + local selected = {} + for _, c in ipairs(cards) do + if sth.cardFilter(sth, c.id, selected) then + table.insert(selected, c.id) + end + end + local tc = sth.viewAs(sth, selected) + if tc and tc:matchPattern(pattern) then + self.use_id = + json.encode { + skill = sth.name, + subcards = selected + } + break + end + end + end +end + +fk.ai_askuse_card["#slash-jinks"] = fk.ai_askuse_card["#slash-jink"] + +fk.ai_use_play.snatch = function(self, card) + for _, p in ipairs(self.friends_noself) do + if card.skill:targetFilter(p.id, self.use_tos, {}, card) and #p:getCardIds("j") > 0 then + self.use_id = card.id + 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 #p:getCardIds("he") > 0 then + self.use_id = card.id + table.insert(self.use_tos, p.id) + end + end +end + +fk.ai_nullification.snatch = function(self, card, to, from, positive) + if positive then + if self:isFriend(to) and not self:isFriend(from) and fk.ai_role[from.id] ~= "neutral" 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:isEnemie(to) and self:isEnemie(from) then + if #self.avail_cards > 1 or self:isWeak(to) then + self.use_id = self.avail_cards[1] + end + end + end +end + +fk.ai_use_play.dismantlement = function(self, card) + for _, p in ipairs(self.friends_noself) do + if card.skill:targetFilter(p.id, self.use_tos, {}, card) and #p:getCardIds("j") > 0 then + self.use_id = card.id + 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 #p:getCardIds("he") > 0 then + self.use_id = card.id + table.insert(self.use_tos, p.id) + end + end +end + +fk.ai_nullification.dismantlement = function(self, card, to, from, positive) + if positive then + if self:isFriend(to) and not self:isFriend(from) and fk.ai_role[from.id] ~= "neutral" 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:isEnemie(to) and self:isEnemie(from) then + if #self.avail_cards > 1 or self:isWeak(to) then + self.use_id = self.avail_cards[1] + end + end + end +end + +fk.ai_use_play.indulgence = function(self, card) + self:sort(self.enemies, nil, true) + for _, p in ipairs(self.enemies) do + if card.skill:targetFilter(p.id, self.use_tos, {}, card) then + self.use_id = card.id + table.insert(self.use_tos, p.id) + end + end +end + +fk.ai_nullification.indulgence = 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:isEnemie(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_use_play.collateral = function(self, card) + local max = (card.skill:getMaxTargetNum(self.player, card) - 1) * 2 + self:sort(self.enemies) + for _, p in ipairs(self.enemies) do + if #self.use_tos < max and card.skill:targetFilter(p.id, {}, {}, card) then + for _, pt in ipairs(self.enemies) do + if p ~= pt and p:inMyAttackRange(pt) then + table.insert(self.use_tos, p.id) + table.insert(self.use_tos, pt.id) + self.use_id = card.id + break + end + end + end + end + for _, p in ipairs(self.friends_noself) do + if #self.use_tos < max and card.skill:targetFilter(p.id, {}, {}, card) then + for _, pt in ipairs(self.enemies) do + if p ~= pt and p:inMyAttackRange(pt) then + table.insert(self.use_tos, p.id) + table.insert(self.use_tos, pt.id) + self.use_id = card.id + break + end + end + end + end +end + +fk.ai_nullification.collateral = function(self, card, to, from, positive) + if positive then + if self:isFriend(to) and self:isEnemie(from) 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 + end +end + +fk.ai_nullification.ex_nihilo = function(self, card, to, from, positive) + if positive then + if self:isEnemie(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:isFriend(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_nullification.savage_assault = 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:isEnemie(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_nullification.archery_attack = 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:isEnemie(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_nullification.god_salvation = function(self, card, to, from, positive) + if positive then + if self:isEnemie(to) and to.hp ~= to.maxHp then + if #self.avail_cards > 1 or self:isWeak(to) then + self.use_id = self.avail_cards[1] + end + end + else + if self:isFriend(to) and to.hp ~= to.maxHp 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 + end +end + +fk.ai_use_play.ex_nihilo = function(self, card) + self.use_id = card.id +end + +fk.ai_use_play.lightning = function(self, card) + self.use_id = #self.enemies > #self.friends and card.id +end + +fk.ai_use_play.peach = function(self, card) + if self.command == "PlayCard" then + self.use_id = self.player.hp ~= self.player.maxHp and self.player.hp < #self.player:getCardIds("h") and card.id + else + for _, p in ipairs(self.friends) do + if p.dying then + self.use_id = card.id + self.use_tos = { p.id } + break + end + end + end +end + +fk.ai_use_play.duel = function(self, card) + self:sort(self.enemies) + for _, p in ipairs(self.enemies) do + if card.skill:targetFilter(p.id, self.use_tos, {}, card) then + self.use_id = card.id + table.insert(self.use_tos, p.id) + end + end +end + +fk.ai_skill_invoke["#ice_sword_skill"] = function(self) + local damage = self:eventData("Damage") + return self:isFriend(damage.to) or not self:isWeak(damage.to) and #damage.to:getCardIds("e") > 1 +end + +fk.ai_skill_invoke["#double_swords_skill"] = function(self) + local use = self:eventData("UseCard") + for _, p in ipairs(TargetGroup:getRealTargets(use.tos)) do + if not self:isFriend(p) and self.room:getPlayerById(p).gender ~= self.player.gender then + return true + end + end +end + +fk.ai_dis_card["#double_swords_skill"] = function(self, min_num, num, include_equip, cancelable, pattern, prompt) + local use = self:eventData("UseCard") + return self:isEnemie(use.from) and { self.player:getCardIds("h")[1] } +end + +fk.ai_dis_card["#axe_skill"] = function(self, min_num, num, include_equip, cancelable, pattern, prompt) + local ids = {} + for _, cid in ipairs(self.player:getCardIds("he")) do + if Fk:getCardById(cid):matchPattern(pattern) then + table.insert(ids, cid) + end + if + #ids >= min_num and self:isEnemie(self.player.axe_to) and + (self:isWeak(self.player.axe_to) or #self.player:getCardIds("he") > 3) + then + return ids + end + end +end + +fk.ai_skill_invoke["#kylin_bow_skill"] = function(self) + local damage = self:eventData("Damage") + return not self:isFriend(damage.to) +end + +fk.ai_skill_invoke["#eight_diagram_skill"] = true diff --git a/packages/test/init.lua b/packages/test/init.lua index 95ce37d8..17ab4006 100644 --- a/packages/test/init.lua +++ b/packages/test/init.lua @@ -90,12 +90,6 @@ 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) @@ -130,22 +124,6 @@ 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", From 099ce2d83a8001b7e11df91d658838521eb54e6e Mon Sep 17 00:00:00 2001 From: notify Date: Thu, 21 Sep 2023 23:21:28 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BF=AE=E6=94=B92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 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/SkillButton.qml | 14 +- README.md | 6 - lua/.vscode/settings.json | 14 + lua/client/client_util.lua | 1021 ++++++------ lua/client/i18n/en_US.lua | 2 +- lua/client/i18n/zh_CN.lua | 2 +- lua/core/engine.lua | 12 - lua/core/player.lua | 69 +- lua/fk_ex.lua | 10 - lua/lsp/freekill.lua | 1 - lua/server/ai/ai说明.txt | 45 + lua/server/ai/init.lua | 22 - lua/server/ai/random_ai.lua | 410 ++--- lua/server/ai/trust_ai.lua | 1082 ++++++++++++- lua/server/events/usecard.lua | 132 +- lua/server/gameevent.lua | 7 - lua/server/gamelogic.lua | 765 ++++----- lua/server/room.lua | 1405 ++++++++++------- lua/server/room.lua.rej | 46 + lua/server/serverplayer.lua | 34 +- packages/maneuvering/init.lua | 805 +++++----- packages/maneuvering/maneuvering_ai.lua | 140 ++ packages/standard/game_rule.lua | 143 +- packages/standard/init.lua | 596 +++---- packages/standard/standard_ai.lua | 227 +++ packages/standard_cards/i18n/zh_CN.lua | 17 +- packages/standard_cards/init.lua | 848 +++++----- packages/standard_cards/standard_cards_ai.lua | 407 +++++ packages/test/init.lua | 22 - 37 files changed, 5265 insertions(+), 3174 deletions(-) create mode 100644 lua/.vscode/settings.json create mode 100644 lua/server/ai/ai说明.txt create mode 100644 lua/server/room.lua.rej create mode 100644 packages/maneuvering/maneuvering_ai.lua create mode 100644 packages/standard/standard_ai.lua create mode 100644 packages/standard_cards/standard_cards_ai.lua diff --git a/Fk/Pages/Lobby.qml b/Fk/Pages/Lobby.qml index 317acfdd..7d4fd364 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").arg(roomModel.count) + text: Backend.translate("Room List") } ListView { id: roomList diff --git a/Fk/Pages/Room.qml b/Fk/Pages/Room.qml index 0194157f..a8622381 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 03c32b55..35bb4c5c 100644 --- a/Fk/Pages/RoomLogic.js +++ b/Fk/Pages/RoomLogic.js @@ -1070,31 +1070,6 @@ 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 adda007c..ebf98e67 100644 --- a/Fk/PhotoElement/MarkArea.qml +++ b/Fk/PhotoElement/MarkArea.qml @@ -62,12 +62,7 @@ Item { } if (mark_name.startsWith('@$')) { - let data = mark_extra.split(','); - if (!Object.is(parseInt(data[0]), NaN)) { - params.ids = data.map(s => parseInt(s)); - } else { - params.cardNames = data; - } + params.cardNames = mark_extra.split(','); } 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 d74ea8d3..c41d789a 100644 --- a/Fk/RoomElement/CardItem.qml +++ b/Fk/RoomElement/CardItem.qml @@ -31,13 +31,11 @@ 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 != "") @@ -90,7 +88,6 @@ Item { visible: false } - Image { id: cardItem source: known ? SkinBank.getCardPicture(cid || name) @@ -220,15 +217,6 @@ 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 @@ -236,24 +224,6 @@ 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 09faeb03..2158a9eb 100644 --- a/Fk/RoomElement/Dashboard.qml +++ b/Fk/RoomElement/Dashboard.qml @@ -166,35 +166,17 @@ 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; - } } }); @@ -220,7 +202,6 @@ 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 { @@ -233,14 +214,6 @@ 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) @@ -399,11 +372,6 @@ 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 2d96bea1..3b282ff1 100644 --- a/Fk/RoomElement/HandcardArea.qml +++ b/Fk/RoomElement/HandcardArea.qml @@ -48,7 +48,6 @@ 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 e9d4a5bf..d6554702 100644 --- a/Fk/RoomElement/PlayerCardBox.qml +++ b/Fk/RoomElement/PlayerCardBox.qml @@ -78,10 +78,10 @@ GraphicsBox { } onSelectedChanged: { if (selected) { - chosenInBox = true; + virt_name = "$Selected"; root.selected_ids.push(cid); } else { - chosenInBox = false; + virt_name = ""; root.selected_ids.splice(root.selected_ids.indexOf(cid), 1); } root.selected_ids = root.selected_ids; @@ -122,6 +122,38 @@ 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/SkillButton.qml b/Fk/RoomElement/SkillButton.qml index d7c9c7f7..09459869 100644 --- a/Fk/RoomElement/SkillButton.qml +++ b/Fk/RoomElement/SkillButton.qml @@ -23,17 +23,9 @@ Item { x: -13 - 120 * 0.166 y: -6 - 55 * 0.166 scale: 0.66 - 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; - } + source: type === "notactive" ? "" + : AppPath + "/image/button/skill/" + type + "/" + + (enabled ? (pressed ? "pressed" : "normal") : "disabled") } Image { diff --git a/README.md b/README.md index 4ab0d5d0..bf763cc9 100644 --- a/README.md +++ b/README.md @@ -63,9 +63,3 @@ ___ ## 许可证 本仓库使用GPLv3作为许可证。详见`LICENSE`文件。 - -___ - -## 点一下小星星呗! - -[![Star History Chart](https://api.star-history.com/svg?repos=Qsgs-Fans/FreeKill&type=Date)](https://star-history.com/#Qsgs-Fans/FreeKill&Date) diff --git a/lua/.vscode/settings.json b/lua/.vscode/settings.json new file mode 100644 index 00000000..fb039827 --- /dev/null +++ b/lua/.vscode/settings.json @@ -0,0 +1,14 @@ +{ + "editor.renderLineHighlight": "none", + "Lua.diagnostics.disable": [ + "undefined-field", + "inject-field", + "return-type-mismatch", + "cast-local-type", + "param-type-mismatch", + "invisible", + "missing-fields", + "assign-type-mismatch", + "undefined-doc-name" + ] +} \ No newline at end of file diff --git a/lua/client/client_util.lua b/lua/client/client_util.lua index e2529057..6a3966d7 100644 --- a/lua/client/client_util.lua +++ b/lua/client/client_util.lua @@ -1,735 +1,698 @@ -- SPDX-License-Identifier: GPL-3.0-or-later - -- All functions in this file are used by Qml - function Translate(src) - return Fk:translate(src) + return Fk:translate(src) end function GetGeneralData(name) - local general = Fk.generals[name] - if general == nil then general = Fk.generals["diaochan"] end - return json.encode { - package = general.package.name, - extension = general.package.extensionName, - kingdom = general.kingdom, - subkingdom = general.subkingdom, - hp = general.hp, - maxHp = general.maxHp, - shield = general.shield, - hidden = general.hidden, - total_hidden = general.total_hidden, - } + local general = Fk.generals[name] + if general == nil then + general = Fk.generals["diaochan"] + end + return json.encode { + package = general.package.name, + extension = general.package.extensionName, + kingdom = general.kingdom, + subkingdom = general.subkingdom, + hp = general.hp, + maxHp = general.maxHp, + shield = general.shield, + hidden = general.hidden, + total_hidden = general.total_hidden + } end function GetGeneralDetail(name) - local general = Fk.generals[name] - if general == nil then general = Fk.generals["diaochan"] end - local ret = { - package = general.package.name, - extension = general.package.extensionName, - kingdom = general.kingdom, - hp = general.hp, - maxHp = general.maxHp, - gender = general.gender, - skill = {}, - related_skill = {}, - companions = general.companions - } - for _, s in ipairs(general.skills) do - table.insert(ret.skill, { - name = s.name, - description = Fk:getDescription(s.name) - }) - end - for _, s in ipairs(general.other_skills) do - table.insert(ret.skill, { - name = s, - description = Fk:getDescription(s) - }) - end - for _, s in ipairs(general.related_skills) do - table.insert(ret.related_skill, { - name = s.name, - description = Fk:getDescription(s.name) - }) - end - for _, s in ipairs(general.related_other_skills) do - table.insert(ret.related_skill, { - name = s, - description = Fk:getDescription(s) - }) - end - for _, g in pairs(Fk.generals) do - if table.contains(g.companions, general.name) then - table.insertIfNeed(ret.companions, g.name) + local general = Fk.generals[name] + if general == nil then + general = Fk.generals["diaochan"] end - end - return json.encode(ret) + local ret = { + package = general.package.name, + extension = general.package.extensionName, + kingdom = general.kingdom, + hp = general.hp, + maxHp = general.maxHp, + gender = general.gender, + skill = {}, + related_skill = {}, + companions = general.companions + } + for _, s in ipairs(general.skills) do + table.insert(ret.skill, { + name = s.name, + description = Fk:getDescription(s.name) + }) + end + for _, s in ipairs(general.other_skills) do + table.insert(ret.skill, { + name = s, + description = Fk:getDescription(s) + }) + end + for _, s in ipairs(general.related_skills) do + table.insert(ret.related_skill, { + name = s.name, + description = Fk:getDescription(s.name) + }) + end + for _, s in ipairs(general.related_other_skills) do + table.insert(ret.related_skill, { + name = s, + description = Fk:getDescription(s) + }) + end + for _, g in pairs(Fk.generals) do + if table.contains(g.companions, general.name) then + table.insertIfNeed(ret.companions, g.name) + end + end + return json.encode(ret) end function GetSameGenerals(name) - return json.encode(Fk:getSameGenerals(name)) + return json.encode(Fk:getSameGenerals(name)) end local cardSubtypeStrings = { - [Card.SubtypeNone] = "none", - [Card.SubtypeDelayedTrick] = "delayed_trick", - [Card.SubtypeWeapon] = "weapon", - [Card.SubtypeArmor] = "armor", - [Card.SubtypeDefensiveRide] = "defensive_horse", - [Card.SubtypeOffensiveRide] = "offensive_horse", - [Card.SubtypeTreasure] = "treasure", + [Card.SubtypeNone] = "none", + [Card.SubtypeDelayedTrick] = "delayed_trick", + [Card.SubtypeWeapon] = "weapon", + [Card.SubtypeArmor] = "armor", + [Card.SubtypeDefensiveRide] = "defensive_horse", + [Card.SubtypeOffensiveRide] = "offensive_horse", + [Card.SubtypeTreasure] = "treasure" } function GetCardData(id, virtualCardForm) - local card = Fk:getCardById(id) - if card == nil then return json.encode{ - cid = id, - known = false - } end - local mark = {} - for k, v in pairs(card.mark) do - if k and k:startsWith("@") and v and v ~= 0 then - table.insert(mark, { - k = k, v = v, - }) + local card = Fk:getCardById(id) + if card == nil then + return json.encode { + cid = id, + known = false + } end - end - local ret = { - cid = id, - name = card.name, - extension = card.package.extensionName, - number = card.number, - suit = card:getSuitString(), - color = card:getColorString(), - mark = mark, - type = card.type, - subtype = cardSubtypeStrings[card.sub_type] - } - if card.skillName ~= "" then - local orig = Fk:getCardById(id, true) - ret.name = orig.name - ret.virt_name = card.name - end - if virtualCardForm then - local virtualCard = ClientInstance:getPlayerById(virtualCardForm):getVirualEquip(id) - if virtualCard then - ret.virt_name = virtualCard.name - ret.subtype = cardSubtypeStrings[virtualCard.sub_type] + local mark = {} + for k, v in pairs(card.mark) do + if k and k:startsWith("@") and v and v ~= 0 then + table.insert(mark, { + k = k, + v = v + }) + end end - end - return json.encode(ret) + local ret = { + cid = id, + name = card.name, + extension = card.package.extensionName, + number = card.number, + suit = card:getSuitString(), + color = card:getColorString(), + mark = mark, + type = card.type, + subtype = cardSubtypeStrings[card.sub_type] + } + if card.skillName ~= "" then + local orig = Fk:getCardById(id, true) + ret.name = orig.name + ret.virt_name = card.name + end + if virtualCardForm then + local virtualCard = ClientInstance:getPlayerById(virtualCardForm):getVirualEquip(id) + if virtualCard then + ret.virt_name = virtualCard.name + ret.subtype = cardSubtypeStrings[virtualCard.sub_type] + end + end + return json.encode(ret) end function GetCardExtensionByName(cardName) - local card = table.find(Fk.cards, function(card) - return card.name == cardName - end) + local card = table.find(Fk.cards, function(card) + return card.name == cardName + end) - return card and card.package.extensionName or "" + return card and card.package.extensionName or "" end function GetAllGeneralPack() - local ret = {} - for _, name in ipairs(Fk.package_names) do - if Fk.packages[name].type == Package.GeneralPack then - table.insert(ret, name) + local ret = {} + for _, name in ipairs(Fk.package_names) do + if Fk.packages[name].type == Package.GeneralPack then + table.insert(ret, name) + end end - end - return json.encode(ret) + return json.encode(ret) end function GetGenerals(pack_name) - local ret = {} - for _, g in ipairs(Fk.packages[pack_name].generals) do - if not g.total_hidden then - table.insert(ret, g.name) + local ret = {} + for _, g in ipairs(Fk.packages[pack_name].generals) do + if not g.total_hidden then + table.insert(ret, g.name) + end end - end - return json.encode(ret) + return json.encode(ret) end function SearchAllGenerals(word) - local ret = {} - for _, name in ipairs(Fk.package_names) do - if Fk.packages[name].type == Package.GeneralPack then - table.insertTable(ret, json.decode(SearchGenerals(name, word))) + local ret = {} + for _, name in ipairs(Fk.package_names) do + if Fk.packages[name].type == Package.GeneralPack then + table.insertTable(ret, json.decode(SearchGenerals(name, word))) + end end - end - return json.encode(ret) + return json.encode(ret) end function SearchGenerals(pack_name, word) - local ret = {} - if word == "" then return GetGenerals(pack_name) end - for _, g in ipairs(Fk.packages[pack_name].generals) do - if not g.total_hidden and string.find(Fk:translate(g.name), word) then - table.insert(ret, g.name) + local ret = {} + if word == "" then + return GetGenerals(pack_name) end - end - return json.encode(ret) + for _, g in ipairs(Fk.packages[pack_name].generals) do + if not g.total_hidden and string.find(Fk:translate(g.name), word) then + table.insert(ret, g.name) + end + end + return json.encode(ret) end function UpdatePackageEnable(pkg, enabled) - if enabled then - table.removeOne(ClientInstance.disabled_packs, pkg) - else - table.insertIfNeed(ClientInstance.disabled_packs, pkg) - end + if enabled then + table.removeOne(ClientInstance.disabled_packs, pkg) + else + table.insertIfNeed(ClientInstance.disabled_packs, pkg) + end end function GetAvailableGeneralsNum() - local generalPool = Fk:getAllGenerals() - local except = {} - local ret = 0 - for _, g in ipairs(Fk.packages["test_p_0"].generals) do - table.insert(except, g.name) - end - - local availableGenerals = {} - for _, general in pairs(generalPool) do - if not table.contains(except, general.name) then - if (not general.hidden and not general.total_hidden) and - #table.filter(availableGenerals, function(g) - return g.trueName == general.trueName - end) == 0 then - ret = ret + 1 - end + local generalPool = Fk:getAllGenerals() + local except = {} + local ret = 0 + for _, g in ipairs(Fk.packages["test_p_0"].generals) do + table.insert(except, g.name) end - end - return ret + local availableGenerals = {} + for _, general in pairs(generalPool) do + if not table.contains(except, general.name) then + if (not general.hidden and not general.total_hidden) and #table.filter(availableGenerals, function(g) + return g.trueName == general.trueName + end) == 0 then + ret = ret + 1 + end + end + end + + return ret end function GetAllCardPack() - local ret = {} - for _, name in ipairs(Fk.package_names) do - if Fk.packages[name].type == Package.CardPack then - table.insert(ret, name) + local ret = {} + for _, name in ipairs(Fk.package_names) do + if Fk.packages[name].type == Package.CardPack then + table.insert(ret, name) + end end - end - return json.encode(ret) + return json.encode(ret) end function GetCards(pack_name) - local ret = {} - for _, c in ipairs(Fk.packages[pack_name].cards) do - table.insert(ret, c.id) - end - return json.encode(ret) + local ret = {} + for _, c in ipairs(Fk.packages[pack_name].cards) do + table.insert(ret, c.id) + end + return json.encode(ret) end function GetCardSpecialSkills(cid) - return json.encode(Fk:getCardById(cid).special_skills or Util.DummyTable) + return json.encode(Fk:getCardById(cid).special_skills or Util.DummyTable) end function DistanceTo(from, to) - local a = ClientInstance:getPlayerById(from) - local b = ClientInstance:getPlayerById(to) - return a:distanceTo(b) + local a = ClientInstance:getPlayerById(from) + local b = ClientInstance:getPlayerById(to) + return a:distanceTo(b) end function GetPile(id, name) - return json.encode(ClientInstance:getPlayerById(id):getPile(name) or {}) + return json.encode(ClientInstance:getPlayerById(id):getPile(name) or {}) end function GetAllPiles(id) - return json.encode(ClientInstance:getPlayerById(id).special_cards or Util.DummyTable) + return json.encode(ClientInstance:getPlayerById(id).special_cards or Util.DummyTable) end function GetPlayerSkills(id) - local p = ClientInstance:getPlayerById(id) - return json.encode(table.map(p.player_skills, function(s) - return s.visible and { - name = s.name, - description = Fk:getDescription(s.name), - } or nil - end)) + local p = ClientInstance:getPlayerById(id) + return json.encode(table.map(p.player_skills, function(s) + return s.visible and { + name = s.name, + description = Fk:getDescription(s.name) + } or nil + end)) end ---@param card string | integer ---@param player integer function CanUseCard(card, player) - local c ---@type Card - if type(card) == "number" then - c = Fk:getCardById(card) - else - local data = json.decode(card) - local skill = Fk.skills[data.skill] - local selected_cards = data.subcards - if skill:isInstanceOf(ViewAsSkill) then - c = skill:viewAs(selected_cards) - if not c then - return "false" - end + local c ---@type Card + if type(card) == "number" then + c = Fk:getCardById(card) else - -- ActiveSkill should return true here - return "true" - end - end - - player = ClientInstance:getPlayerById(player) - local ret = c.skill:canUse(player, c) - ret = ret and not player:prohibitUse(c) - if ret then - local min_target = c.skill:getMinTargetNum() - if min_target > 0 then - for _, p in ipairs(ClientInstance.players) do - if c.skill:targetFilter(p.id, {}, {}, c) then - return "true" + local data = json.decode(card) + local skill = Fk.skills[data.skill] + local selected_cards = data.subcards + if skill:isInstanceOf(ViewAsSkill) then + c = skill:viewAs(selected_cards) + if not c then + return "false" + end + else + -- ActiveSkill should return true here + return "true" end - end - return "false" end - end - return json.encode(ret) + + player = ClientInstance:getPlayerById(player) + local ret = c.skill:canUse(player, c) + ret = ret and not player:prohibitUse(c) + if ret then + local min_target = c.skill:getMinTargetNum() + if min_target > 0 then + for _, p in ipairs(ClientInstance.players) do + if c.skill:targetFilter(p.id, {}, {}, c) then + return "true" + end + end + return "false" + end + end + return json.encode(ret) end function CardProhibitedUse(card) - local c ---@type Card - local ret = false - if type(card) == "number" then - c = Fk:getCardById(card) - else - local data = json.decode(card) - local skill = Fk.skills[data.skill] - local selected_cards = data.subcards - if skill:isInstanceOf(ViewAsSkill) then - c = skill:viewAs(selected_cards) + local c ---@type Card + local ret = false + if type(card) == "number" then + c = Fk:getCardById(card) + else + local data = json.decode(card) + local skill = Fk.skills[data.skill] + local selected_cards = data.subcards + if skill:isInstanceOf(ViewAsSkill) then + c = skill:viewAs(selected_cards) + end end - end - if c == nil then - return "true" - else - ret = Self:prohibitUse(c) - end - return json.encode(ret) + if c == nil then + return "true" + else + ret = Self:prohibitUse(c) + end + return json.encode(ret) end ---@param card string | integer ---@param to_select integer @ id of the target ---@param selected integer[] @ ids of selected targets function CanUseCardToTarget(card, to_select, selected) - if ClientInstance:getPlayerById(to_select).dead then - return "false" - end - 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 ActiveTargetFilter(t.skill, to_select, selected, t.subcards) - end + if ClientInstance:getPlayerById(to_select).dead then + return "false" + end + 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 ActiveTargetFilter(t.skill, to_select, selected, t.subcards) + end - local ret = c.skill:targetFilter(to_select, selected, selected_cards, c) - ret = ret and not Self:isProhibited(Fk:currentRoom():getPlayerById(to_select), c) - return json.encode(ret) + local ret = c.skill:targetFilter(to_select, selected, selected_cards, c) + ret = ret and not Self:isProhibited(Fk:currentRoom():getPlayerById(to_select), c) + return json.encode(ret) end ---@param card string | integer ---@param to_select integer @ id of a card not selected ---@param selected_targets integer[] @ ids of selected players function CanSelectCardForSkill(card, to_select, selected_targets) - local c ---@type Card - local selected_cards - if type(card) == "number" then - c = Fk:getCardById(card) - selected_cards = {card} - else - error() - end + local c ---@type Card + local selected_cards + if type(card) == "number" then + c = Fk:getCardById(card) + selected_cards = {card} + else + error() + end - local ret = c.skill:cardFilter(to_select, selected_cards, selected_targets) - return json.encode(ret) + local ret = c.skill:cardFilter(to_select, selected_cards, selected_targets) + return json.encode(ret) end ---@param card string | integer ---@param selected_targets integer[] @ ids of selected players function CardFeasible(card, selected_targets) - 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 ActiveFeasible(t.skill, selected_targets, t.subcards) - end + 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 ActiveFeasible(t.skill, selected_targets, t.subcards) + end - local ret = c.skill:feasible(selected_targets, selected_cards, Self, c) - return json.encode(ret) + local ret = c.skill:feasible(selected_targets, selected_cards, Self, c) + return json.encode(ret) end -- Handle skills function GetSkillData(skill_name) - local skill = Fk.skills[skill_name] - if not skill then return "null" end - local freq = "notactive" - if skill:isInstanceOf(ActiveSkill) or skill:isInstanceOf(ViewAsSkill) then - freq = "active" - end - local frequency - if skill.frequency == Skill.Limited then - frequency = "limit" - elseif skill.frequency == Skill.Wake then - frequency = "wake" - elseif skill.frequency == Skill.Quest then - frequency = "quest" - end - return json.encode{ - skill = Fk:translate(skill_name), - orig_skill = skill_name, - extension = skill.package.extensionName, - freq = freq, - frequency = frequency, - switchSkillName = skill.switchSkillName, - isViewAsSkill = skill:isInstanceOf(ViewAsSkill), - } + local skill = Fk.skills[skill_name] + if not skill then + return "null" + end + local freq = "notactive" + if skill:isInstanceOf(ActiveSkill) or skill:isInstanceOf(ViewAsSkill) then + freq = "active" + end + local frequency + if skill.frequency == Skill.Limited then + frequency = "limit" + elseif skill.frequency == Skill.Wake then + frequency = "wake" + elseif skill.frequency == Skill.Quest then + frequency = "quest" + end + return json.encode { + skill = Fk:translate(skill_name), + orig_skill = skill_name, + extension = skill.package.extensionName, + freq = freq, + frequency = frequency, + switchSkillName = skill.switchSkillName, + isViewAsSkill = skill:isInstanceOf(ViewAsSkill) + } end function ActiveCanUse(skill_name) - local skill = Fk.skills[skill_name] - local ret = false - if skill then - if skill:isInstanceOf(ActiveSkill) then - ret = skill:canUse(Self) - elseif skill:isInstanceOf(ViewAsSkill) then - ret = skill:enabledAtPlay(Self) - if ret then - local exp = Exppattern:Parse(skill.pattern) - local cnames = {} - for _, m in ipairs(exp.matchers) do - if m.name then - table.insertTable(cnames, m.name) - end - if m.trueName then - table.insertTable(cnames, m.trueName) - end + local skill = Fk.skills[skill_name] + local ret = false + if skill then + if skill:isInstanceOf(ActiveSkill) then + ret = skill:canUse(Self) + elseif skill:isInstanceOf(ViewAsSkill) then + ret = skill:enabledAtPlay(Self) + if ret then + local exp = Exppattern:Parse(skill.pattern) + local cnames = {} + for _, m in ipairs(exp.matchers) do + if m.name then + table.insertTable(cnames, m.name) + end + if m.trueName then + table.insertTable(cnames, m.trueName) + end + end + for _, n in ipairs(cnames) do + local c = Fk:cloneCard(n) + c.skillName = skill_name + ret = c.skill:canUse(Self, c) + if ret then + break + end + end + end end - for _, n in ipairs(cnames) do - local c = Fk:cloneCard(n) - c.skillName = skill_name - ret = c.skill:canUse(Self, c) - if ret then break end - end - end end - end - return json.encode(ret) + return json.encode(ret) end function ActiveSkillPrompt(skill_name, selected, selected_targets) - local skill = Fk.skills[skill_name] - local ret = false - if skill then - if type(skill.prompt) == "function" then - ret = skill:prompt(selected, selected_targets) - else - ret = skill.prompt + local skill = Fk.skills[skill_name] + local ret = false + if skill then + if type(skill.prompt) == "function" then + ret = skill:prompt(selected, selected_targets) + else + ret = skill.prompt + end end - end - return json.encode(ret or "") + return json.encode(ret or "") end function ActiveCardFilter(skill_name, to_select, selected, selected_targets) - local skill = Fk.skills[skill_name] - local ret = false - if skill then - if skill:isInstanceOf(ActiveSkill) then - ret = skill:cardFilter(to_select, selected, selected_targets) - elseif skill:isInstanceOf(ViewAsSkill) then - ret = skill:cardFilter(to_select, selected) + local skill = Fk.skills[skill_name] + local ret = false + if skill then + if skill:isInstanceOf(ActiveSkill) then + ret = skill:cardFilter(to_select, selected, selected_targets) + elseif skill:isInstanceOf(ViewAsSkill) then + ret = skill:cardFilter(to_select, selected) + end end - end - return json.encode(ret) + return json.encode(ret) end function ActiveTargetFilter(skill_name, to_select, selected, selected_cards) - local skill = Fk.skills[skill_name] - local ret = false - if skill then - if skill:isInstanceOf(ActiveSkill) then - ret = skill:targetFilter(to_select, selected, selected_cards) - elseif skill:isInstanceOf(ViewAsSkill) then - local card = skill:viewAs(selected_cards) - if card then - ret = card.skill:targetFilter(to_select, selected, selected_cards, card) - ret = ret and not Self:isProhibited(Fk:currentRoom():getPlayerById(to_select), card) - end + local skill = Fk.skills[skill_name] + local ret = false + if skill then + if skill:isInstanceOf(ActiveSkill) then + ret = skill:targetFilter(to_select, selected, selected_cards) + elseif skill:isInstanceOf(ViewAsSkill) then + local card = skill:viewAs(selected_cards) + if card then + ret = card.skill:targetFilter(to_select, selected, selected_cards, card) + ret = ret and not Self:isProhibited(Fk:currentRoom():getPlayerById(to_select), card) + end + end end - end - return json.encode(ret) + return json.encode(ret) end function ActiveFeasible(skill_name, selected, selected_cards) - local skill = Fk.skills[skill_name] - local ret = false - if skill then - if skill:isInstanceOf(ActiveSkill) then - ret = skill:feasible(selected, selected_cards, Self, nil) - elseif skill:isInstanceOf(ViewAsSkill) then - local card = skill:viewAs(selected_cards) - if card then - ret = card.skill:feasible(selected, selected_cards, Self, card) - end + local skill = Fk.skills[skill_name] + local ret = false + if skill then + if skill:isInstanceOf(ActiveSkill) then + ret = skill:feasible(selected, selected_cards, Self, nil) + elseif skill:isInstanceOf(ViewAsSkill) then + local card = skill:viewAs(selected_cards) + if card then + ret = card.skill:feasible(selected, selected_cards, Self, card) + end + end end - end - return json.encode(ret) + return json.encode(ret) end function CanViewAs(skill_name, card_ids) - local skill = Fk.skills[skill_name] - local ret = false - if skill then - if skill:isInstanceOf(ViewAsSkill) then - ret = skill:viewAs(card_ids) ~= nil - elseif skill:isInstanceOf(ActiveSkill) then - ret = true + local skill = Fk.skills[skill_name] + local ret = false + if skill then + if skill:isInstanceOf(ViewAsSkill) then + ret = skill:viewAs(card_ids) ~= nil + elseif skill:isInstanceOf(ActiveSkill) then + ret = true + end end - end - return json.encode(ret) + return json.encode(ret) end -- card_name may be id, name of card, or json string function CardFitPattern(card_name, pattern) - local exp = Exppattern:Parse(pattern) - local c - local ret = false - if type(card_name) == "number" then - c = Fk:getCardById(card_name) - ret = exp:match(c) - elseif string.sub(card_name, 1, 1) == "{" then - local data = json.decode(card_name) - local skill = Fk.skills[data.skill] - local selected_cards = data.subcards - if skill:isInstanceOf(ViewAsSkill) then - c = skill:viewAs(selected_cards) - if c then + local exp = Exppattern:Parse(pattern) + local c + local ret = false + if type(card_name) == "number" then + c = Fk:getCardById(card_name) ret = exp:match(c) - end + elseif string.sub(card_name, 1, 1) == "{" then + local data = json.decode(card_name) + local skill = Fk.skills[data.skill] + local selected_cards = data.subcards + if skill:isInstanceOf(ViewAsSkill) then + c = skill:viewAs(selected_cards) + if c then + ret = exp:match(c) + end + else + return "true" + end else - return "true" + ret = exp:matchExp(card_name) end - else - ret = exp:matchExp(card_name) - end - return json.encode(ret) + return json.encode(ret) end function SkillFitPattern(skill_name, pattern) - local skill = Fk.skills[skill_name] - local ret = false - if skill and skill.pattern then - local exp = Exppattern:Parse(pattern) - ret = exp:matchExp(skill.pattern) - end - return json.encode(ret) + local skill = Fk.skills[skill_name] + local ret = false + if skill and skill.pattern then + local exp = Exppattern:Parse(pattern) + ret = exp:matchExp(skill.pattern) + end + return json.encode(ret) end function CardProhibitedResponse(card) - local c ---@type Card - local ret = false - if type(card) == "number" then - c = Fk:getCardById(card) - else - local data = json.decode(card) - local skill = Fk.skills[data.skill] - local selected_cards = data.subcards - if skill:isInstanceOf(ViewAsSkill) then - c = skill:viewAs(selected_cards) + local c ---@type Card + local ret = false + if type(card) == "number" then + c = Fk:getCardById(card) + else + local data = json.decode(card) + local skill = Fk.skills[data.skill] + local selected_cards = data.subcards + if skill:isInstanceOf(ViewAsSkill) then + c = skill:viewAs(selected_cards) + end end - end - if c == nil then - return "true" - else - ret = Self:prohibitUse(c) - end - return json.encode(ret) + if c == nil then + return "true" + else + ret = Self:prohibitUse(c) + end + return json.encode(ret) end function SkillCanResponse(skill_name, cardResponsing) - local skill = Fk.skills[skill_name] - local ret = false - if skill and skill:isInstanceOf(ViewAsSkill) then - ret = skill:enabledAtResponse(Self, cardResponsing) - end - return json.encode(ret) + local skill = Fk.skills[skill_name] + local ret = false + if skill and skill:isInstanceOf(ViewAsSkill) then + ret = skill:enabledAtResponse(Self, cardResponsing) + end + return json.encode(ret) end function GetVirtualEquip(player, cid) - local c = ClientInstance:getPlayerById(player):getVirualEquip(cid) - if not c then return "null" end - return json.encode{ - name = c.name, - cid = c.subcards[1], - } + local c = ClientInstance:getPlayerById(player):getVirualEquip(cid) + if not c then + return "null" + end + return json.encode { + name = c.name, + cid = c.subcards[1] + } end function GetExpandPileOfSkill(skillName) - local skill = Fk.skills[skillName] - return skill and (skill.expand_pile or "") or "" + local skill = Fk.skills[skillName] + return skill and (skill.expand_pile or "") or "" end function GetGameModes() - local ret = {} - for k, v in pairs(Fk.game_modes) do - table.insert(ret, { - name = Fk:translate(v.name), - orig_name = v.name, - minPlayer = v.minPlayer, - maxPlayer = v.maxPlayer, - }) - end - table.sort(ret, function(a, b) return a.name > b.name end) - return json.encode(ret) + local ret = {} + for k, v in pairs(Fk.game_modes) do + table.insert(ret, { + name = Fk:translate(v.name), + orig_name = v.name, + minPlayer = v.minPlayer, + maxPlayer = v.maxPlayer + }) + end + table.sort(ret, function(a, b) + return a.name > b.name + end) + return json.encode(ret) end function GetInteractionOfSkill(skill_name) - local skill = Fk.skills[skill_name] - if skill and skill.interaction then - return json.encode(skill:interaction()) - end - return "null" + local skill = Fk.skills[skill_name] + if skill and skill.interaction then + return json.encode(skill:interaction()) + end + return "null" end function SetInteractionDataOfSkill(skill_name, data) - local skill = Fk.skills[skill_name] - if skill and skill.interaction then - skill.interaction.data = json.decode(data) - end + local skill = Fk.skills[skill_name] + if skill and skill.interaction then + skill.interaction.data = json.decode(data) + end end function ChangeSelf(pid) - local c = ClientInstance - c.client:changeSelf(pid) -- for qml - Self = c:getPlayerById(pid) + ClientInstance.client:changeSelf(pid) -- for qml + Self = ClientInstance:getPlayerById(pid) end function GetPlayerHandcards(pid) - local c = ClientInstance - local p = c:getPlayerById(pid) - return json.encode(p.player_cards[Player.Hand]) + local p = ClientInstance:getPlayerById(pid) + return json.encode(p.player_cards[Player.Hand]) end function GetPlayerEquips(pid) - local c = ClientInstance - local p = c:getPlayerById(pid) - return json.encode(p.player_cards[Player.Equip]) + local p = ClientInstance:getPlayerById(pid) + return json.encode(p.player_cards[Player.Equip]) end function ResetClientLua() - local _data = ClientInstance.enter_room_data; - local data = ClientInstance.room_settings - Self = ClientPlayer:new(fk.Self) - ClientInstance = Client:new() -- clear old client data - ClientInstance.players = {Self} - ClientInstance.alive_players = {Self} - ClientInstance.discard_pile = {} + local _data = ClientInstance.enter_room_data; + local data = ClientInstance.room_settings + Self = ClientPlayer:new(fk.Self) + ClientInstance = Client:new() -- clear old client data + ClientInstance.players = {Self} + ClientInstance.alive_players = {Self} + ClientInstance.discard_pile = {} - ClientInstance.enter_room_data = _data; - ClientInstance.room_settings = data + ClientInstance.enter_room_data = _data; + ClientInstance.room_settings = data - ClientInstance.disabled_packs = data.disabledPack - ClientInstance.disabled_generals = data.disabledGenerals - -- ClientInstance:notifyUI("EnterRoom", jsonData) + ClientInstance.disabled_packs = data.disabledPack + ClientInstance.disabled_generals = data.disabledGenerals + -- ClientInstance:notifyUI("EnterRoom", jsonData) end function ResetAddPlayer(j) - fk.client_callback["AddPlayer"](j) + fk.client_callback["AddPlayer"](j) end function GetRoomConfig() - return json.encode(ClientInstance.room_settings) + return json.encode(ClientInstance.room_settings) end function GetPlayerGameData(pid) - local c = ClientInstance - local p = c:getPlayerById(pid) - if not p then return "[0, 0, 0]" end - local raw = p.player:getGameData() - local ret = {} - for _, i in fk.qlist(raw) do - table.insert(ret, i) - end - return json.encode(ret) + local p = ClientInstance:getPlayerById(pid) + if not p then + return "[0, 0, 0]" + end + local raw = p.player:getGameData() + local ret = {} + for _, i in fk.qlist(raw) do + table.insert(ret, i) + end + return json.encode(ret) end function SetPlayerGameData(pid, data) - local c = ClientInstance - local p = c:getPlayerById(pid) - p.player:setGameData(table.unpack(data)) - table.insert(data, 1, pid) - ClientInstance:notifyUI("UpdateGameData", json.encode(data)) + local p = ClientInstance:getPlayerById(pid) + p.player:setGameData(table.unpack(data)) + table.insert(data, 1, pid) + ClientInstance:notifyUI("UpdateGameData", json.encode(data)) end function FilterMyHandcards() - Self:filterHandcards() + Self:filterHandcards() end function SetObserving(o) - ClientInstance.observing = o + ClientInstance.observing = o end function CheckSurrenderAvailable(playedTime) - local curMode = ClientInstance.room_settings.gameMode - return json.encode(Fk.game_modes[curMode]:surrenderFunc(playedTime)) + local curMode = ClientInstance.room_settings.gameMode + return json.encode(Fk.game_modes[curMode]:surrenderFunc(playedTime)) end function SaveRecord() - local c = ClientInstance - 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)) + local c = ClientInstance + c.client:saveRecord(json.encode(c.record), c.record[2]) end dofile "lua/client/i18n/init.lua" diff --git a/lua/client/i18n/en_US.lua b/lua/client/i18n/en_US.lua index 13c6ad0e..983c4489 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 (currently have %1 rooms)", + -- ["Room List"] = "房间列表", -- ["Enter"] = "进入", -- ["Observe"] = "旁观", diff --git a/lua/client/i18n/zh_CN.lua b/lua/client/i18n/zh_CN.lua index 5071ee73..14137fb5 100644 --- a/lua/client/i18n/zh_CN.lua +++ b/lua/client/i18n/zh_CN.lua @@ -2,7 +2,7 @@ Fk:loadTranslationTable{ -- Lobby - ["Room List"] = "房间列表 (共%1个房间)", + ["Room List"] = "房间列表", ["Enter"] = "进入", ["Observe"] = "旁观", diff --git a/lua/core/engine.lua b/lua/core/engine.lua index ebbb63e6..3cd53dce 100644 --- a/lua/core/engine.lua +++ b/lua/core/engine.lua @@ -25,7 +25,6 @@ ---@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的构造函数。 @@ -56,7 +55,6 @@ function Engine:initialize() self.game_mode_disabled = {} self.kingdoms = {} self._custom_events = {} - self.poxi_methods = {} self:loadPackages() self:loadDisabled() @@ -336,16 +334,6 @@ 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/core/player.lua b/lua/core/player.lua index 5d978940..72ca5657 100644 --- a/lua/core/player.lua +++ b/lua/core/player.lua @@ -136,8 +136,10 @@ function Player:setGeneral(general, setHp, addSkills) end function Player:getGeneralMaxHp() - local general = Fk.generals[type(self:getMark("__heg_general")) == "string" and self:getMark("__heg_general") or self.general] - local deputy = Fk.generals[type(self:getMark("__heg_deputy")) == "string" and self:getMark("__heg_deputy") or self.deputyGeneral] + local general = Fk.generals + [type(self:getMark("__heg_general")) == "string" and self:getMark("__heg_general") or self.general] + local deputy = Fk.generals + [type(self:getMark("__heg_deputy")) == "string" and self:getMark("__heg_deputy") or self.deputyGeneral] if not deputy then return general.maxHp + general.mainMaxHpAdjustedValue @@ -267,7 +269,7 @@ function Player:removeCards(playerArea, cardIds, specialName) if table.contains(fromAreaIds, id) then table.removeOne(fromAreaIds, id) - -- FIXME: 为客户端移动id为-1的牌考虑,但总感觉有地方需要商讨啊! + -- FIXME: 为客户端移动id为-1的牌考虑,但总感觉有地方需要商讨啊! elseif table.every(fromAreaIds, function(e) return e == -1 end) then table.remove(fromAreaIds, 1) elseif id == -1 then @@ -329,12 +331,18 @@ end function Player:getCardIds(playerAreas, specialName) local rightAreas = { Player.Hand, Player.Equip, Player.Judge } playerAreas = playerAreas or rightAreas + local cardIds = {} if type(playerAreas) == "string" then local str = playerAreas playerAreas = {} if str:find("h") then table.insert(playerAreas, Player.Hand) end + if str:find("&") then + for k, v in pairs(self.special_cards) do + if k:endsWith("&") then table.insertTable(cardIds, v) end + end + end if str:find("e") then table.insert(playerAreas, Player.Equip) end @@ -346,7 +354,6 @@ function Player:getCardIds(playerAreas, specialName) local areas = type(playerAreas) == "table" and playerAreas or { playerAreas } local rightAreas = { Player.Hand, Player.Equip, Player.Judge, Player.Special } - local cardIds = {} for _, area in ipairs(areas) do assert(table.contains(rightAreas, area)) assert(area ~= Player.Special or type(specialName) == "string") @@ -510,7 +517,8 @@ function Player:distanceTo(other, mode, ignore_dead) if temp ~= other then print("Distance malfunction: start and end does not matched.") end - local left = #(ignore_dead and Fk:currentRoom().players or Fk:currentRoom().alive_players) - right - #table.filter(Fk:currentRoom().alive_players, function(p) return p:isRemoved() end) + local left = #(ignore_dead and Fk:currentRoom().players or Fk:currentRoom().alive_players) - right - + #table.filter(Fk:currentRoom().alive_players, function(p) return p:isRemoved() end) local ret = 0 if mode == "left" then ret = left @@ -586,7 +594,7 @@ function Player:addCardUseHistory(cardName, num) num = num or 1 assert(type(num) == "number" and num ~= 0) - self.cardUsedHistory[cardName] = self.cardUsedHistory[cardName] or {0, 0, 0, 0} + self.cardUsedHistory[cardName] = self.cardUsedHistory[cardName] or { 0, 0, 0, 0 } local t = self.cardUsedHistory[cardName] for i, _ in ipairs(t) do t[i] = t[i] + num @@ -623,7 +631,7 @@ function Player:addSkillUseHistory(skill_name, num) num = num or 1 assert(type(num) == "number" and num ~= 0) - self.skillUsedHistory[skill_name] = self.skillUsedHistory[skill_name] or {0, 0, 0, 0} + self.skillUsedHistory[skill_name] = self.skillUsedHistory[skill_name] or { 0, 0, 0, 0 } local t = self.skillUsedHistory[skill_name] for i, _ in ipairs(t) do t[i] = t[i] + num @@ -648,7 +656,7 @@ function Player:setSkillUseHistory(skill_name, num, scope) return end - self.skillUsedHistory[skill_name] = self.skillUsedHistory[skill_name] or {0, 0, 0, 0} + self.skillUsedHistory[skill_name] = self.skillUsedHistory[skill_name] or { 0, 0, 0, 0 } self.skillUsedHistory[skill_name][scope] = num end @@ -681,7 +689,7 @@ end --- 获取玩家是否无手牌及装备牌。 function Player:isNude() - return #self:getCardIds{Player.Hand, Player.Equip} == 0 + return #self:getCardIds { Player.Hand, Player.Equip } == 0 end --- 获取玩家所有区域是否无牌。 @@ -729,9 +737,8 @@ function Player:hasSkill(skill, ignoreNullified, ignoreAlive) end if self:isInstanceOf(ServerPlayer) and -- isInstanceOf(nil) will return false - table.contains(self._fake_skills, skill) and - table.contains(self.prelighted_skills, skill) then - + table.contains(self._fake_skills, skill) and + table.contains(self.prelighted_skills, skill) then return true end @@ -751,7 +758,7 @@ end function Player:addSkill(skill, source_skill) skill = getActualSkill(skill) - local toget = {table.unpack(skill.related_skills)} + local toget = { table.unpack(skill.related_skills) } table.insert(toget, skill) local room = Fk:currentRoom() @@ -812,7 +819,7 @@ function Player:loseSkill(skill, source_skill) table.insert(tolose, skill) self.derivative_skills[skill] = nil - local ret = {} ---@type Skill[] + local ret = {} ---@type Skill[] for _, s in ipairs(tolose) do if not self:hasSkill(s, true, true) then table.insert(ret, s) @@ -824,7 +831,7 @@ end --- 获取对应玩家所有技能。 -- return all skills that xxx:hasSkill() == true function Player:getAllSkills() - local ret = {table.unpack(self.player_skills)} + local ret = { table.unpack(self.player_skills) } for _, t in pairs(self.derivative_skills) do for _, s in ipairs(t) do table.insertIfNeed(ret, s) @@ -844,8 +851,7 @@ end ---@param to Player @ 特定玩家 ---@param card Card @ 特定牌 function Player:isProhibited(to, card) - local r = Fk:currentRoom() - + if type(card) == "number" then card = Fk:getCardById(card) end if card.type == Card.TypeEquip and #to:getAvailableEquipSlots(card.sub_type) == 0 then return true end @@ -855,7 +861,12 @@ function Player:isProhibited(to, card) return true end - local status_skills = r.status_skills[ProhibitSkill] or Util.DummyTable + if fk.useMustTargets and + not table.contains(fk.useMustTargets, to.id) then + return true + end + + local status_skills = Fk:currentRoom().status_skills[ProhibitSkill] or Util.DummyTable for _, skill in ipairs(status_skills) do if skill:isProhibited(self, to, card) then return true @@ -907,8 +918,8 @@ function Player:prohibitReveal(isDeputy) return true end for _, m in ipairs(table.map(MarkEnum.TempMarkSuffix, function(s) - return self:getMark(MarkEnum.RevealProhibited .. s) - end)) do + return self:getMark(MarkEnum.RevealProhibited .. s) + end)) do if type(m) == "table" and table.contains(m, place) then return true end @@ -928,9 +939,11 @@ fk.SwitchYin = 1 ---@return number|string @ 转换技状态 function Player:getSwitchSkillState(skillName, afterUse, inWord) if afterUse then - return self:getMark(MarkEnum.SwithSkillPreName .. skillName) < 1 and (inWord and "yin" or fk.SwitchYin) or (inWord and "yang" or fk.SwitchYang) + return self:getMark(MarkEnum.SwithSkillPreName .. skillName) < 1 and (inWord and "yin" or fk.SwitchYin) or + (inWord and "yang" or fk.SwitchYang) else - return self:getMark(MarkEnum.SwithSkillPreName .. skillName) < 1 and (inWord and "yang" or fk.SwitchYang) or (inWord and "yin" or fk.SwitchYin) + return self:getMark(MarkEnum.SwithSkillPreName .. skillName) < 1 and (inWord and "yang" or fk.SwitchYang) or + (inWord and "yin" or fk.SwitchYin) end end @@ -946,12 +959,12 @@ function Player:canMoveCardInBoardTo(to, id) return to:hasEmptyEquipSlot(card.sub_type) else return - not ( - table.find(to:getCardIds(Player.Judge), function(cardId) - return Fk:getCardById(cardId).name == card.name - end) or - table.contains(to.sealedSlots, Player.JudgeSlot) - ) + not ( + table.find(to:getCardIds(Player.Judge), function(cardId) + return Fk:getCardById(cardId).name == card.name + end) or + table.contains(to.sealedSlots, Player.JudgeSlot) + ) end end diff --git a/lua/fk_ex.lua b/lua/fk_ex.lua index 3a1bcf85..5d1882b5 100644 --- a/lua/fk_ex.lua +++ b/lua/fk_ex.lua @@ -588,13 +588,3 @@ 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 994ce5dc..f5f4ef65 100644 --- a/lua/lsp/freekill.lua +++ b/lua/lsp/freekill.lua @@ -7,7 +7,6 @@ ---@alias null nil ---@alias bool boolean | nil ----@alias int integer ---@class fk ---FreeKill's lua API diff --git a/lua/server/ai/ai说明.txt b/lua/server/ai/ai说明.txt new file mode 100644 index 00000000..1aaf7756 --- /dev/null +++ b/lua/server/ai/ai说明.txt @@ -0,0 +1,45 @@ +拓展包ai文件写法: +按照包文件夹名+_ai,例如standard_cards是standard_cards_ai.lua,直接放在init.lua同位置下。 + +分辨敌我: +设置反值内值,同时设置技能和牌的身份值;例如杀的值为100,主公或跳身份的忠被杀时,来源反值+100,当一名角色反值大于50时,将识别为反贼;当跳反者杀反贼(或跳忠者杀主忠)时,他内值就+100,内值大于50的将被识别为内奸;当然,标记身份是以最高值开始标记身份,例如场上两名角色为400和300的最高反值,但是反贼只剩下一个,那么就只将400的标记为反贼,其余身份也是如此。优先针对虚弱,相同虚弱再优先仇恨。 + +要让身份值加给角色,就需要在使用技能或牌时接入ai接口,所以我在触发时机函数的最后加了接口,用于获取此时触发时机的数据,例如1号位杀2号位,此时触发了指定目标时机,就可以通过接口接入ai,然后让来源反值+100。但是N神不想这样增加接口,但我还不了解怎么通过其他方式同步获取使用技能或牌的数据...... + +空闲点ai出牌: +先定义阶段技或卡牌的优先度,然后获取角色所有可用的阶段技和卡牌,按照优先度进行排序,再进行for列表逐一检测是否可使用,可使用则检测是否有使用函数,有则执行使用函数;在使用函数里进行定义self.use_id和给self.use_tos添加角色id;然后系统检测有self.use_id则输出使用(如果是使用卡则self.use_id=卡id,阶段技是self.use_id=子卡表{}),同时可以给self.use_tos添加角色id做为牌或技能的目标,会同步输出。 + +请求ai使用牌: +包含请求无懈,请求桃,借刀请求杀等 +请求无懈有正无懈和反无懈,系统根据生效锦囊牌名定位请求代码(之后可以增加来源和目标技能名来修正是否使用,例如要通过伤害锦囊卖血时,可以通过技能名阻止友方使用无懈),同时请求代码会带有正反无懈的参数,根据这个参数进行分类讨论,请求代码需定义self.use_id为将要使用的无懈id,当然也可以定义self.use_id=true,这样就会根据卡牌优先度选择第一个无懈使用。因为要区分正反无懈,我在room里的请求无懈时增加了fk.askNullification和fk.askNullificationData,前者用来区分正反无懈,后者记录要无懈的锦囊数据,因为直接获取使用数据,可能会是上个无懈的数据而不是最初锦囊的数据,当然我并不知道其他的识别正反无懈和获取源锦囊生效数据的方法,只能是手动增加记录的这个样子来实现..... + +请求桃默认给友军使用,但是可以通过来源和目标的技能来修正来源是否要使用桃 + +剩下的请求牌根据提示信息名进行决策,兜底决策是套用空闲点出牌代码,所以说请求使用牌和空闲点使用牌的的函数是相通的,如果请求使用牌没有定义函数,就会调用空闲点使用牌的函数来兜底。 + +技能转化牌: +急救桃、倾国闪等 +也是通过技能名定位转化代码,也需要定义self.use_id为技能子卡表{}就行,如果没有子卡就定义空表{},如果不定义就表示不使用转化技。 + +请求ai打出牌: +杀响应决斗南蛮等 +根据牌名或技能名来分别决策,同时优先以技能名决策(用于卖血技),兜底决策是默认不响应,因为有部分是技能请求响应,例如鬼才改判打出。响应牌依旧是将要响应的卡的id定义为self.use_id(懒得再定义个新参数) + +请求ai弃置牌: +给每个牌名定义保留值,然后排序并优先弃置低保留值的牌。 + +请求ai发动技能: +直接按照技能名分别决策。 +技能请求选择角色是给self.use_tos添加目标id,例如突袭 +既请求选择角色又请求选择牌就再增加定义self.use_id为技能子卡表{} + +请求ai选择角色区域牌: +按照提示信息名分别决策,同时设置兜底决策,对敌军优先选择其重要的牌。 +依旧是定义self.use_id为技能子卡表{},不论选择一张牌还是多张牌,都是将牌id添加到表然后定义给self.use_id。 + +请求ai选择选项: +按照提示信息名分别决策,同时兜底决策是随机选择。 +这个就直接返回需要选择的选项就行。 + + + diff --git a/lua/server/ai/init.lua b/lua/server/ai/init.lua index 2b150ff8..8426698b 100644 --- a/lua/server/ai/init.lua +++ b/lua/server/ai/init.lua @@ -3,25 +3,3 @@ 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/random_ai.lua b/lua/server/ai/random_ai.lua index f788115c..7e7e037e 100644 --- a/lua/server/ai/random_ai.lua +++ b/lua/server/ai/random_ai.lua @@ -1,5 +1,4 @@ -- SPDX-License-Identifier: GPL-3.0-or-later - ---@class RandomAI: AI local RandomAI = AI:subclass("RandomAI") @@ -7,234 +6,273 @@ local RandomAI = AI:subclass("RandomAI") ---@param skill ActiveSkill ---@param card Card | nil local function useActiveSkill(self, skill, card) - local room = self.room - local player = self.player + local room = self.room + local player = self.player - local filter_func = skill.cardFilter - if card then - filter_func = function() return false end - end - - if self.command == "PlayCard" and (not skill:canUse(player, card) or player:prohibitUse(card)) then - return "" - end - - local max_try_times = 100 - local selected_targets = {} - local selected_cards = {} - local min = skill:getMinTargetNum() - local max = skill:getMaxTargetNum(player, card) - local min_card = skill:getMinCardNum() - local max_card = skill:getMaxCardNum() - for _ = 0, max_try_times do - if skill:feasible(selected_targets, selected_cards, self.player, card) then break end - local avail_targets = table.filter(self.room:getAlivePlayers(), function(p) - local ret = skill:targetFilter(p.id, selected_targets, selected_cards, card or Fk:cloneCard'zixing') - if ret and card then - if player:prohibitUse(card) then - ret = false + local filter_func = skill.cardFilter + if card then + filter_func = function() + return false end - end - return ret - end) - avail_targets = table.map(avail_targets, function(p) return p.id end) - local avail_cards = table.filter(player:getCardIds{ Player.Hand, Player.Equip }, function(id) - return filter_func(skill, id, selected_cards, selected_targets) - end) + end - if #avail_targets == 0 and #avail_cards == 0 then break end - table.insertIfNeed(selected_targets, table.random(avail_targets)) - table.insertIfNeed(selected_cards, table.random(avail_cards)) - end - if skill:feasible(selected_targets, selected_cards, self.player, card) then - local ret = json.encode{ - card = card and card.id or json.encode{ - skill = skill.name, - subcards = selected_cards, - }, - targets = selected_targets, - } - -- print(ret) - return ret - end - return "" + if self.command == "PlayCard" and (not skill:canUse(player, card) or player:prohibitUse(card)) then + return "" + end + + local max_try_times = 100 + local selected_targets = {} + local selected_cards = {} + local min = skill:getMinTargetNum() + local max = skill:getMaxTargetNum(player, card) + local min_card = skill:getMinCardNum() + local max_card = skill:getMaxCardNum() + for _ = 0, max_try_times do + if skill:feasible(selected_targets, selected_cards, self.player, card) then + break + end + local avail_targets = table.filter(self.room:getAlivePlayers(), function(p) + local ret = skill:targetFilter(p.id, selected_targets, selected_cards, card or Fk:cloneCard 'zixing') + if ret and card then + if player:prohibitUse(card) then + ret = false + end + end + return ret + end) + avail_targets = table.map(avail_targets, function(p) + return p.id + end) + local avail_cards = table.filter(player:getCardIds{Player.Hand, Player.Equip}, function(id) + return filter_func(skill, id, selected_cards, selected_targets) + end) + + if #avail_targets == 0 and #avail_cards == 0 then + break + end + table.insertIfNeed(selected_targets, table.random(avail_targets)) + table.insertIfNeed(selected_cards, table.random(avail_cards)) + end + if skill:feasible(selected_targets, selected_cards, self.player, card) then + local ret = json.encode { + card = card and card.id or json.encode { + skill = skill.name, + subcards = selected_cards + }, + targets = selected_targets + } + -- print(ret) + return ret + end + return "" end ---@param self RandomAI ---@param skill ViewAsSkill local function useVSSkill(self, skill, pattern, cancelable, extra_data) - local player = self.player - local room = self.room - local precondition + local player = self.player + local room = self.room + local precondition - if self.command == "PlayCard" then - precondition = skill:enabledAtPlay(player) - if not precondition then return nil 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 + if self.command == "PlayCard" then + precondition = skill:enabledAtPlay(player) + if not precondition then + return nil + 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(Self, c) + if precondition then + break + end + end + else + precondition = skill:enabledAtResponse(player) + if not precondition then + return nil + end + local exp = Exppattern:Parse(pattern) + precondition = exp:matchExp(skill.pattern) end - for _, n in ipairs(cnames) do - local c = Fk:cloneCard(n) - precondition = c.skill:canUse(Self, c) - if precondition then break end + + if (not precondition) or math.random() < 0.2 then + return nil end - else - precondition = skill:enabledAtResponse(player) - if not precondition then return nil end - local exp = Exppattern:Parse(pattern) - precondition = exp:matchExp(skill.pattern) - end - if (not precondition) or math.random() < 0.2 then return nil end + local selected_cards = {} + local max_try_time = 100 - local selected_cards = {} - local max_try_time = 100 - - for _ = 0, max_try_time do - local avail_cards = table.filter(player:getCardIds{ Player.Hand, Player.Equip }, function(id) - return skill:cardFilter(id, selected_cards) - end) - if #avail_cards == 0 then break end - table.insert(selected_cards, table.random(avail_cards)) - if skill:viewAs(selected_cards) then - return { - skill = skill.name, - subcards = selected_cards, - } + for _ = 0, max_try_time do + local avail_cards = table.filter(player:getCardIds{Player.Hand, Player.Equip}, function(id) + return skill:cardFilter(id, selected_cards) + end) + if #avail_cards == 0 then + break + end + table.insert(selected_cards, table.random(avail_cards)) + if skill:viewAs(selected_cards) then + return { + skill = skill.name, + subcards = selected_cards + } + end end - end - return nil + return nil end local random_cb = {} random_cb.AskForUseActiveSkill = function(self, jsonData) - local data = json.decode(jsonData) - local skill = Fk.skills[data[1]] - local cancelable = data[3] - if cancelable and math.random() < 0.25 then return "" end - local extra_data = json.decode(data[4]) - for k, v in pairs(extra_data) do - skill[k] = v - end - return useActiveSkill(self, skill) + local data = json.decode(jsonData) + local skill = Fk.skills[data[1]] + local cancelable = data[3] + if cancelable and math.random() < 0.25 then + return "" + end + local extra_data = json.decode(data[4]) + for k, v in pairs(extra_data) do + skill[k] = v + end + return useActiveSkill(self, skill) end random_cb.AskForSkillInvoke = function(self, jsonData) - return table.random{"1", ""} + return table.random {"1", ""} 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 cancelable = data[4] or true - local exp = Exppattern:Parse(pattern) + local player = self.player + local data = json.decode(jsonData) + local card_name = data[1] + local pattern = data[2] or card_name + local cancelable = data[4] or true + local exp = Exppattern:Parse(pattern) - local avail_cards = table.filter(player:getCardIds("he"), function(id) - return exp:match(Fk:getCardById(id)) and not player:prohibitUse(Fk:getCardById(id)) - end) - if #avail_cards > 0 then - if math.random() < 0.25 then return "" end - for _, card in ipairs(avail_cards) do - local skill = Fk:getCardById(card).skill - local max_try_times = 100 - local selected_targets = {} - local min = skill:getMinTargetNum() - local max = skill:getMaxTargetNum(player, card) - local min_card = skill:getMinCardNum() - local max_card = skill:getMaxCardNum() - for _ = 0, max_try_times do - if skill:feasible(selected_targets, {card}, self.player, card) then break end - local avail_targets = table.filter(self.room:getAlivePlayers(), function(p) - local ret = skill:targetFilter(p.id, selected_targets, {card}, card or Fk:cloneCard'zixing') - return ret + local avail_cards = table.filter(player:getCardIds("he"), function(id) + return exp:match(Fk:getCardById(id)) and not player:prohibitUse(Fk:getCardById(id)) + end) + if #avail_cards > 0 then + if math.random() < 0.25 then + return "" + end + avail_cards = table.map(avail_cards, function(id) + return Fk:getCardById(id) end) - avail_targets = table.map(avail_targets, function(p) return p.id end) + for _, card in ipairs(avail_cards) do + local skill = card.skill + local max_try_times = 100 + local selected_targets = {} + local min = skill:getMinTargetNum() + local max = skill:getMaxTargetNum(player, card) + local min_card = skill:getMinCardNum() + local max_card = skill:getMaxCardNum() + for _ = 0, max_try_times do + if skill:feasible(selected_targets, {card.id}, self.player, card) then + break + end + local avail_targets = table.filter(self.room:getAlivePlayers(), function(p) + local ret = skill:targetFilter(p.id, selected_targets, {card}, card or Fk:cloneCard 'zixing') + return ret + end) + avail_targets = table.map(avail_targets, function(p) + return p.id + end) - if #avail_targets == 0 and #avail_cards == 0 then break end - table.insertIfNeed(selected_targets, table.random(avail_targets)) - table.insertIfNeed({card}, table.random(avail_cards)) - end - if skill:feasible(selected_targets, {card}, self.player, card) then - return json.encode{ - card = table.random(avail_cards), - targets = selected_targets, - } - end + if #avail_targets + #avail_cards < 1 then + break + end + table.insertIfNeed(selected_targets, table.random(avail_targets)) + end + if skill:feasible(selected_targets, {card.id}, self.player, card) then + return json.encode { + card = table.random(avail_cards), + targets = selected_targets + } + end + end end - end - return "" + return "" end ---@param self RandomAI random_cb.AskForResponseCard = function(self, jsonData) - local data = json.decode(jsonData) - local pattern = data[2] - local cancelable = true - local exp = Exppattern:Parse(pattern) - local avail_cards = table.filter(self.player:getCardIds{ Player.Hand, Player.Equip }, function(id) - return exp:match(Fk:getCardById(id)) - end) - if #avail_cards > 0 then return json.encode{ - card = table.random(avail_cards), - targets = {}, - } end - -- TODO: vs skill - return "" + local data = json.decode(jsonData) + local pattern = data[2] + local cancelable = true + local exp = Exppattern:Parse(pattern) + local avail_cards = table.filter(self.player:getCardIds{Player.Hand, Player.Equip}, function(id) + return exp:match(Fk:getCardById(id)) + end) + if #avail_cards > 0 then + return json.encode { + card = table.random(avail_cards), + targets = {} + } + end + -- TODO: vs skill + return "" end ---@param self RandomAI random_cb.PlayCard = function(self, jsonData) - local cards = table.map(self.player:getCardIds(Player.Hand), - function(id) return Fk:getCardById(id) end) - 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) + local cards = table.map(self.player:getCardIds(Player.Hand), function(id) + return Fk:getCardById(id) + end) + 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.random(cards) - if sth:isInstanceOf(Card) then - local card = sth - local skill = card.skill ---@type ActiveSkill - if math.random() > 0.15 then - local ret = useActiveSkill(self, skill, card) - if ret ~= "" then return ret end - table.removeOne(cards, card) - else - table.removeOne(cards, card) - end - elseif sth:isInstanceOf(ActiveSkill) then - local active = sth - if math.random() > 0.30 then - local ret = useActiveSkill(self, active, nil) - if ret ~= "" then return ret end - end - table.removeOne(cards, active) - else - local vs = sth - if math.random() > 0.20 then - local ret = useVSSkill(self, vs) - -- TODO: handle vs result - end - table.removeOne(cards, vs) + while #cards > 0 do + local sth = table.random(cards) + if sth:isInstanceOf(Card) then + local card = sth + local skill = card.skill ---@type ActiveSkill + if math.random() > 0.15 then + local ret = useActiveSkill(self, skill, card) + if ret ~= "" then + return ret + end + table.removeOne(cards, card) + else + table.removeOne(cards, card) + end + elseif sth:isInstanceOf(ActiveSkill) then + local active = sth + if math.random() > 0.30 then + local ret = useActiveSkill(self, active, nil) + if ret ~= "" then + return ret + end + end + table.removeOne(cards, active) + else + local vs = sth + if math.random() > 0.20 then + local ret = useVSSkill(self, vs) + -- TODO: handle vs result + end + table.removeOne(cards, vs) + end end - end - return "" + return "" end function RandomAI:initialize(player) - AI.initialize(self, player) - self.cb_table = random_cb + AI.initialize(self, player) + self.cb_table = random_cb end return RandomAI diff --git a/lua/server/ai/trust_ai.lua b/lua/server/ai/trust_ai.lua index 152e6a1d..f1ec68f7 100644 --- a/lua/server/ai/trust_ai.lua +++ b/lua/server/ai/trust_ai.lua @@ -1,15 +1,1091 @@ -- SPDX-License-Identifier: GPL-3.0-or-later - -- Trust AI - ---@class TrustAI: AI -local TrustAI = AI:subclass("TrustAI") +TrustAI = AI:subclass("TrustAI") + +---@param self TrustAI +---@param skill ActiveSkill|ViewAsSkill|Card +local function usePlaySkill(self, skill) + self.use_id = nil + self.use_tos = {} + Self = self.player + self.special_skill = nil + if skill:isInstanceOf(Card) then + local uc = fk.ai_use_play[skill.name] + if type(uc) == "function" then + uc(self, skill) + end + if self.use_id == nil then + if type(skill.special_skills) == "table" then + for _, sn in ipairs(skill.special_skills) do + uc = fk.ai_use_play[sn] + if type(uc) == "function" then + uc(self, skill) + if self.use_id then + break + end + end + end + end + if skill.type == 3 then + if self.player:getEquipment(skill.sub_type) or #self.player:getCardIds("h") <= self.player.hp then + return "" + end + self.use_id = skill.id + elseif skill.is_damage_card and skill.multiple_targets then + if #self.enemies < #self.friends_noself then + return "" + end + self.use_id = skill.id + end + end + elseif skill:isInstanceOf(ViewAsSkill) then + local selected = {} + local cards = + table.map( + self.player:getCardIds("&he"), + function(id) + return Fk:getCardById(id) + end + ) + self:sortValue(cards) + for _, c in ipairs(cards) do + if skill:cardFilter(c.id, selected) then + table.insert(selected, c.id) + end + end + local tc = skill:viewAs(selected) + if tc then + local uc = fk.ai_use_play[tc.name] + if type(uc) == "function" then + uc(self, tc) + if self.use_id then + self.use_id = selected + end + end + end + else + local uc = fk.ai_use_play[skill.name] + if type(uc) == "function" then + uc(self, skill) + end + end + if self.use_id then + if not skill:isInstanceOf(Card) then + self.use_id = + json.encode { + skill = skill.name, + subcards = self.use_id + } + end + return json.encode { + card = self.use_id, + targets = self.use_tos, + special_skill = self.special_skill + } + end + return "" +end + +fk.ai_use_play = {} local trust_cb = {} +trust_cb.AskForUseActiveSkill = function(self, jsonData) + local data = json.decode(jsonData) + local skill = Fk.skills[data[1]] + local prompt = data[2] + local cancelable = data[3] + self:updatePlayers() + local extra_data = json.decode(data[4]) + for k, v in pairs(extra_data) do + skill[k] = v + end + self.use_id = nil + self.use_tos = {} + local ask = fk.ai_use_skill[data[1]] + if type(ask) == "function" then + ask(self, prompt, cancelable, extra_data) + end + if self.use_id then + return json.encode { + card = self.use_id, + targets = self.use_tos + } + end + return "" +end + +fk.ai_use_skill = {} + +fk.ai_use_skill.choose_players_skill = function(self, prompt, cancelable, data) + local ask = fk.ai_choose_players[data.skillName] + if type(ask) == "function" then + ask(self, data.targets, data.min_num, data.num, cancelable) + end + if #self.use_tos > 0 then + if self.use_id then + self.use_id = + json.encode { + skill = data.skillName, + subcards = self.use_id + } + else + self.use_id = + json.encode { + skill = data.skillName, + subcards = {} + } + end + end +end + +fk.ai_choose_players = {} + +fk.ai_use_skill.discard_skill = function(self, prompt, cancelable, data) + local ask = fk.ai_dis_card[data.skillName] + self:assignValue() + if type(ask) == "function" then + ask = ask(self, data.min_num, data.num, data.include_equip, cancelable, data.pattern, prompt) + end + if type(ask) ~= "table" and not cancelable then + local flag = "h" + if data.include_equip then + flag = "he" + end + ask = {} + local cards = + table.map( + self.player:getCardIds(flag), + function(id) + return Fk:getCardById(id) + end + ) + self:sortValue(cards) + for _, c in ipairs(cards) do + table.insert(ask, c.id) + if #ask >= data.min_num then + break + end + end + end + if type(ask) == "table" and #ask >= data.min_num then + self.use_id = + json.encode { + skill = data.skillName, + subcards = ask + } + end +end + +fk.ai_dis_card = {} + +trust_cb.AskForSkillInvoke = function(self, jsonData) + local data = json.decode(jsonData) + local prompt = data[2] + local extra_data = data[3] + local ask = fk.ai_skill_invoke[data[1]] + self:updatePlayers() + if type(ask) == "function" then + return ask(self, extra_data, prompt) and "1" or "" + elseif type(ask) == "boolean" then + return ask and "1" or "" + elseif Fk.skills[data[1]].frequency == 1 then + return "1" + else + return table.random { "1", "" } + end +end + +fk.ai_skill_invoke = {} + +trust_cb.AskForUseCard = function(self, jsonData) + local data = json.decode(jsonData) + local pattern = data[2] + local prompt = data[3] + local cancelable = data[4] + local extra_data = data[5] + self:updatePlayers() + self.use_id = nil + self.use_tos = {} + local exp = Exppattern:Parse(data[2] or data[1]) + self.avail_cards = + table.filter( + self.player:getCardIds("&he"), + function(id) + return exp:match(Fk:getCardById(id)) and not self.player:prohibitUse(Fk:getCardById(id)) + end + ) + Self = self.player + local ask = fk.ai_askuse_card[prompt:split(":")[1]] + if type(ask) == "function" then + ask(self, pattern, prompt, cancelable, extra_data) + else + local cards = + table.map( + self.player:getCardIds("&he"), + function(id) + return Fk:getCardById(id) + end + ) + self:sortValue(cards) + for _, sth in ipairs(self:getActives(pattern)) do + if sth:isInstanceOf(Card) then + if sth.skill:canUse(self.player, sth) and not self.player:prohibitUse(sth) then + local ret = usePlaySkill(self, sth) + if ret ~= "" then + return ret + end + 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(pattern) then + local uc = fk.ai_use_play[tc.name] + if type(uc) == "function" then + uc(self, tc) + if self.use_id then + self.use_id = + json.encode { + skill = sth.name, + subcards = selected + } + break + end + end + end + end + end + end + ask = fk.ai_askuse_card[data[1]] + if self.use_id == nil and type(ask) == "function" then + ask(self, pattern, prompt, cancelable, extra_data) + end + if self.use_id == true then + self.use_id = self.avail_cards[1] + end + if self.use_id then + return json.encode { + card = self.use_id, + targets = self.use_tos + } + end + return "" +end + +fk.ai_askuse_card = {} +fk.ai_nullification = {} + +fk.ai_askuse_card.nullification = function(self, pattern, prompt, cancelable, extra_data) + local effect = self:eventData("CardEffect") + if effect.to then + fk.askNullificationData = effect + fk.askNullification = 1 + elseif effect.from then + fk.askNullification = fk.askNullification + 1 + end + effect = fk.askNullificationData + local positive = fk.askNullification % 2 == 1 + local ask = fk.ai_nullification[effect.card.name] + if type(ask) == "function" then + ask(self, effect.card, self.room:getPlayerById(effect.to), self.room:getPlayerById(effect.from), positive) + end +end + +fk.ai_askuse_card["#AskForPeaches"] = function(self, pattern, prompt, cancelable, extra_data) + local dying = self:eventData("Dying") + local who = self.room:getPlayerById(dying.who) + if who and self:isFriend(who) then + local cards = + table.map( + self.player:getCardIds("&he"), + function(id) + return Fk:getCardById(id) + end + ) + self:sortValue(cards) + for _, sth in ipairs(self:getActives(pattern)) do + if sth:isInstanceOf(Card) then + self.use_id = sth.id + break + else + local selected = {} + for _, c in ipairs(cards) do + if sth.cardFilter(sth, c.id, selected) then + table.insert(selected, c.id) + end + end + local tc = sth.viewAs(sth, selected) + if tc and tc:matchPattern(pattern) then + self.use_id = + json.encode { + skill = sth.name, + subcards = selected + } + break + end + end + end + end +end + +fk.ai_askuse_card["#AskForPeachesSelf"] = fk.ai_askuse_card["#AskForPeaches"] + +fk.ai_card = {} +fk.cardValue = {} + +function TrustAI:assignValue(assign) + assign = assign or { "slash", "peach", "jink", "nullification" } + for v, p in ipairs(assign) do + local kept = {} + v = fk.ai_card[p] + v = v and v.value or 3 + for _, sth in ipairs(self:getActives(p)) do + if sth:isInstanceOf(Card) then + fk.cardValue[sth.id] = self:getValue(sth, kept) + else + fk.cardValue[sth.name] = self:getValue(sth, kept) + v + end + table.insert(kept, sth) + end + self.keptCv = nil + end +end + +function TrustAI:getValue(card, kept) + local v = fk.ai_card[card.name] + v = v and v.value or 0 + if kept then + if card:isInstanceOf(Card) then + if self.keptCv == nil then + self.keptCv = v + end + return v - #kept * 0.25 + else + return (self.keptCv or v) - #kept * 0.25 + end + elseif card:isInstanceOf(Card) then + return fk.cardValue[card.id] or v + else + return fk.cardValue[card.name] or v + end + return v +end + +function TrustAI:getPriority(card) + local v = card and fk.ai_card[card.name] + v = v and v.priority or 0 + if card:isInstanceOf(Card) then + if card:isInstanceOf(Armor) then + v = v + 7 + elseif card:isInstanceOf(Weapon) then + v = v + 3 + elseif card:isInstanceOf(OffensiveRide) then + v = v + 6 + elseif card:isInstanceOf(DefensiveRide) then + v = v + 4 + end + v = v + (13 - card.number) / 100 + v = v + card.suit / 100 + end + return v +end + +fk.compareFunc = { + hp = function(p) + return p.hp + end, + maxHp = function(p) + return p.maxHp + end, + hand = function(p) + return #p:getHandlyIds(true) + end, + equip = function(p) + return #p:getCardIds("e") + end, + maxcards = function(p) + return p.hp + end, + skill = function(p) + return #p:getAllSkills() + end, + defense = function(p) + return p.hp + #p:getHandlyIds(true) + end +} + +function TrustAI:sort(players, key, inverse) + key = key or "defense" + local func = fk.compareFunc[key] + if func == nil then + func = fk.compareFunc.defense + end + local function compare_func(a, b) + return func(a) < func(b) + end + table.sort(players, compare_func) + if inverse then + players = table.reverse(players) + end +end + +function TrustAI:sortValue(cards, inverse) + local function compare_func(a, b) + return self:getValue(a) < self:getValue(b) + end + table.sort(cards, compare_func) + if inverse then + cards = table.reverse(cards) + end +end + +function TrustAI:sortPriority(cards, inverse) + local function compare_func(a, b) + local va = a and self:getPriority(a) or 0 + local vb = b and self:getPriority(b) or 0 + if va == vb then + va = a and self:getValue(a) or 0 + vb = b and self:getValue(b) or 0 + end + return va > vb + end + table.sort(cards, compare_func) + if inverse then + cards = table.reverse(cards) + end +end + +---@param self TrustAI +trust_cb.AskForResponseCard = function(self, jsonData) + local data = json.decode(jsonData) + local pattern = data[2] + local prompt = data[3] + local cancelable = data[4] + local extra_data = data[5] + self:updatePlayers() + self.use_id = nil + local ask = fk.ai_response_card[prompt:split(":")[1]] + if type(ask) == "function" then + ask(self, pattern, prompt, cancelable, extra_data) + else + ask = fk.ai_response_card[data[1]] + if type(ask) == "function" then + ask(self, pattern, prompt, cancelable, extra_data) + end + local effect = self:eventData("CardEffect") + if effect and effect.card then + self:setUseId(pattern) + end + end + if self.use_id then + return json.encode { + card = self.use_id, + targets = {} + } + end + return "" +end + +fk.ai_response_card = {} + +function TrustAI:getRetrialCardId(cards, exchange) + local judge = self:eventData("Judge") + local isgood = judge.card:matchPattern(judge.pattern) + local canRetrial = {} + self:sortValue(cards) + if exchange then + for _, c in ipairs(cards) do + if c:matchPattern(judge.pattern) == isgood then + table.insert(canRetrial, c) + end + end + else + if isgood then + if self:isFriend(judge.who) then + return + end + elseif self:isEnemie(judge.who) then + return + end + end + for _, c in ipairs(cards) do + if + self:isFriend(judge.who) and c:matchPattern(judge.pattern) or + self:isEnemie(judge.who) and not c:matchPattern(judge.pattern) + then + table.insert(canRetrial, c) + end + end + if #canRetrial > 0 then + return canRetrial[1].id + end +end + +function TrustAI:getActives(pattern) + local cards = + table.map( + self.player:getCardIds("&he"), + function(id) + return Fk:getCardById(id) + end + ) + local exp = Exppattern:Parse(pattern) + cards = + table.filter( + cards, + function(c) + return exp:match(c) + end + ) + table.insertTable( + cards, + table.filter( + self.player:getAllSkills(), + function(s) + return s:isInstanceOf(ViewAsSkill) and s:enabledAtResponse(self.player, pattern) + end + ) + ) + self:sortPriority(cards) + return cards +end + +function TrustAI:setUseId(pattern) + local cards = + table.map( + self.player:getCardIds("&he"), + function(id) + return Fk:getCardById(id) + end + ) + self:sortValue(cards) + for _, sth in ipairs(self:getActives(pattern)) do + if sth:isInstanceOf(Card) then + self.use_id = sth.id + break + 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(pattern) then + self.use_id = + json.encode { + skill = sth.name, + subcards = selected + } + break + end + end + end +end + +function TrustAI:cardsView(pattern) + local actives = + table.filter( + self.player:getAllSkills(), + function(s) + return s:isInstanceOf(ViewAsSkill) and s:enabledAtResponse(self.player, pattern) + end + ) + return actives +end + +---@param self TrustAI +trust_cb.PlayCard = function(self, jsonData) + local cards = + table.map( + self.player:getHandlyIds(true), + function(id) + return Fk:getCardById(id) + end + ) + cards = + table.filter( + cards, + function(c) + return c.skill:canUse(self.player, c) and not self.player:prohibitUse(c) + end + ) + table.insertTable( + cards, + table.filter( + self.player:getAllSkills(), + function(s) + return s:isInstanceOf(ActiveSkill) and s:canUse(self.player) or + s:isInstanceOf(ViewAsSkill) and s:enabledAtPlay(self.player) + end + ) + ) + if #cards < 1 then + return + end + self:updatePlayers() + self:sortPriority(cards) + for _, sth in ipairs(cards) do + local ret = usePlaySkill(self, sth) + if ret ~= "" then + return ret + end + end + return "" +end + +fk.ai_card_chosen = {} + +trust_cb.AskForCardChosen = function(self, jsonData) + local data = json.decode(jsonData) + local to = self.room:getPlayerById(data[1]) + local chosen = fk.ai_card_chosen[data[3]] + if type(chosen) == "function" then + return chosen(self, to, data[2]) + elseif table.contains(self.friends, to) then + if string.find(data[2], "j") then + local jc = to:getCardIds("j") + if #jc > 0 then + return table.random(jc) + end + end + else + if string.find(data[2], "h") then + local hc = to:getCardIds("h") + if #hc == 1 then + return hc[1] + end + end + if string.find(data[2], "e") then + local ec = to:getCardIds("e") + if #ec > 0 then + return table.random(ec) + end + for c, id in ipairs(to:getCardIds("e")) do + --c = Fk:getCardById(id) + return id + end + end + if string.find(data[2], "h") then + local hc = to:getCardIds("h") + if #hc > 0 then + return table.random(hc) + end + end + end + return "" +end + +fk.ai_role = {} +fk.roleValue = {} + +fk.trick_judge = {} + +fk.trick_judge.indulgence = ".|.|heart" +fk.trick_judge.lightning = ".|.|^spade" +fk.trick_judge.supply_shortage = ".|.|club" + +local function table_clone(self) + local t = {} + for _, r in ipairs(self) do + table.insert(t, r) + end + return t +end + +trust_cb.AskForGuanxing = function(self, jsonData) + local data = json.decode(jsonData) + local cards = + table.map( + data.cards, + function(id) + return Fk:getCardById(id) + end + ) + self:sortValue(cards) + local top = {} + if self.room.current.phase < Player.Play then + local jt = + table.map( + self.room.current:getCardIds("j"), + function(id) + return Fk:getCardById(id) + end + ) + if #jt > 0 then + for i, j in ipairs(table.reverse(jt)) do + local tj = fk.trick_judge[j.name] + if tj then + for _, c in ipairs(table_clone(cards)) do + if c:matchPattern(tj) and #top < data.max_top_cards then + table.insert(top, c.id) + table.removeOne(cards, c) + tj = 1 + break + end + end + end + if tj ~= 1 and #cards > 0 and #top < data.max_top_cards then + table.insert(top, cards[1].id) + table.remove(cards, 1) + end + end + end + self:sortValue(cards, true) + for _, c in ipairs(table_clone(cards)) do + if #top < data.max_top_cards and c.skill:canUse(self.player, c) and usePlaySkill(self, c) ~= "" then + table.insert(top, c.id) + table.removeOne(cards, c) + break + end + end + end + for _, c in ipairs(table_clone(cards)) do + if #top < data.min_top_cards then + table.insert(top, c.id) + table.removeOne(cards, c) + break + end + end + return json.encode { + top, + table.map( + cards, + function(c) + return c.id + end + ) + } +end + function TrustAI:initialize(player) AI.initialize(self, player) self.cb_table = trust_cb + self.player = player + self.room = RoomInstance or ClientInstance + + fk.ai_role[player.id] = "neutral" + fk.roleValue[player.id] = { + lord = 0, + loyalist = 0, + rebel = 0, + renegade = 0 + } + self:updatePlayers() +end + +function TrustAI:isRolePredictable() + return self.room.settings.gameMode ~= "aaa_role_mode" +end + +local function aliveRoles(room) + fk.alive_roles = { + lord = 0, + loyalist = 0, + rebel = 0, + renegade = 0 + } + for _, ap in ipairs(room:getAllPlayers(false)) do + fk.alive_roles[ap.role] = 0 + end + for _, ap in ipairs(room:getAlivePlayers(false)) do + fk.alive_roles[ap.role] = fk.alive_roles[ap.role] + 1 + end + return fk.alive_roles +end + +function TrustAI:objectiveLevel(to) + if self.player.id == to.id then + return -2 + elseif #self.room:getAlivePlayers(false) < 3 then + return 5 + end + local ars = aliveRoles(self.room) + if self:isRolePredictable() then + fk.ai_role[self.player.id] = self.role + fk.roleValue[self.player.id][self.role] = 666 + if self.role == "renegade" then + fk.explicit_renegade = true + end + for _, p in ipairs(self.room:getAlivePlayers()) do + if + p.role == self.role or p.role == "lord" and self.role == "loyalist" or + p.role == "loyalist" and self.role == "lord" + then + table.insert(self.friends, p) + if p.id ~= self.player.id then + table.insert(self.friends_noself, p) + end + else + table.insert(self.enemies, p) + end + end + elseif self.role == "renegade" then + if to.role == "lord" then + return -1 + elseif ars.rebel < 1 then + return 4 + elseif fk.ai_role[to.id] == "loyalist" then + return ars.lord + ars.loyalist - ars.rebel + elseif fk.ai_role[to.id] == "rebel" then + local r = ars.rebel - ars.lord + ars.loyalist + if r >= 0 then + return 3 + else + return r + end + end + elseif self.role == "lord" or self.role == "loyalist" then + if fk.ai_role[to.id] == "rebel" then + return 5 + elseif to.role == "lord" then + return -2 + elseif ars.rebel < 1 then + if self.role == "lord" then + return fk.explicit_renegade and fk.ai_role[to.id] == "renegade" and 4 or to.hp > 1 and 2 or 0 + elseif fk.explicit_renegade then + return fk.ai_role[to.id] == "renegade" and 4 or -1 + else + return 3 + end + elseif fk.ai_role[to.id] == "loyalist" then + return -2 + elseif fk.ai_role[to.id] == "renegade" then + local r = ars.lord + ars.loyalist - ars.rebel + if r <= 0 then + return r + else + return 3 + end + end + elseif self.role == "rebel" then + if to.role == "lord" then + return 5 + elseif fk.ai_role[to.id] == "loyalist" then + return 4 + elseif fk.ai_role[to.id] == "rebel" then + return -2 + elseif fk.ai_role[to.id] == "renegade" then + local r = ars.rebel - ars.lord + ars.loyalist + if r > 0 then + return 1 + else + return r + end + end + end + return 0 +end + +function TrustAI:updatePlayers(update) + self.role = self.player.role + local neutrality = {} + self.enemies = {} + self.friends = {} + self.friends_noself = {} + + local aps = self.room:getAlivePlayers() + local function compare_func(a, b) + local v1 = fk.roleValue[a.id].rebel + local v2 = fk.roleValue[b.id].rebel + if v1 == v2 then + v1 = fk.roleValue[a.id].renegade + v2 = fk.roleValue[b.id].renegade + end + return v1 > v2 + end + table.sort(aps, compare_func) + fk.explicit_renegade = false + local ars = aliveRoles(self.room) + local rebel, renegade, loyalist = 0, 0, 0 + for _, ap in ipairs(aps) do + if ap.role == "lord" then + fk.ai_role[ap.id] = "loyalist" + elseif fk.roleValue[ap.id].rebel > 50 and ars.rebel > rebel then + rebel = rebel + 1 + fk.ai_role[ap.id] = "rebel" + elseif fk.roleValue[ap.id].renegade > 50 and ars.renegade > renegade then + renegade = renegade + 1 + fk.ai_role[ap.id] = "renegade" + fk.explicit_renegade = fk.roleValue[ap.id].renegade > 100 + elseif fk.roleValue[ap.id].rebel < -50 and ars.loyalist > loyalist then + loyalist = loyalist + 1 + fk.ai_role[ap.id] = "loyalist" + else + fk.ai_role[ap.id] = "neutral" + end + end + + for n, p in ipairs(self.room:getAlivePlayers(false)) do + n = self:objectiveLevel(p) + if n < 0 then + table.insert(self.friends, p) + if p.id ~= self.player.id then + table.insert(self.friends_noself, p) + end + elseif n > 0 then + table.insert(self.enemies, p) + else + table.insert(neutrality, p) + end + end + self:assignValue() + --[[ + if self.enemies<1 and #neutrality>0 + and#self.toUse<3 and self:getOverflow()>0 + then + function compare_func(a,b) + return sgs.getDefense(a) 0 or fk.roleValue[player.id].rebel > 0 and intention < 0 then + fk.roleValue[player.id].renegade = fk.roleValue[player.id].renegade + + intention * 100 / (200 + fk.roleValue[player.id].renegade) + end + local aps = player.room:getAlivePlayers() + local function compare_func(a, b) + local v1 = fk.roleValue[a.id].rebel + local v2 = fk.roleValue[b.id].rebel + if v1 == v2 then + v1 = fk.roleValue[a.id].renegade + v2 = fk.roleValue[b.id].renegade + end + return v1 > v2 + end + table.sort(aps, compare_func) + fk.explicit_renegade = false + local ars = aliveRoles(player.room) + local rebel, renegade, loyalist = 0, 0, 0 + for _, ap in ipairs(aps) do + if ap.role == "lord" then + fk.ai_role[ap.id] = "loyalist" + elseif fk.roleValue[ap.id].rebel > 50 and ars.rebel > rebel then + rebel = rebel + 1 + fk.ai_role[ap.id] = "rebel" + elseif fk.roleValue[ap.id].renegade > 50 and ars.renegade > renegade then + renegade = renegade + 1 + fk.ai_role[ap.id] = "renegade" + fk.explicit_renegade = fk.roleValue[ap.id].renegade > 100 + elseif fk.roleValue[ap.id].rebel < -50 and ars.loyalist > loyalist then + loyalist = loyalist + 1 + fk.ai_role[ap.id] = "loyalist" + else + fk.ai_role[ap.id] = "neutral" + end + end --[[ + fk.qWarning( + player.general .. + " " .. + intention .. + " " .. + fk.ai_role[player.id] .. + " rebelValue:" .. fk.roleValue[player.id].rebel .. " renegadeValue:" .. fk.roleValue[player.id].renegade + )--]] + end +end + +function TrustAI:filterEvent(event, player, data) + if event == fk.TargetSpecified then + local callback = fk.ai_card[data.card.name] + callback = callback and callback.intention + if type(callback) == "function" then + for _, p in ipairs(TargetGroup:getRealTargets(data.tos)) do + p = self.room:getPlayerById(p) + local intention = callback(p.ai, data.card, self.room:getPlayerById(data.from)) + if type(intention) == "number" then + updateIntention(self.room:getPlayerById(data.from), p, intention) + end + end + elseif type(callback) == "number" then + for _, p in ipairs(TargetGroup:getRealTargets(data.tos)) do + p = self.room:getPlayerById(p) + updateIntention(self.room:getPlayerById(data.from), p, callback) + end + end + elseif event == fk.StartJudge then + fk.trick_judge[data.reason] = data.pattern + elseif event == fk.AfterCardsMove then + end +end + +function TrustAI:isWeak(player, getAP) + player = player or self.player + if type(player) == "number" then + player = self.room:getPlayerById(player) + end + return player.hp < 2 or player.hp <= 2 and #player:getCardIds("&h") <= 2 +end + +function TrustAI:isFriend(pid, tid) + if tid then + local bt = self:isFriend(pid) + return bt ~= nil and bt == self:isFriend(tid) + end + if type(pid) == "number" then + pid = self.room:getPlayerById(pid) + end + local ve = self:objectiveLevel(pid) + if ve < 0 then + return true + elseif ve > 0 then + return false + end +end + +function TrustAI:isEnemie(pid, tid) + if tid then + local bt = self:isFriend(pid) + return bt ~= nil and bt ~= self:isFriend(tid) + end + if type(pid) == "number" then + pid = self.room:getPlayerById(pid) + end + local ve = self:objectiveLevel(pid) + if ve > 0 then + return true + elseif ve < 0 then + return false + end +end + +function TrustAI:eventData(game_event) + local event = self.room.logic:getCurrentEvent():findParent(GameEvent[game_event], true) + return event and event.data[1] +end + +for _, n in ipairs(FileIO.ls("packages")) do + if FileIO.isDir("packages/" .. n) and FileIO.exists("packages/" .. n .. "/" .. n .. "_ai.lua") then + dofile("packages/" .. n .. "/" .. n .. "_ai.lua") + end +end +-- 加载两次拓展是为了能够引用,例如属性杀的使用直接套入普通杀的使用 +for _, n in ipairs(FileIO.ls("packages")) do + if FileIO.isDir("packages/" .. n) and FileIO.exists("packages/" .. n .. "/" .. n .. "_ai.lua") then + dofile("packages/" .. n .. "/" .. n .. "_ai.lua") + end end return TrustAI diff --git a/lua/server/events/usecard.lua b/lua/server/events/usecard.lua index c411f9a5..32613bcd 100644 --- a/lua/server/events/usecard.lua +++ b/lua/server/events/usecard.lua @@ -1,15 +1,18 @@ -- SPDX-License-Identifier: GPL-3.0-or-later - local playCardEmotionAndSound = function(room, player, card) if card.type ~= Card.TypeEquip then local anim_path = "./packages/" .. card.package.extensionName .. "/image/anim/" .. card.name if not FileIO.exists(anim_path) then for _, dir in ipairs(FileIO.ls("./packages/")) do anim_path = "./packages/" .. dir .. "/image/anim/" .. card.name - if FileIO.exists(anim_path) then break end + if FileIO.exists(anim_path) then + break + end end end - if FileIO.exists(anim_path) then room:setEmotion(player, anim_path) end + if FileIO.exists(anim_path) then + room:setEmotion(player, anim_path) + end end local soundName @@ -25,13 +28,15 @@ local playCardEmotionAndSound = function(room, player, card) soundName = "./audio/card/common/" .. subTypeStr else - soundName = "./packages/" .. card.package.extensionName .. "/audio/card/" - .. (player.gender == General.Male and "male/" or "female/") .. card.name + soundName = "./packages/" .. card.package.extensionName .. "/audio/card/" .. + (player.gender == General.Male and "male/" or "female/") .. card.name if not FileIO.exists(soundName .. ".mp3") then for _, dir in ipairs(FileIO.ls("./packages/")) do - soundName = "./packages/" .. dir .. "/audio/card/" - .. (player.gender == General.Male and "male/" or "female/") .. card.name - if FileIO.exists(soundName .. ".mp3") then break end + soundName = "./packages/" .. dir .. "/audio/card/" .. + (player.gender == General.Male and "male/" or "female/") .. card.name + if FileIO.exists(soundName .. ".mp3") then + break + end end end end @@ -49,17 +54,19 @@ local sendCardEmotionAndLog = function(room, cardUseEvent) local card = _card ---[[ if not _card:isVirtual() then - local temp = { card = _card } + local temp = { + card = _card + } Fk:filterCard(_card.id, room:getPlayerById(from), temp) card = temp.card end cardUseEvent.card = card - --]] + -- ]] playCardEmotionAndSound(room, room:getPlayerById(from), card) room:doAnimate("Indicate", { from = from, - to = cardUseEvent.tos or Util.DummyTable, + to = cardUseEvent.tos or Util.DummyTable }) local useCardIds = card:isVirtual() and card.subcards or { card.id } @@ -71,23 +78,23 @@ local sendCardEmotionAndLog = function(room, cardUseEvent) if card:isVirtual() or (card ~= _card) then if #useCardIds == 0 then - room:sendLog{ + room:sendLog { type = "#UseV0CardToTargets", from = from, to = to, - arg = card:toLogString(), + arg = card:toLogString() } else - room:sendLog{ + room:sendLog { type = "#UseVCardToTargets", from = from, to = to, card = useCardIds, - arg = card:toLogString(), + arg = card:toLogString() } end else - room:sendLog{ + room:sendLog { type = "#UseCardToTargets", from = from, to = to, @@ -97,74 +104,78 @@ local sendCardEmotionAndLog = function(room, cardUseEvent) for _, t in ipairs(cardUseEvent.tos) do if t[2] then - local temp = {table.unpack(t)} + local temp = { table.unpack(t) } table.remove(temp, 1) - room:sendLog{ + room:sendLog { type = "#CardUseCollaborator", from = t[1], to = temp, - arg = card.name, + arg = card.name } end end elseif cardUseEvent.toCard then if card:isVirtual() or (card ~= _card) then if #useCardIds == 0 then - room:sendLog{ + room:sendLog { type = "#UseV0CardToCard", from = from, arg = cardUseEvent.toCard.name, - arg2 = card:toLogString(), + arg2 = card:toLogString() } else - room:sendLog{ + room:sendLog { type = "#UseVCardToCard", from = from, card = useCardIds, arg = cardUseEvent.toCard.name, - arg2 = card:toLogString(), + arg2 = card:toLogString() } end else - room:sendLog{ + room:sendLog { type = "#UseCardToCard", from = from, card = useCardIds, - arg = cardUseEvent.toCard.name, + arg = cardUseEvent.toCard.name } end else if card:isVirtual() or (card ~= _card) then if #useCardIds == 0 then - room:sendLog{ + room:sendLog { type = "#UseV0Card", from = from, - arg = card:toLogString(), + arg = card:toLogString() } else - room:sendLog{ + room:sendLog { type = "#UseVCard", from = from, card = useCardIds, - arg = card:toLogString(), + arg = card:toLogString() } end else - room:sendLog{ + room:sendLog { type = "#UseCard", from = from, - card = useCardIds, + card = useCardIds } end end - if #useCardIds == 0 then return end + if #useCardIds == 0 then + return + end if cardUseEvent.tos and #cardUseEvent.tos > 0 and #cardUseEvent.tos <= 2 then - local tos = table.map(cardUseEvent.tos, function(e) return e[1] end) + local tos = table.map(cardUseEvent.tos, function(e) + return e[1] + end) room:sendFootnote(useCardIds, { type = "##UseCardTo", from = from, - to = tos, + to = tos }) if card:isVirtual() then room:sendCardVirtName(useCardIds, card.name) @@ -172,7 +183,7 @@ local sendCardEmotionAndLog = function(room, cardUseEvent) else room:sendFootnote(useCardIds, { type = "##UseCard", - from = from, + from = from }) if card:isVirtual() then room:sendCardVirtName(useCardIds, card.name) @@ -185,16 +196,18 @@ GameEvent.functions[GameEvent.UseCard] = function(self) local room = self.room local logic = room.logic - room:moveCardTo(cardUseEvent.card, Card.Processing, nil, fk.ReasonUse) - if cardUseEvent.card.skill then cardUseEvent.card.skill:onUse(room, cardUseEvent) end if logic:trigger(fk.PreCardUse, room:getPlayerById(cardUseEvent.from), cardUseEvent) then + cardUseEvent.breakEvent = true + self.data = { cardUseEvent } logic:breakEvent() end + room:moveCardTo(cardUseEvent.card, Card.Processing, nil, fk.ReasonUse) + sendCardEmotionAndLog(room, cardUseEvent) if not cardUseEvent.extraUse then @@ -205,7 +218,6 @@ GameEvent.functions[GameEvent.UseCard] = function(self) cardUseEvent.responseToEvent.cardsResponded = cardUseEvent.responseToEvent.cardsResponded or {} table.insertIfNeed(cardUseEvent.responseToEvent.cardsResponded, cardUseEvent.card) end - for _, event in ipairs({ fk.AfterCardUseDeclared, fk.AfterCardTargetDeclared, fk.CardUsing }) do if not cardUseEvent.toCard and #TargetGroup:getRealTargets(cardUseEvent.tos) == 0 then break @@ -229,7 +241,7 @@ GameEvent.cleaners[GameEvent.UseCard] = function(self) room:moveCards({ ids = leftRealCardIds, toArea = Card.DiscardPile, - moveReason = fk.ReasonUse, + moveReason = fk.ReasonUse }) end end @@ -238,50 +250,54 @@ GameEvent.functions[GameEvent.RespondCard] = function(self) local cardResponseEvent = table.unpack(self.data) local room = self.room local logic = room.logic + + if logic:trigger(fk.PreCardRespond, room:getPlayerById(cardResponseEvent.from), cardResponseEvent) then + cardResponseEvent.breakEvent = true + self.data = { cardResponseEvent } + logic:breakEvent() + end + local from = cardResponseEvent.customFrom or cardResponseEvent.from local card = cardResponseEvent.card local cardIds = room:getSubcardsByRule(card) if card:isVirtual() then if #cardIds == 0 then - room:sendLog{ + room:sendLog { type = "#ResponsePlayV0Card", from = from, - arg = card:toLogString(), + arg = card:toLogString() } else - room:sendLog{ + room:sendLog { type = "#ResponsePlayVCard", from = from, card = cardIds, - arg = card:toLogString(), + arg = card:toLogString() } end else - room:sendLog{ + room:sendLog { type = "#ResponsePlayCard", from = from, - card = cardIds, + card = cardIds } end room:moveCardTo(card, Card.Processing, nil, fk.ReasonResonpse) if #cardIds > 0 then room:sendFootnote(cardIds, { type = "##ResponsePlayCard", - from = from, + from = from }) if card:isVirtual() then room:sendCardVirtName(cardIds, card.name) end end - - if logic:trigger(fk.PreCardRespond, room:getPlayerById(cardResponseEvent.from), cardResponseEvent) then - logic:breakEvent() + if cardResponseEvent.retrial ~= true then + playCardEmotionAndSound(room, room:getPlayerById(from), card) end - - playCardEmotionAndSound(room, room:getPlayerById(from), card) - logic:trigger(fk.CardResponding, room:getPlayerById(cardResponseEvent.from), cardResponseEvent) + self.data = { cardResponseEvent } end GameEvent.cleaners[GameEvent.RespondCard] = function(self) @@ -295,7 +311,7 @@ GameEvent.cleaners[GameEvent.RespondCard] = function(self) room:moveCards({ ids = realCardIds, toArea = Card.DiscardPile, - moveReason = fk.ReasonResonpse, + moveReason = fk.ReasonResonpse }) end end @@ -315,13 +331,9 @@ GameEvent.functions[GameEvent.CardEffect] = function(self) end end - if - not cardEffectEvent.toCard and - ( - not (room:getPlayerById(cardEffectEvent.to):isAlive() and cardEffectEvent.to) - or #room:deadPlayerFilter(TargetGroup:getRealTargets(cardEffectEvent.tos)) == 0 - ) - then + if not cardEffectEvent.toCard and + (not (room:getPlayerById(cardEffectEvent.to):isAlive() and cardEffectEvent.to) or + #room:deadPlayerFilter(TargetGroup:getRealTargets(cardEffectEvent.tos)) == 0) then logic:breakEvent() end diff --git a/lua/server/gameevent.lua b/lua/server/gameevent.lua index 4c884f96..61e4cfbe 100644 --- a/lua/server/gameevent.lua +++ b/lua/server/gameevent.lua @@ -73,13 +73,6 @@ 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 b0d28f53..429c4ca9 100644 --- a/lua/server/gamelogic.lua +++ b/lua/server/gamelogic.lua @@ -1,5 +1,4 @@ -- SPDX-License-Identifier: GPL-3.0-or-later - ---@class GameLogic: Object ---@field public room Room ---@field public skill_table table @@ -14,421 +13,424 @@ local GameLogic = class("GameLogic") function GameLogic:initialize(room) - self.room = room - self.skill_table = {} -- TriggerEvent --> TriggerSkill[] - self.skill_priority_table = {} - self.refresh_skill_table = {} - self.skills = {} -- skillName[] - self.game_event_stack = Stack:new() - self.all_game_events = {} - self.event_recorder = {} - self.current_event_id = 0 + self.room = room + self.skill_table = {} -- TriggerEvent --> TriggerSkill[] + self.skill_priority_table = {} + self.refresh_skill_table = {} + self.skills = {} -- skillName[] + self.game_event_stack = Stack:new() + self.all_game_events = {} + self.event_recorder = {} + self.current_event_id = 0 - self.role_table = { - { "lord" }, - { "lord", "rebel" }, - { "lord", "rebel", "renegade" }, - { "lord", "loyalist", "rebel", "renegade" }, - { "lord", "loyalist", "rebel", "rebel", "renegade" }, - { "lord", "loyalist", "rebel", "rebel", "rebel", "renegade" }, - { "lord", "loyalist", "loyalist", "rebel", "rebel", "rebel", "renegade" }, - { "lord", "loyalist", "loyalist", "rebel", "rebel", "rebel", "rebel", "renegade" }, - } + self.role_table = {{"lord"}, {"lord", "rebel"}, {"lord", "rebel", "renegade"}, + {"lord", "loyalist", "rebel", "renegade"}, {"lord", "loyalist", "rebel", "rebel", "renegade"}, + {"lord", "loyalist", "rebel", "rebel", "rebel", "renegade"}, + {"lord", "loyalist", "loyalist", "rebel", "rebel", "rebel", "renegade"}, + {"lord", "loyalist", "loyalist", "rebel", "rebel", "rebel", "rebel", "renegade"}} end function GameLogic:run() - -- default logic - local room = self.room - table.shuffle(self.room.players) - self:assignRoles() - self.room.game_started = true - room:doBroadcastNotify("StartGame", "") - room:adjustSeats() + -- default logic + local room = self.room + table.shuffle(self.room.players) + self:assignRoles() + self.room.game_started = true + room:doBroadcastNotify("StartGame", "") + room:adjustSeats() - self:chooseGenerals() + self:chooseGenerals() - self:buildPlayerCircle() - self:broadcastGeneral() - self:prepareDrawPile() - self:attachSkillToPlayers() - self:prepareForStart() + self:buildPlayerCircle() + self:broadcastGeneral() + self:prepareDrawPile() + self:attachSkillToPlayers() + self:prepareForStart() - self:action() + self:action() end local function execGameEvent(type, ...) - local event = GameEvent:new(type, ...) - local _, ret = event:exec() - return ret + local event = GameEvent:new(type, ...) + local _, ret = event:exec() + return ret end - function GameLogic:assignRoles() - local room = self.room - local n = #room.players - local roles = self.role_table[n] - table.shuffle(roles) + local room = self.room + local n = #room.players + local roles = self.role_table[n] + table.shuffle(roles) - for i = 1, n do - local p = room.players[i] - p.role = roles[i] - if p.role == "lord" then - p.role_shown = true - room:broadcastProperty(p, "role") - else - room:notifyProperty(p, p, "role") + for i = 1, n do + local p = room.players[i] + p.role = roles[i] + if p.role == "lord" then + p.role_shown = true + room:broadcastProperty(p, "role") + else + room:notifyProperty(p, p, "role") + end end - end end function GameLogic:chooseGenerals() - local room = self.room - local generalNum = room.settings.generalNum - local n = room.settings.enableDeputy and 2 or 1 - local lord = room:getLord() - local lord_generals = {} + local room = self.room + local generalNum = room.settings.generalNum + local n = room.settings.enableDeputy and 2 or 1 + local lord = room:getLord() + local lord_generals = {} - if lord ~= nil then - room.current = lord - local generals = {} - local lordlist = {} - local lordpools = {} - if room.settings.gameMode == "aaa_role_mode" then - for _, general in pairs(Fk:getAllGenerals()) do - if (not general.hidden and not general.total_hidden) and - table.find(general.skills, function(s) - return s.lordSkill - end) and - not table.find(lordlist, function(g) - return g.trueName == general.trueName - end) then - table.insert(lordlist, general) + if lord ~= nil then + room.current = lord + local generals = {} + local lordlist = {} + local lordpools = {} + if room.settings.gameMode == "aaa_role_mode" then + for _, general in pairs(Fk:getAllGenerals()) do + if (not general.hidden and not general.total_hidden) and table.find(general.skills, function(s) + return s.lordSkill + end) and not table.find(lordlist, function(g) + return g.trueName == general.trueName + end) then + table.insert(lordlist, general) + end + end + lordlist = table.random(lordlist, 3) or {} end - end - lordlist = table.random(lordlist, 3) or {} - end - table.insertTable(generals, Fk:getGeneralsRandomly(generalNum, Fk:getAllGenerals(), nil, function(g) - return table.contains(table.map(lordlist, function(g) return g.trueName end), g.trueName) - end)) - for i = 1, #generals do - generals[i] = generals[i].name - end - lordpools = table.simpleClone(generals) - table.insertTable(lordpools, table.map(lordlist, function(g) return g.name end)) - lord_generals = room:askForGeneral(lord, lordpools, n) - local lord_general, deputy - if type(lord_generals) == "table" then - deputy = lord_generals[2] - lord_general = lord_generals[1] - else - lord_general = lord_generals - lord_generals = {lord_general} + table.insertTable(generals, Fk:getGeneralsRandomly(generalNum, Fk:getAllGenerals(), nil, function(g) + return table.contains(table.map(lordlist, function(g) + return g.trueName + end), g.trueName) + end)) + for i = 1, #generals do + generals[i] = generals[i].name + end + lordpools = table.simpleClone(generals) + table.insertTable(lordpools, table.map(lordlist, function(g) + return g.name + end)) + lord_generals = room:askForGeneral(lord, lordpools, n) + local lord_general, deputy + if type(lord_generals) == "table" then + deputy = lord_generals[2] + lord_general = lord_generals[1] + else + lord_general = lord_generals + lord_generals = {lord_general} + end + + room:setPlayerGeneral(lord, lord_general, true) + room:askForChooseKingdom({lord}) + room:broadcastProperty(lord, "general") + room:broadcastProperty(lord, "kingdom") + room:setDeputyGeneral(lord, deputy) + room:broadcastProperty(lord, "deputyGeneral") end - room:setPlayerGeneral(lord, lord_general, true) - room:askForChooseKingdom({lord}) - room:broadcastProperty(lord, "general") - room:broadcastProperty(lord, "kingdom") - room:setDeputyGeneral(lord, deputy) - room:broadcastProperty(lord, "deputyGeneral") - end - - local nonlord = room:getOtherPlayers(lord, true) - local generals = Fk:getGeneralsRandomly(#nonlord * generalNum, nil, lord_generals) - table.shuffle(generals) - for _, p in ipairs(nonlord) do - local arg = {} - for i = 1, generalNum do - table.insert(arg, table.remove(generals, 1).name) + local nonlord = room:getOtherPlayers(lord, true) + local generals = Fk:getGeneralsRandomly(#nonlord * generalNum, nil, lord_generals) + table.shuffle(generals) + for _, p in ipairs(nonlord) do + local arg = {} + for i = 1, generalNum do + table.insert(arg, table.remove(generals, 1).name) + end + p.request_data = json.encode {arg, n} + p.default_reply = table.random(arg, n) end - p.request_data = json.encode{ arg, n } - p.default_reply = table.random(arg, n) - end - room:notifyMoveFocus(nonlord, "AskForGeneral") - room:doBroadcastRequest("AskForGeneral", nonlord) + room:notifyMoveFocus(nonlord, "AskForGeneral") + room:doBroadcastRequest("AskForGeneral", nonlord) - for _, p in ipairs(nonlord) do - if p.general == "" and p.reply_ready then - local generals = json.decode(p.client_reply) - local general = generals[1] - local deputy = generals[2] - room:setPlayerGeneral(p, general, true, true) - room:setDeputyGeneral(p, deputy) - else - room:setPlayerGeneral(p, p.default_reply[1], true, true) - room:setDeputyGeneral(p, p.default_reply[2]) + for _, p in ipairs(nonlord) do + if p.general == "" and p.reply_ready then + local generals = json.decode(p.client_reply) + local general = generals[1] + local deputy = generals[2] + room:setPlayerGeneral(p, general, true, true) + room:setDeputyGeneral(p, deputy) + else + room:setPlayerGeneral(p, p.default_reply[1], true, true) + room:setDeputyGeneral(p, p.default_reply[2]) + end + p.default_reply = "" end - p.default_reply = "" - end - room:askForChooseKingdom(nonlord) + room:askForChooseKingdom(nonlord) end function GameLogic:buildPlayerCircle() - local room = self.room - local players = room.players - room.alive_players = {table.unpack(players)} - for i = 1, #players - 1 do - players[i].next = players[i + 1] - end - players[#players].next = players[1] + local room = self.room + local players = room.players + room.alive_players = {table.unpack(players)} + for i = 1, #players - 1 do + players[i].next = players[i + 1] + end + players[#players].next = players[1] end function GameLogic:broadcastGeneral() - local room = self.room - local players = room.players + local room = self.room + local players = room.players - for _, p in ipairs(players) do - assert(p.general ~= "") - local general = Fk.generals[p.general] - local deputy = Fk.generals[p.deputyGeneral] - p.maxHp = p:getGeneralMaxHp() - p.hp = deputy and math.floor((deputy.hp + general.hp) / 2) or general.hp - p.shield = math.min(general.shield + (deputy and deputy.shield or 0), 5) - -- TODO: setup AI here + for _, p in ipairs(players) do + assert(p.general ~= "") + local general = Fk.generals[p.general] + local deputy = Fk.generals[p.deputyGeneral] + p.maxHp = p:getGeneralMaxHp() + p.hp = deputy and math.floor((deputy.hp + general.hp) / 2) or general.hp + p.shield = math.min(general.shield + (deputy and deputy.shield or 0), 5) + -- TODO: setup AI here - if p.role ~= "lord" then - room:broadcastProperty(p, "general") - room:broadcastProperty(p, "kingdom") - room:broadcastProperty(p, "deputyGeneral") - elseif #players >= 5 then - p.maxHp = p.maxHp + 1 - p.hp = p.hp + 1 + if p.role ~= "lord" then + room:broadcastProperty(p, "general") + room:broadcastProperty(p, "kingdom") + room:broadcastProperty(p, "deputyGeneral") + elseif #players >= 5 then + p.maxHp = p.maxHp + 1 + p.hp = p.hp + 1 + end + room:broadcastProperty(p, "maxHp") + room:broadcastProperty(p, "hp") + room:broadcastProperty(p, "shield") end - room:broadcastProperty(p, "maxHp") - room:broadcastProperty(p, "hp") - room:broadcastProperty(p, "shield") - end end function GameLogic:prepareDrawPile() - local room = self.room - local allCardIds = Fk:getAllCardIds() + local room = self.room + local allCardIds = Fk:getAllCardIds() - for i = #allCardIds, 1, -1 do - if Fk:getCardById(allCardIds[i]).is_derived then - local id = allCardIds[i] - table.removeOne(allCardIds, id) - table.insert(room.void, id) - room:setCardArea(id, Card.Void, nil) + for i = #allCardIds, 1, -1 do + if Fk:getCardById(allCardIds[i]).is_derived then + local id = allCardIds[i] + table.removeOne(allCardIds, id) + table.insert(room.void, id) + room:setCardArea(id, Card.Void, nil) + end end - end - table.shuffle(allCardIds) - room.draw_pile = allCardIds - for _, id in ipairs(room.draw_pile) do - self.room:setCardArea(id, Card.DrawPile, nil) - end + table.shuffle(allCardIds) + room.draw_pile = allCardIds + for _, id in ipairs(room.draw_pile) do + self.room:setCardArea(id, Card.DrawPile, nil) + end end function GameLogic:attachSkillToPlayers() - local room = self.room - local players = room.players + local room = self.room + local players = room.players - local addRoleModSkills = function(player, skillName) - local skill = Fk.skills[skillName] - if skill.lordSkill and (player.role ~= "lord" or #room.players < 5) then - return - end + local addRoleModSkills = function(player, skillName) + local skill = Fk.skills[skillName] + if skill.lordSkill and (player.role ~= "lord" or #room.players < 5) then + return + end - if #skill.attachedKingdom > 0 and not table.contains(skill.attachedKingdom, player.kingdom) then - return - end + if #skill.attachedKingdom > 0 and not table.contains(skill.attachedKingdom, player.kingdom) then + return + end - room:handleAddLoseSkills(player, skillName, nil, false) - end - for _, p in ipairs(room.alive_players) do - local skills = Fk.generals[p.general].skills - for _, s in ipairs(skills) do - addRoleModSkills(p, s.name) - end - for _, sname in ipairs(Fk.generals[p.general].other_skills) do - addRoleModSkills(p, sname) + room:handleAddLoseSkills(player, skillName, nil, false) end + for _, p in ipairs(room.alive_players) do + local skills = Fk.generals[p.general].skills + for _, s in ipairs(skills) do + addRoleModSkills(p, s.name) + end + for _, sname in ipairs(Fk.generals[p.general].other_skills) do + addRoleModSkills(p, sname) + end - local deputy = Fk.generals[p.deputyGeneral] - if deputy then - skills = deputy.skills - for _, s in ipairs(skills) do - addRoleModSkills(p, s.name) - end - for _, sname in ipairs(deputy.other_skills) do - addRoleModSkills(p, sname) - end + local deputy = Fk.generals[p.deputyGeneral] + if deputy then + skills = deputy.skills + for _, s in ipairs(skills) do + addRoleModSkills(p, s.name) + end + for _, sname in ipairs(deputy.other_skills) do + addRoleModSkills(p, sname) + end + end end - end end function GameLogic:prepareForStart() - local room = self.room - local players = room.players + local room = self.room + local players = room.players - self:addTriggerSkill(GameRule) - for _, trig in ipairs(Fk.global_trigger) do - self:addTriggerSkill(trig) - end + self:addTriggerSkill(GameRule) + for _, trig in ipairs(Fk.global_trigger) do + self:addTriggerSkill(trig) + end - self.room:sendLog{ type = "$GameStart" } + self.room:sendLog{ + type = "$GameStart" + } end function GameLogic:action() - self:trigger(fk.GamePrepared) - local room = self.room + self:trigger(fk.GamePrepared) + local room = self.room - execGameEvent(GameEvent.DrawInitial) + execGameEvent(GameEvent.DrawInitial) - while true do - execGameEvent(GameEvent.Round) - if room.game_finished then break end - end + while true do + execGameEvent(GameEvent.Round) + if room.game_finished then + break + end + end end ---@param skill TriggerSkill function GameLogic:addTriggerSkill(skill) - if skill == nil or table.contains(self.skills, skill.name) then - return - end - - table.insert(self.skills, skill.name) - - for _, event in ipairs(skill.refresh_events) do - if self.refresh_skill_table[event] == nil then - self.refresh_skill_table[event] = {} - end - table.insert(self.refresh_skill_table[event], skill) - end - - for _, event in ipairs(skill.events) do - if self.skill_table[event] == nil then - self.skill_table[event] = {} - end - table.insert(self.skill_table[event], skill) - - if self.skill_priority_table[event] == nil then - self.skill_priority_table[event] = {} + if skill == nil or table.contains(self.skills, skill.name) then + return end - local priority_tab = self.skill_priority_table[event] - local prio = skill.priority_table[event] - if not table.contains(priority_tab, prio) then - for i, v in ipairs(priority_tab) do - if v < prio then - table.insert(priority_tab, i, prio) - break + table.insert(self.skills, skill.name) + + for _, event in ipairs(skill.refresh_events) do + if self.refresh_skill_table[event] == nil then + self.refresh_skill_table[event] = {} end - end - - if not table.contains(priority_tab, prio) then - table.insert(priority_tab, prio) - end + table.insert(self.refresh_skill_table[event], skill) end - if not table.contains(self.skill_priority_table[event], - skill.priority_table[event]) then + for _, event in ipairs(skill.events) do + if self.skill_table[event] == nil then + self.skill_table[event] = {} + end + table.insert(self.skill_table[event], skill) - table.insert(self.skill_priority_table[event], - skill.priority_table[event]) - end - end + if self.skill_priority_table[event] == nil then + self.skill_priority_table[event] = {} + end - if skill.visible then - if (Fk.related_skills[skill.name] == nil) then return end - for _, s in ipairs(Fk.related_skills[skill.name]) do - if (s.class == TriggerSkill) then - self:addTriggerSkill(s) - end + local priority_tab = self.skill_priority_table[event] + local prio = skill.priority_table[event] + if not table.contains(priority_tab, prio) then + for i, v in ipairs(priority_tab) do + if v < prio then + table.insert(priority_tab, i, prio) + break + end + end + + if not table.contains(priority_tab, prio) then + table.insert(priority_tab, prio) + end + end + + if not table.contains(self.skill_priority_table[event], skill.priority_table[event]) then + + table.insert(self.skill_priority_table[event], skill.priority_table[event]) + end + end + + if skill.visible then + if (Fk.related_skills[skill.name] == nil) then + return + end + for _, s in ipairs(Fk.related_skills[skill.name]) do + if (s.class == TriggerSkill) then + self:addTriggerSkill(s) + end + end end - end end ---@param event Event ---@param target ServerPlayer|nil ---@param data any|nil function GameLogic:trigger(event, target, data, refresh_only) - local room = self.room - local broken = false - local skills = self.skill_table[event] or {} - local skills_to_refresh = self.refresh_skill_table[event] or Util.DummyTable - local _target = room.current -- for iteration - local player = _target - - if #skills_to_refresh > 0 then repeat do - -- refresh skills. This should not be broken - for _, skill in ipairs(skills_to_refresh) do - if skill:canRefresh(event, target, player, data) then - skill:refresh(event, target, player, data) - end - end - player = player.next - end until player == _target end - - if #skills == 0 or refresh_only then return end - - local prio_tab = self.skill_priority_table[event] - local prev_prio = math.huge - - for _, prio in ipairs(prio_tab) do - if broken then break end - if prio >= prev_prio then - -- continue - goto trigger_loop_continue + local room = self.room + local broken = false + local skills = self.skill_table[event] or {} + local skills_to_refresh = self.refresh_skill_table[event] or Util.DummyTable + local _target = room.current -- for iteration + local player = _target + if #skills_to_refresh > 0 then + repeat + do + -- refresh skills. This should not be broken + for _, skill in ipairs(skills_to_refresh) do + if skill:canRefresh(event, target, player, data) then + skill:refresh(event, target, player, data) + end + end + player = player.next + end + until player == _target end - repeat do - 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 + if #skills == 0 or refresh_only then + return + end - local skill_names = table.map(table.filter(skills, filter_func), Util.NameMapper) + local prio_tab = self.skill_priority_table[event] + local prev_prio = math.huge - while #skill_names > 0 do - local skill_name = prio <= 0 and table.random(skill_names) or - room:askForChoice(player, skill_names, "trigger", "#choose-trigger") + for _, prio in ipairs(prio_tab) do + if broken then + break + end + if prio >= prev_prio then + -- continue + goto trigger_loop_continue + end - local skill = skill_name == "game_rule" and GameRule - or Fk.skills[skill_name] + repeat + do + local triggerables = table.filter(skills, function(skill) + return skill.priority_table[event] == prio and skill:triggerable(event, target, player, data) + end) - table.insert(invoked_skills, skill) - broken = skill:trigger(event, target, player, data) - skill_names = table.map(table.filter(skills, filter_func), Util.NameMapper) + local skill_names = table.map(triggerables, function(skill) + return skill.name + end) - broken = broken or (event == fk.AskForPeaches - and room:getPlayerById(data.who).hp > 0) + while #skill_names > 0 do + local skill_name = prio <= 0 and table.random(skill_names) or + room:askForChoice(player, skill_names, "trigger", "#choose-trigger") - if broken then break end - end + local skill = skill_name == "game_rule" and GameRule or Fk.skills[skill_name] - if broken then break end + local len = #skills + broken = skill:trigger(event, target, player, data) - player = player.next - end until player == _target + 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)) - prev_prio = prio - ::trigger_loop_continue:: - end + broken = broken or (event == fk.AskForPeaches and room:getPlayerById(data.who).hp > 0) - return broken + if broken then + break + end + table.removeOne(skill_names, skill_name) + end + if broken then + break + end + player = player.next + end + until player == _target + + prev_prio = prio + ::trigger_loop_continue:: + end + _target.ai:filterEvent(event, target, data) + return broken end ---@return GameEvent 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 + return self.game_event_stack.t[self.game_event_stack.p] end -- 在指定历史范围中找至多n个符合条件的事件 @@ -438,85 +440,90 @@ end ---@param scope integer @ 查询历史范围,只能是当前阶段/回合/轮次 ---@return GameEvent[] @ 找到的符合条件的所有事件,最多n个但不保证有n个 function GameLogic:getEventsOfScope(eventType, n, func, scope) - scope = scope or Player.HistoryTurn - local event = self:getCurrentEvent() - local start_event ---@type GameEvent - if scope == Player.HistoryGame then - start_event = self.all_game_events[1] - elseif scope == Player.HistoryRound then - start_event = event:findParent(GameEvent.Round, true) - elseif scope == Player.HistoryTurn then - start_event = event:findParent(GameEvent.Turn, true) - elseif scope == Player.HistoryPhase then - start_event = event:findParent(GameEvent.Phase, true) - end + scope = scope or Player.HistoryTurn + local event = self:getCurrentEvent() + local start_event ---@type GameEvent + if scope == Player.HistoryGame then + start_event = self.all_game_events[1] + elseif scope == Player.HistoryRound then + start_event = event:findParent(GameEvent.Round, true) + elseif scope == Player.HistoryTurn then + start_event = event:findParent(GameEvent.Turn, true) + elseif scope == Player.HistoryPhase then + start_event = event:findParent(GameEvent.Phase, true) + end - return start_event:searchEvents(eventType, n, func) + return start_event:searchEvents(eventType, n, func) end function GameLogic:dumpEventStack(detailed) - local top = self:getCurrentEvent() - local i = self.game_event_stack.p - local inspect = p - if not top then return end - - print("===== Start of event stack dump =====") - if not detailed then print("") end - - repeat - local printable_data - if type(top.data) ~= "table" then - printable_data = top.data - else - printable_data = table.cloneWithoutClass(top.data) + local top = self:getCurrentEvent() + local i = self.game_event_stack.p + local inspect = p + if not top then + return end + print("===== Start of event stack dump =====") if not detailed then - print("Stack level #" .. i .. ": " .. tostring(top)) - else - print("\nStack level #" .. i .. ":") - inspect{ - eventId = GameEvent:translate(top.event), - data = printable_data or "nil", - } + print("") end - top = top.parent - i = i - 1 - until not top + repeat + local printable_data + if type(top.data) ~= "table" then + printable_data = top.data + else + printable_data = table.cloneWithoutClass(top.data) + end - print("\n===== End of event stack dump =====") + if not detailed then + print("Stack level #" .. i .. ": " .. tostring(top)) + else + print("\nStack level #" .. i .. ":") + inspect { + eventId = GameEvent:translate(top.event), + data = printable_data or "nil" + } + end + + top = top.parent + i = i - 1 + until not top + + print("\n===== End of event stack dump =====") end function GameLogic:dumpAllEvents(from, to) - from = from or 1 - to = to or #self.all_game_events - assert(from <= to) + from = from or 1 + to = to or #self.all_game_events + assert(from <= to) - local indent = 0 - local tab = " " - for i = from, to, 1 do - local v = self.all_game_events[i] - if type(v) ~= "table" then - indent = math.max(indent - 1, 0) - -- v = "End" - -- print(tab:rep(indent) .. string.format("#%d: %s", i, v)) - else - print(tab:rep(indent) .. string.format("%s", tostring(v))) - if v.id ~= v.end_id then - indent = indent + 1 - end + local indent = 0 + local tab = " " + for i = from, to, 1 do + local v = self.all_game_events[i] + if type(v) ~= "table" then + indent = math.max(indent - 1, 0) + -- v = "End" + -- print(tab:rep(indent) .. string.format("#%d: %s", i, v)) + else + print(tab:rep(indent) .. string.format("%s", tostring(v))) + if v.id ~= v.end_id then + indent = indent + 1 + end + end end - end end function GameLogic:breakEvent(ret) - coroutine.yield("__breakEvent", ret) + self.room.breakEvent = true + coroutine.yield("__breakEvent", ret) end function GameLogic:breakTurn() - local event = self:getCurrentEvent():findParent(GameEvent.Turn) - event:shutdown() + local event = self:getCurrentEvent():findParent(GameEvent.Turn) + event:shutdown() end return GameLogic diff --git a/lua/server/room.lua b/lua/server/room.lua index 721cc1ef..ac3b15e9 100644 --- a/lua/server/room.lua +++ b/lua/server/room.lua @@ -1,5 +1,4 @@ -- SPDX-License-Identifier: GPL-3.0-or-later - --- Room是fk游戏逻辑运行的主要场所,同时也提供了许多API函数供编写技能使用。 --- --- 一个房间中只有一个Room实例,保存在RoomInstance全局变量中。 @@ -56,7 +55,8 @@ dofile "lua/server/ai/init.lua" gameevent.lua (游戏事件的执行逻辑,以及各种事件的执行方法) game_rule.lua (基础游戏规则,包括执行阶段、决胜负等) aux_skills.lua (某些交互方法是套壳askForUseActiveSkill,就是在这定义的) -]]---------------------------------------------------------------------- +]] +---------------------------------------------------------------------- ------------------------------------------------------------------------ -- 构造函数 @@ -84,7 +84,7 @@ function Room:initialize(_room) self.owner_map = {} self.status_skills = {} for class, skills in pairs(Fk.global_status_skill) do - self.status_skills[class] = {table.unpack(skills)} + self.status_skills[class] = { table.unpack(skills) } end self.request_queue = {} self.request_self = {} @@ -107,10 +107,13 @@ end function Room:resume() -- 如果还没运行的话就先创建自己的主协程 if not self.main_co then - self.main_co = coroutine.create(function() - self.tag["_general_pile"] = Fk:getAllGenerals() - self:run() - end) + self.main_co = + coroutine.create( + function() + self.tag["_general_pile"] = Fk:getAllGenerals() + self:run() + end + ) end local ret, err_msg, rest_time = true, true, nil @@ -174,8 +177,7 @@ function Room:isReady() if p._splayer:thinking() then ret = false -- 烧条烧光了的话就把thinking设为false - rest = p.request_timeout * 1000 - (os.getms() - - p.request_start) / 1000 + rest = p.request_timeout * 1000 - (os.getms() - p.request_start) / 1000 if rest <= 0 or p.serverplayer:getState() ~= fk.Player_Online then p._splayer:setThinking(false) @@ -193,7 +195,9 @@ function Room:isReady() end function Room:checkNoHuman(chkOnly) - if #self.players == 0 then return end + if #self.players == 0 then + return + end for _, p in ipairs(self.players) do -- TODO: trust @@ -217,7 +221,6 @@ function Room:__gc() self.room:checkAbandoned() end --]] - --- 正式在这个房间中开始游戏。 --- --- 当这个函数返回之后,整个Room线程也宣告结束。 @@ -232,7 +235,9 @@ function Room:run() local mode = Fk.game_modes[self.settings.gameMode] self.logic = (mode.logic and mode.logic() or GameLogic):new(self) - if mode.rule then self.logic:addTriggerSkill(mode.rule) end + if mode.rule then + self.logic:addTriggerSkill(mode.rule) + end self.logic:run() end @@ -275,33 +280,36 @@ end ---@param id integer @ 玩家的id ---@return ServerPlayer @ 这个id对应的ServerPlayer实例 function Room:getPlayerById(id) - if not id then return nil end - assert(type(id) == "number") - + --assert(type(id) == "number") for _, p in ipairs(self.players) do if p.id == id then return p end end - return nil end --- 将房间中的玩家按照座位顺序重新排序。 ---@param playerIds integer[] @ 玩家id列表,这个数组会被这个函数排序 function Room:sortPlayersByAction(playerIds, isTargetGroup) - table.sort(playerIds, function(prev, next) - local prevSeat = self:getPlayerById(isTargetGroup and prev[1] or prev).seat - local nextSeat = self:getPlayerById(isTargetGroup and next[1] or next).seat + table.sort( + playerIds, + function(prev, next) + local prevSeat = self:getPlayerById(isTargetGroup and prev[1] or prev).seat + local nextSeat = self:getPlayerById(isTargetGroup and next[1] or next).seat - return prevSeat < nextSeat - end) + return prevSeat < nextSeat + end + ) if - self.current and - table.find(isTargetGroup and TargetGroup:getRealTargets(playerIds) or playerIds, function(id) - return self:getPlayerById(id).seat >= self.current.seat - end) + self.current and + table.find( + isTargetGroup and TargetGroup:getRealTargets(playerIds) or playerIds, + function(id) + return self:getPlayerById(id).seat >= self.current.seat + end + ) then while self:getPlayerById(isTargetGroup and playerIds[1][1] or playerIds[1]).seat < self.current.seat do local toPlayerId = table.remove(playerIds, 1) @@ -330,15 +338,13 @@ function Room:getAllPlayers(sortBySeat) if not self.game_started then return { table.unpack(self.players) } end - if sortBySeat == nil or sortBySeat then - local current = self.current - local temp = current.next - local ret = {current} - while temp ~= current do + if sortBySeat ~= false and self.current then + local temp = self.current.next + local ret = { self.current } + while temp ~= self.current do table.insert(ret, temp) temp = temp.next end - return ret else return { table.unpack(self.players) } @@ -349,7 +355,7 @@ end ---@param sortBySeat bool ---@return ServerPlayer[] function Room:getAlivePlayers(sortBySeat) - if sortBySeat == nil or sortBySeat then + if sortBySeat ~= false and self.current then local current = self.current local temp = current.next @@ -357,14 +363,13 @@ function Room:getAlivePlayers(sortBySeat) if temp == nil then return { table.unpack(self.players) } end - local ret = {current} + local ret = { current } while temp ~= current do if not temp.dead then table.insert(ret, temp) end temp = temp.next end - return ret else return { table.unpack(self.alive_players) } @@ -398,9 +403,13 @@ end ---@return ServerPlayer | nil @ 主公 function Room:getLord() local lord = self.players[1] - if lord.role == "lord" then return lord end + if lord.role == "lord" then + return lord + end for _, p in ipairs(self.players) do - if p.role == "lord" then return p end + if p.role == "lord" then + return p + end end return nil @@ -447,11 +456,7 @@ end ---@param value any @ 要设为的值,其实也可以设为字符串 function Room:setPlayerMark(player, mark, value) player:setMark(mark, value) - self:doBroadcastNotify("SetPlayerMark", json.encode{ - player.id, - mark, - value - }) + self:doBroadcastNotify("SetPlayerMark", json.encode { player.id, mark, value }) end --- 将一名玩家的mark标记增加count个。 @@ -485,11 +490,7 @@ end function Room:setCardMark(card, mark, value) card:setMark(mark, value) if not card:isVirtual() then - self:doBroadcastNotify("SetCardMark", json.encode{ - card.id, - mark, - value - }) + self:doBroadcastNotify("SetCardMark", json.encode { card.id, mark, value }) end end @@ -553,7 +554,9 @@ end ---@param changeKingdom bool ---@param noBroadcast bool function Room:setPlayerGeneral(player, general, changeKingdom, noBroadcast) - if Fk.generals[general] == nil then return end + if Fk.generals[general] == nil then + return + end player.general = general player.gender = Fk.generals[general].gender self:notifyProperty(player, player, "general") @@ -572,7 +575,9 @@ end ---@param player ServerPlayer ---@param general string function Room:setDeputyGeneral(player, general) - if Fk.generals[general] == nil then return end + if Fk.generals[general] == nil then + return + end player.deputyGeneral = general self:notifyProperty(player, player, "deputyGeneral") end @@ -590,23 +595,25 @@ function Room:changeHero(player, new_general, full, isDeputy, sendLog, maxHpChan if not isDeputy and (new.kingdom == "god" or new.subkingdom) then local allKingdoms = {} if new.kingdom == "god" then - allKingdoms = {"wei", "shu", "wu", "qun", "jin"} + allKingdoms = { "wei", "shu", "wu", "qun", "jin" } elseif new.subkingdom then allKingdoms = { new.kingdom, new.subkingdom } end kingdom = self:askForChoice(player, allKingdoms, "AskForKingdom", "#ChooseInitialKingdom") end - execGameEvent(GameEvent.ChangeProperty, - { - from = player, - general = not isDeputy and new_general or "", - deputyGeneral = isDeputy and new_general or "", - gender = isDeputy and player.gender or new.gender, - kingdom = kingdom, - sendLog = sendLog, - results = {}, - }) + execGameEvent( + GameEvent.ChangeProperty, + { + from = player, + general = not isDeputy and new_general or "", + deputyGeneral = isDeputy and new_general or "", + gender = isDeputy and player.gender or new.gender, + kingdom = kingdom, + sendLog = sendLog, + results = {} + } + ) maxHpChange = (maxHpChange == nil) and true or maxHpChange if maxHpChange then @@ -621,16 +628,20 @@ end ---@param kingdom string @ 要变更的势力 ---@param sendLog bool @ 是否发Log function Room:changeKingdom(player, kingdom, sendLog) - if kingdom == player.kingdom then return end + if kingdom == player.kingdom then + return + end sendLog = sendLog or false - execGameEvent(GameEvent.ChangeProperty, - { - from = player, - kingdom = kingdom, - sendLog = sendLog, - results = {}, - }) + execGameEvent( + GameEvent.ChangeProperty, + { + from = player, + kingdom = kingdom, + sendLog = sendLog, + results = {} + } + ) end ------------------------------------------------------------------------ @@ -651,11 +662,7 @@ end ---@param player ServerPlayer @ 拥有那个属性的玩家 ---@param property string @ 属性名称 function Room:notifyProperty(p, player, property) - p:doNotify("PropertyUpdate", json.encode{ - player.id, - property, - player[property], - }) + p:doNotify("PropertyUpdate", json.encode { player.id, property, player[property] }) end --- 向多名玩家广播一条消息。 @@ -676,12 +683,11 @@ end ---@param wait bool @ 是否要等待答复,默认为true ---@return string | nil @ 收到的答复,如果wait为false的话就返回nil function Room:doRequest(player, command, jsonData, wait) - if wait == nil then wait = true end self.request_queue = {} self.race_request_list = nil player:doRequest(command, jsonData, self.timeout) - if wait then + if wait ~= false then local ret = player:waitForReply(self.timeout) player.serverplayer:setBusy(false) player.serverplayer:setThinking(false) @@ -735,12 +741,11 @@ function Room:doRaceRequest(command, players, jsonData) p:doRequest(command, jsonData or p.request_data) end + local elapsed = 0 + local canceled_players = {} + local ret = nil local remainTime = self.timeout local currentTime = os.time() - local elapsed = 0 - local winner - local canceled_players = {} - local ret while true do elapsed = os.time() - currentTime if remainTime - elapsed <= 0 then @@ -749,29 +754,25 @@ function Room:doRaceRequest(command, players, jsonData) for _, p in ipairs(players) do p:waitForReply(0) if p.reply_ready == true then - winner = p + ret = p break end - if p.reply_cancel then - table.removeOne(players, p) table.insertIfNeed(canceled_players, p) + table.removeOne(players, p) elseif p.id > 0 then -- 骗过调度器让他以为自己尚未就绪 p.request_timeout = remainTime - elapsed p.serverplayer:setThinking(true) end end - if winner then + if ret then self:doBroadcastNotify("CancelRequest", "") - ret = winner break end - - if player_len == #canceled_players then + if #canceled_players >= player_len then break end - coroutine.yield("__handleRequest", (remainTime - elapsed) * 1000) end @@ -783,14 +784,12 @@ function Room:doRaceRequest(command, players, jsonData) return ret end - --- 延迟一段时间。 --- --- 这个函数不应该在请求处理协程中使用。 ---@param ms integer @ 要延迟的毫秒数 function Room:delay(ms) - local start = os.getms() - self.delay_start = start + self.delay_start = os.getms() self.delay_duration = ms self.in_delay = true coroutine.yield("__handleRequest", ms) @@ -801,7 +800,9 @@ end ---@param card_moves CardsMoveStruct[] @ 要告知的移牌信息列表 ---@param forceVisible bool @ 是否让所有牌对告知目标可见 function Room:notifyMoveCards(players, card_moves, forceVisible) - if players == nil or players == {} then players = self.players end + if players == nil or players == {} then + players = self.players + end for _, p in ipairs(players) do local arg = table.clone(card_moves) for _, move in ipairs(arg) do @@ -821,25 +822,35 @@ function Room:notifyMoveCards(players, card_moves, forceVisible) end end - local function containArea(area, relevant) --处理区的处理? - local areas = relevant - and {Card.PlayerEquip, Card.PlayerJudge, Card.DiscardPile, Card.Processing, Card.PlayerHand, Card.PlayerSpecial} - or {Card.PlayerEquip, Card.PlayerJudge, Card.DiscardPile, Card.Processing} + local function containArea(area, relevant) -- 处理区的处理? + local areas = + relevant and + { + Card.PlayerEquip, + Card.PlayerJudge, + Card.DiscardPile, + Card.Processing, + Card.PlayerHand, + Card.PlayerSpecial + } or + { Card.PlayerEquip, Card.PlayerJudge, Card.DiscardPile, Card.Processing } return table.contains(areas, area) end -- forceVisible make the move visible -- if move is relevant to player's hands or equips, it should be open - -- cards move from/to equip/judge/discard/processing should be open + -- cards move from/to equip/judge/discard/processing should be open - if not (move.moveVisible or forceVisible or containArea(move.toArea, move.to and p.isBuddy and p:isBuddy(move.to))) then + if + not (move.moveVisible or forceVisible or containArea(move.toArea, move.to and p.isBuddy and p:isBuddy(move.to))) + then for _, info in ipairs(move.moveInfo) do if not containArea(info.fromArea, move.from == p.id) then - info.cardId = -1 + info.cardId = -1 + end end end end - end p:doNotify("MoveCards", json.encode(arg)) end end @@ -851,7 +862,7 @@ end ---@param command string @ 烧条的提示文字 function Room:notifyMoveFocus(players, command) if (players.class) then - players = {players} + players = { players } end local ids = {} @@ -868,10 +879,7 @@ function Room:notifyMoveFocus(players, command) end end - self:doBroadcastNotify("MoveFocus", json.encode{ - ids, - command - }) + self:doBroadcastNotify("MoveFocus", json.encode { ids, command }) end --- 向战报中发送一条log。 @@ -881,11 +889,11 @@ function Room:sendLog(log) end function Room:sendFootnote(ids, log) - self:doBroadcastNotify("SetCardFootnote", json.encode{ ids, log }) + self:doBroadcastNotify("SetCardFootnote", json.encode { ids, log }) end function Room:sendCardVirtName(ids, name) - self:doBroadcastNotify("SetCardVirtName", json.encode{ ids, name }) + self:doBroadcastNotify("SetCardVirtName", json.encode { ids, name }) end --- 播放某种动画效果给players看。 @@ -904,10 +912,13 @@ end ---@param player ServerPlayer @ 被播放动画的那个角色 ---@param name string @ emotion名字,可以是一个路径 function Room:setEmotion(player, name) - self:doAnimate("Emotion", { - player = player.id, - emotion = name - }) + self:doAnimate( + "Emotion", + { + player = player.id, + emotion = name + } + ) end --- 在一张card上播放一段emotion动效。 @@ -916,11 +927,14 @@ end ---@param cid integer @ 被播放动效的那个牌的id ---@param name string @ emotion名字,可以是一个路径 function Room:setCardEmotion(cid, name) - self:doAnimate("Emotion", { - player = cid, - emotion = name, - is_card = true, - }) + self:doAnimate( + "Emotion", + { + player = cid, + emotion = name, + is_card = true + } + ) end --- 播放一个全屏大动画。可以自己指定qml文件路径和额外的信息。 @@ -928,10 +942,13 @@ end ---@param extra_data any @ 要传递的额外信息 function Room:doSuperLightBox(path, extra_data) path = path or "RoomElement/SuperLightBox.qml" - self:doAnimate("SuperLightBox", { - path = path, - data = extra_data, - }) + self:doAnimate( + "SuperLightBox", + { + path = path, + data = extra_data + } + ) end --- 基本上是个不常用函数就是了 @@ -945,20 +962,26 @@ end ---@param skill_name nil @ 技能名 ---@param index integer | nil @ 语音编号,默认为-1(也就是随机播放) function Room:broadcastSkillInvoke(skill_name, index) - print 'Room:broadcastSkillInvoke deprecated; use SPlayer:broadcastSkillInvoke' + print "Room:broadcastSkillInvoke deprecated; use SPlayer:broadcastSkillInvoke" index = index or -1 - self:sendLogEvent("PlaySkillSound", { - name = skill_name, - i = index - }) + self:sendLogEvent( + "PlaySkillSound", + { + name = skill_name, + i = index + } + ) end --- 播放一段音频。 ---@param path string @ 音频文件路径 function Room:broadcastPlaySound(path) - self:sendLogEvent("PlaySound", { - name = path, - }) + self:sendLogEvent( + "PlaySound", + { + name = path + } + ) end --- 在player的脸上播放技能发动的特效。 @@ -971,7 +994,9 @@ function Room:notifySkillInvoked(player, skill_name, skill_type) local bigAnim = false if not skill_type then local skill = Fk.skills[skill_name] - if not skill then skill_type = "" end + if not skill then + skill_type = "" + end if skill.frequency == Skill.Limited or skill.frequency == Skill.Wake then bigAnim = true @@ -980,25 +1005,33 @@ function Room:notifySkillInvoked(player, skill_name, skill_type) skill_type = skill.anim_type end - if skill_type == "big" then bigAnim = true end + if skill_type == "big" then + bigAnim = true + end - self:sendLog{ + self:sendLog { type = "#InvokeSkill", from = player.id, - arg = skill_name, + arg = skill_name } if not bigAnim then - self:doAnimate("InvokeSkill", { - name = skill_name, - player = player.id, - skill_type = skill_type, - }) + self:doAnimate( + "InvokeSkill", + { + name = skill_name, + player = player.id, + skill_type = skill_type + } + ) else - self:doAnimate("InvokeUltSkill", { - name = skill_name, - player = player.id, - }) + self:doAnimate( + "InvokeUltSkill", + { + name = skill_name, + player = player.id + } + ) self:delay(2000) end end @@ -1011,10 +1044,13 @@ function Room:doIndicate(source, targets) for _, id in ipairs(targets) do table.insert(target_group, { id }) end - self:doAnimate("Indicate", { - from = source, - to = target_group, - }) + self:doAnimate( + "Indicate", + { + from = source, + to = target_group + } + ) end ------------------------------------------------------------------------ @@ -1043,8 +1079,8 @@ 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, json.encode(extra_data)} + self:notifyMoveFocus(player, extra_data.skillName or skill_name) -- for display skill name instead of command name + local data = { skill_name, prompt, cancelable, json.encode(extra_data) } Fk.currentResponseReason = extra_data.skillName local result = self:doRequest(player, command, json.encode(data)) @@ -1068,11 +1104,14 @@ function Room:askForUseActiveSkill(player, skill_name, prompt, cancelable, extra end if skill:isInstanceOf(ActiveSkill) then - skill:onUse(self, { - from = player.id, - cards = selected_cards, - tos = targets, - }) + skill:onUse( + self, + { + from = player.id, + cards = selected_cards, + tos = targets + } + ) end return true, { @@ -1097,37 +1136,49 @@ Room.askForUseViewAsSkill = Room.askForUseActiveSkill ---@param skipDiscard bool @ 是否跳过弃牌(即只询问选择可以弃置的牌) ---@param no_indicate bool @ 是否不显示指示线 ---@return integer[] @ 弃掉的牌的id列表,可能是空的 -function Room:askForDiscard(player, minNum, maxNum, includeEquip, skillName, cancelable, pattern, prompt, skipDiscard, no_indicate) +function Room:askForDiscard( + player, + minNum, + maxNum, + includeEquip, + skillName, + cancelable, + pattern, + prompt, + skipDiscard, + no_indicate) cancelable = (cancelable == nil) and true or cancelable no_indicate = no_indicate or false pattern = pattern or "." - local canDiscards = table.filter( - player:getCardIds{ Player.Hand, includeEquip and Player.Equip or nil }, function(id) - local checkpoint = true - local card = Fk:getCardById(id) + local canDiscards = + table.filter( + player:getCardIds { Player.Hand, includeEquip and Player.Equip or nil }, + function(id) + local checkpoint = true + local card = Fk:getCardById(id) - local status_skills = Fk:currentRoom().status_skills[ProhibitSkill] or Util.DummyTable - for _, skill in ipairs(status_skills) do - if skill:prohibitDiscard(player, card) then - return false - end - end - if skillName == "game_rule" then - status_skills = Fk:currentRoom().status_skills[MaxCardsSkill] or Util.DummyTable - for _, skill in ipairs(status_skills) do - if skill:excludeFrom(player, card) then - return false + local status_skills = Fk:currentRoom().status_skills[ProhibitSkill] or Util.DummyTable + for _, skill in ipairs(status_skills) do + if skill:prohibitDiscard(player, card) then + return false + end + end + if skillName == "game_rule" then + status_skills = Fk:currentRoom().status_skills[MaxCardsSkill] or Util.DummyTable + for _, skill in ipairs(status_skills) do + if skill:excludeFrom(player, card) then + return false + end + end end - end - end - if pattern ~= "" then - checkpoint = checkpoint and (Exppattern:Parse(pattern):match(card)) - end - return checkpoint - end - ) + if pattern ~= "" then + checkpoint = checkpoint and (Exppattern:Parse(pattern):match(card)) + end + return checkpoint + end + ) -- maxNum = math.min(#canDiscards, maxNum) -- minNum = math.min(#canDiscards, minNum) @@ -1145,7 +1196,7 @@ function Room:askForDiscard(player, minNum, maxNum, includeEquip, skillName, can min_num = minNum, include_equip = includeEquip, skillName = skillName, - pattern = pattern, + pattern = pattern } local prompt = prompt or ("#AskForDiscard:::" .. maxNum .. ":" .. minNum) local _, ret = self:askForUseActiveSkill(player, "discard_skill", prompt, cancelable, data, no_indicate) @@ -1153,7 +1204,9 @@ function Room:askForDiscard(player, minNum, maxNum, includeEquip, skillName, can if ret then toDiscard = ret.cards else - if cancelable then return {} end + if cancelable then + return {} + end toDiscard = table.random(canDiscards, minNum) end @@ -1214,7 +1267,17 @@ end ---@param expand_pile string|nil @ 可选私人牌堆名称 ---@param no_indicate bool @ 是否不显示指示线 ---@return integer[] @ 选择的牌的id列表,可能是空的 -function Room:askForCard(player, minNum, maxNum, includeEquip, skillName, cancelable, pattern, prompt, expand_pile, no_indicate) +function Room:askForCard( + player, + minNum, + maxNum, + includeEquip, + skillName, + cancelable, + pattern, + prompt, + expand_pile, + no_indicate) if minNum < 1 then return nil end @@ -1229,14 +1292,16 @@ function Room:askForCard(player, minNum, maxNum, includeEquip, skillName, cancel include_equip = includeEquip, skillName = skillName, pattern = pattern, - expand_pile = expand_pile, + expand_pile = expand_pile } local prompt = prompt or ("#AskForCard:::" .. maxNum .. ":" .. minNum) local _, ret = self:askForUseActiveSkill(player, "choose_cards_skill", prompt, cancelable, data, no_indicate) if ret then chosenCards = ret.cards else - if cancelable then return {} end + if cancelable then + return {} + end local hands = player:getCardIds(Player.Hand) if includeEquip then table.insertTable(hands, player:getCardIds(Player.Equip)) @@ -1252,7 +1317,6 @@ function Room:askForCard(player, minNum, maxNum, includeEquip, skillName, cancel end --- 询问玩家选择1张牌和若干名角色。 ---- --- 返回两个值,第一个是选择的目标列表,第二个是选择的那张牌的id ---@param player ServerPlayer @ 要询问的玩家 ---@param targets integer[] @ 选择目标的id范围 @@ -1263,7 +1327,16 @@ end ---@param cancelable bool @ 能否点取消 ---@param no_indicate bool @ 是否不显示指示线 ---@return integer[], integer -function Room:askForChooseCardAndPlayers(player, targets, minNum, maxNum, pattern, prompt, skillName, cancelable, no_indicate) +function Room:askForChooseCardAndPlayers( + player, + targets, + minNum, + maxNum, + pattern, + prompt, + skillName, + cancelable, + no_indicate) if maxNum < 1 then return {} end @@ -1271,11 +1344,17 @@ function Room:askForChooseCardAndPlayers(player, targets, minNum, maxNum, patter no_indicate = no_indicate or false pattern = pattern or "." - local pcards = table.filter(player:getCardIds({ Player.Hand, Player.Equip }), function(id) - local c = Fk:getCardById(id) - return c:matchPattern(pattern) - end) - if #pcards == 0 and not cancelable then return {} end + local pcards = + table.filter( + player:getCardIds({ Player.Hand, Player.Equip }), + function(id) + local c = Fk:getCardById(id) + return c:matchPattern(pattern) + end + ) + if #pcards == 0 and not cancelable then + return {} + end local data = { targets = targets, @@ -1307,18 +1386,22 @@ function Room:askForGeneral(player, generals, n, noConvert) self:notifyMoveFocus(player, command) n = n or 1 - if #generals == n then return n == 1 and generals[1] or generals end + 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 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 + if #choices == 1 then + return choices[1] + end return choices end @@ -1329,16 +1412,26 @@ end ---@param players ServerPlayer[]|nil @ 询问目标 function Room:askForChooseKingdom(players) players = players or self.alive_players - local specialKingdomPlayers = table.filter(players, function(p) - return p.kingdom == "god" or Fk.generals[p.general].subkingdom - end) + local specialKingdomPlayers = + table.filter( + players, + function(p) + return p.kingdom == "god" or Fk.generals[p.general].subkingdom + end + ) if #specialKingdomPlayers > 0 then local choiceMap = {} for _, p in ipairs(specialKingdomPlayers) do local allKingdoms = {} if p.kingdom == "god" then - allKingdoms = table.filter({"wei", "shu", "wu", "qun", "jin"}, function(k) return table.contains(Fk.kingdoms, k) end) + allKingdoms = + table.filter( + { "wei", "shu", "wu", "qun", "jin" }, + function(k) + return table.contains(Fk.kingdoms, k) + end + ) else local curGeneral = Fk.generals[p.general] allKingdoms = { curGeneral.kingdom, curGeneral.subkingdom } @@ -1376,9 +1469,8 @@ end function Room:askForCardChosen(chooser, target, flag, reason) local command = "AskForCardChosen" self:notifyMoveFocus(chooser, command) - local data = {target.id, flag, reason} + local data = { target.id, flag, reason } local result = self:doRequest(chooser, command, json.encode(data)) - if result == "" then local areas = {} local handcards @@ -1404,7 +1496,6 @@ function Room:askForCardChosen(chooser, target, flag, reason) if #handcards == 0 then return end result = table.random(handcards) end - return result end @@ -1424,7 +1515,7 @@ function Room:askForCardsChosen(chooser, target, min, max, flag, reason) local command = "AskForCardsChosen" self:notifyMoveFocus(chooser, command) - local data = {target.id, min, max, flag, reason} + local data = { target.id, min, max, flag, reason } local result = self:doRequest(chooser, command, json.encode(data)) local ret @@ -1434,9 +1525,15 @@ function Room:askForCardsChosen(chooser, target, min, max, flag, reason) local areas = {} local handcards if type(flag) == "string" then - if string.find(flag, "h") then table.insert(areas, Player.Hand) end - if string.find(flag, "e") then table.insert(areas, Player.Equip) end - if string.find(flag, "j") then table.insert(areas, Player.Judge) end + if string.find(flag, "h") then + table.insert(areas, Player.Hand) + end + if string.find(flag, "e") then + table.insert(areas, Player.Equip) + end + if string.find(flag, "j") then + table.insert(areas, Player.Judge) + end handcards = target:getCardIds(areas) else handcards = {} @@ -1444,11 +1541,19 @@ function Room:askForCardsChosen(chooser, target, min, max, flag, reason) table.insertTable(handcards, t[2]) end end - if #handcards == 0 then return {} end + if #handcards == 0 then + return {} + end ret = table.random(handcards, math.min(min, #handcards)) end - local new_ret = table.filter(ret, function(id) return id ~= -1 end) + local new_ret = + table.filter( + ret, + function(id) + return id ~= -1 + end + ) local hand_num = #ret - #new_ret if hand_num > 0 then table.insertTable(new_ret, table.random(target:getCardIds(Player.Hand), hand_num)) @@ -1457,33 +1562,6 @@ 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[] @ 可选选项列表 @@ -1493,16 +1571,26 @@ end ---@param all_choices string[]|nil @ 所有选项(不可选变灰) ---@return string @ 选择的选项 function Room:askForChoice(player, choices, skill_name, prompt, detailed, all_choices) - if #choices == 1 and not all_choices then return choices[1] end - assert(not all_choices or table.every(choices, function(c) return table.contains(all_choices, c) end)) + if #choices == 1 and not all_choices then + return choices[1] + end + assert( + not all_choices or + table.every( + choices, + function(c) + return table.contains(all_choices, c) + end + ) + ) 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{ - choices, all_choices, skill_name, prompt, detailed - }) - if result == "" then result = choices[1] end + local result = self:doRequest(player, command, json.encode { choices, all_choices, skill_name, prompt, detailed }) + if result == "" then + result = choices[1] + end return result end @@ -1516,12 +1604,14 @@ 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 + local result = self:doRequest(player, command, json.encode { skill_name, prompt, data }) + if result ~= "" then + invoked = true + end return invoked end ---为使用牌增减目标 +-- 为使用牌增减目标 ---@param player ServerPlayer @ 执行的玩家 ---@param targets ServerPlayer[] @ 可选的目标范围 ---@param num integer @ 可选的目标数 @@ -1538,12 +1628,23 @@ function Room:askForAddTarget(player, targets, num, can_minus, distance_limited, local room = player.room local tos = {} local orig_tos = table.simpleClone(AimGroup:getAllTargets(data.tos)) - if can_minus and #orig_tos > 1 then --默认不允许减目标至0 - tos = table.map(table.filter(targets, function(p) - return table.contains(AimGroup:getAllTargets(data.tos), p.id) end), Util.IdMapper) + if can_minus and #orig_tos > 1 then -- 默认不允许减目标至0 + tos = + table.map( + table.filter( + targets, + function(p) + return table.contains(AimGroup:getAllTargets(data.tos), p.id) + end + ), + Util.IdMapper + ) end for _, p in ipairs(targets) do - if not table.contains(AimGroup:getAllTargets(data.tos), p.id) and not room:getPlayerById(data.from):isProhibited(p, data.card) then + if + not table.contains(AimGroup:getAllTargets(data.tos), p.id) and + not room:getPlayerById(data.from):isProhibited(p, data.card) + then if data.card.skill:modTargetFilter(p.id, orig_tos, data.from, data.card, distance_limited) then table.insertIfNeed(tos, p.id) end @@ -1551,18 +1652,35 @@ function Room:askForAddTarget(player, targets, num, can_minus, distance_limited, end if #tos > 0 then tos = room:askForChoosePlayers(player, tos, 1, num, prompt, skillName, true) - --借刀……! + -- 借刀……! if data.card.name ~= "collateral" then return tos else local result = {} for _, id in ipairs(tos) do local to = room:getPlayerById(id) - local target = room:askForChoosePlayers(player, table.map(table.filter(room:getOtherPlayers(player), function(v) - return to:inMyAttackRange(v) end), function(p) return p.id end), 1, 1, - "#collateral-choose::"..to.id..":"..data.card:toLogString(), "collateral_skill", true) + local target = + room:askForChoosePlayers( + player, + table.map( + table.filter( + room:getOtherPlayers(player), + function(v) + return to:inMyAttackRange(v) + end + ), + function(p) + return p.id + end + ), + 1, + 1, + "#collateral-choose::" .. to.id .. ":" .. data.card:toLogString(), + "collateral_skill", + true + ) if #target > 0 then - table.insert(result, {id, target[1]}) + table.insert(result, { id, target[1] }) end end if #result > 0 then @@ -1601,7 +1719,10 @@ function Room:askForGuanxing(player, cards, top_limit, bottom_limit, customNotif assert(bottom_limit[1] <= bottom_limit[2], "limits error: The upper limit should be less than the lower limit") end if #top_limit > 0 and #bottom_limit > 0 then - assert(#cards >= top_limit[1] + bottom_limit[1] and #cards <= top_limit[2] + bottom_limit[2], "limits Error: No enough space") + assert( + #cards >= top_limit[1] + bottom_limit[1] and #cards <= top_limit[2] + bottom_limit[2], + "limits Error: No enough space" + ) end if areaNames then assert(#areaNames == 2, "areaNames error: Should have 2 elements") @@ -1616,7 +1737,7 @@ function Room:askForGuanxing(player, cards, top_limit, bottom_limit, customNotif min_bottom_cards = bottom_limit and bottom_limit[1] or 0, max_bottom_cards = bottom_limit and bottom_limit[2] or #cards, top_area_name = areaNames and areaNames[1] or "Top", - bottom_area_name = areaNames and areaNames[2] or "Bottom", + bottom_area_name = areaNames and areaNames[2] or "Bottom" } local result = self:doRequest(player, command, json.encode(data)) @@ -1632,7 +1753,15 @@ function Room:askForGuanxing(player, cards, top_limit, bottom_limit, customNotif end else top = table.random(cards, top_limit and top_limit[2] or #cards) or Util.DummyTable - bottom = table.shuffle(table.filter(cards, function(id) return not table.contains(top, id) end)) or Util.DummyTable + bottom = + table.shuffle( + table.filter( + cards, + function(id) + return not table.contains(top, id) + end + ) + ) or Util.DummyTable end if not noPut then @@ -1643,15 +1772,18 @@ function Room:askForGuanxing(player, cards, top_limit, bottom_limit, customNotif table.insert(self.draw_pile, bottom[i]) end - self:sendLog{ + self:sendLog { type = "#GuanxingResult", from = player.id, arg = #top, - arg2 = #bottom, + arg2 = #bottom } end - return { top = top, bottom = bottom } + return { + top = top, + bottom = bottom + } end --- 询问玩家任意交换几堆牌堆。 @@ -1673,7 +1805,7 @@ function Room:askForExchange(player, piles, piles_name, customNotify) self:notifyMoveFocus(player, customNotify or command) local data = { piles = piles, - piles_name = piles_name, + piles_name = piles_name } local result = self:doRequest(player, command, json.encode(data)) if result ~= "" then @@ -1683,6 +1815,7 @@ function Room:askForExchange(player, piles, piles_name, customNotify) return piles end end + --- 平时写DIY用不到的函数。 ---@param player ServerPlayer ---@param data string @@ -1695,22 +1828,30 @@ function Room:handleUseCardReply(player, data) local card_data = json.decode(card) local skill = Fk.skills[card_data.skill] local selected_cards = card_data.subcards - if skill.interaction then skill.interaction.data = data.interaction_data end + if skill.interaction then + skill.interaction.data = data.interaction_data + end if skill:isInstanceOf(ActiveSkill) then - self:useSkill(player, skill, function() - self:doIndicate(player.id, targets) - skill:onUse(self, { - from = player.id, - cards = selected_cards, - tos = targets, - }) - end) - return nil + self:useSkill( + player, + skill, + function() + self:doIndicate(player.id, targets) + skill:onUse( + self, + { + from = player.id, + cards = selected_cards, + tos = targets + } + ) + end + ) elseif skill:isInstanceOf(ViewAsSkill) then Self = player local c = skill:viewAs(selected_cards) if c then - local use = {} ---@type CardUseStruct + local use = {} ---@type CardUseStruct use.from = player.id use.tos = {} for _, target in ipairs(targets) do @@ -1732,26 +1873,30 @@ function Room:handleUseCardReply(player, data) if data.special_skill then local skill = Fk.skills[data.special_skill] assert(skill:isInstanceOf(ActiveSkill)) - skill:onUse(self, { - from = player.id, - cards = { card }, - tos = targets, - }) - return nil + skill:onUse( + self, + { + from = player.id, + cards = { card }, + tos = targets + } + ) + else + local use = {} ---@type CardUseStruct + use.from = player.id + use.tos = {} + for _, target in ipairs(targets) do + table.insert(use.tos, { target }) + end + if #use.tos == 0 then + use.tos = nil + end + Fk:filterCard(card, player) + use.card = Fk:getCardById(card) + return use end - local use = {} ---@type CardUseStruct - use.from = player.id - use.tos = {} - for _, target in ipairs(targets) do - table.insert(use.tos, { target }) - end - if #use.tos == 0 then - use.tos = nil - end - Fk:filterCard(card, player) - use.card = Fk:getCardById(card) - return use end + return nil end -- available extra_data: @@ -1769,67 +1914,89 @@ end ---@param event_data CardEffectEvent|nil @ 事件信息 ---@return CardUseStruct | nil @ 返回关于本次使用牌的数据,以便后续处理 function Room:askForUseCard(player, card_name, pattern, prompt, cancelable, extra_data, event_data) - if event_data and (event_data.disresponsive or table.contains(event_data.disresponsiveList or Util.DummyTable, player.id)) then + if + event_data and + (event_data.disresponsive or table.contains(event_data.disresponsiveList or Util.DummyTable, player.id)) + then return nil end if event_data and event_data.prohibitedCardNames and card_name then local splitedCardNames = card_name:split(",") - splitedCardNames = table.filter(splitedCardNames, function(name) - return not table.contains(event_data.prohibitedCardNames, name) - end) + splitedCardNames = + table.filter( + splitedCardNames, + function(name) + return not table.contains(event_data.prohibitedCardNames, name) + end + ) if #splitedCardNames == 0 then return nil end - card_name = table.concat(splitedCardNames, ",") end - if extra_data then if extra_data.bypass_distances then player.room:setPlayerMark(player, MarkEnum.BypassDistancesLimit .. "-tmp", 1) end - if extra_data.bypass_times == nil or extra_data.bypass_times then + if extra_data.bypass_times ~= false then player.room:setPlayerMark(player, MarkEnum.BypassTimesLimit .. "-tmp", 1) end + fk.useMustTargets = extra_data.must_targets end local command = "AskForUseCard" self:notifyMoveFocus(player, card_name) - cancelable = (cancelable == nil) and true or cancelable - extra_data = extra_data or Util.DummyTable + cancelable = cancelable ~= false pattern = pattern or card_name prompt = prompt or "" - local askForUseCardData = { + local useData = { user = player, cardName = card_name, pattern = pattern, - extraData = extra_data, - eventData = event_data, + extraData = extra_data or Util.DummyTable, + eventData = event_data } - self.logic:trigger(fk.AskForCardUse, player, askForUseCardData) - - if askForUseCardData.result and type(askForUseCardData.result) == 'table' then - player.room:setPlayerMark(player, MarkEnum.BypassDistancesLimit .. "-tmp", 0) - player.room:setPlayerMark(player, MarkEnum.BypassTimesLimit .. "-tmp", 0) - return askForUseCardData.result - else - local data = {card_name, pattern, prompt, cancelable, extra_data} + local use = nil + self.logic:trigger(fk.AskForCardUse, player, useData) + if type(useData.result) == "table" then + useData = useData.result + useData.extraUse = extra_data ~= nil + self:useCard(useData) + if useData.nullified then + use = false + elseif useData.breakEvent ~= true then + use = useData + end + end + local data = { card_name, pattern, prompt, cancelable, extra_data or Util.DummyTable } + while use == nil do Fk.currentResponsePattern = pattern local result = self:doRequest(player, command, json.encode(data)) Fk.currentResponsePattern = nil - if result ~= "" then - player.room:setPlayerMark(player, MarkEnum.BypassDistancesLimit .. "-tmp", 0) - player.room:setPlayerMark(player, MarkEnum.BypassTimesLimit .. "-tmp", 0) - return self:handleUseCardReply(player, result) + result = self:handleUseCardReply(player, result) + if result then + result.extraUse = extra_data ~= nil + self:useCard(result) + if result.nullified then + use = false + elseif result.breakEvent ~= true then + use = result + end + else + break + end + else + break end end + fk.useMustTargets = nil player.room:setPlayerMark(player, MarkEnum.BypassDistancesLimit .. "-tmp", 0) player.room:setPlayerMark(player, MarkEnum.BypassTimesLimit .. "-tmp", 0) - return nil + return use end --- 询问一名玩家打出一张牌。 @@ -1840,15 +2007,19 @@ end ---@param cancelable bool @ 能否取消 ---@param extra_data any|nil @ 额外数据 ---@param effectData CardEffectEvent|nil @ 关联的卡牌生效流程 +---@param retrial bool @ 改判打出 ---@return Card | nil @ 打出的牌 -function Room:askForResponse(player, card_name, pattern, prompt, cancelable, extra_data, effectData) - if effectData and (effectData.disresponsive or table.contains(effectData.disresponsiveList or Util.DummyTable, player.id)) then +function Room:askForResponse(player, card_name, pattern, prompt, cancelable, extra_data, effectData, retrial) + if + effectData and + (effectData.disresponsive or table.contains(effectData.disresponsiveList or Util.DummyTable, player.id)) + then return nil end local command = "AskForResponseCard" self:notifyMoveFocus(player, card_name) - cancelable = (cancelable == nil) and true or cancelable + cancelable = cancelable ~= false extra_data = extra_data or Util.DummyTable pattern = pattern or card_name prompt = prompt or "" @@ -1857,27 +2028,52 @@ function Room:askForResponse(player, card_name, pattern, prompt, cancelable, ext user = player, cardName = card_name, pattern = pattern, - extraData = extra_data, + extraData = extra_data } + local response = nil self.logic:trigger(fk.AskForCardResponse, player, eventData) - if eventData.result then - return eventData.result - else - local data = {card_name, pattern, prompt, cancelable, extra_data} - + eventData = { + from = player.id, + card = eventData.result, + skipDrop = true, + retrial = retrial, + responseToEvent = effectData + } + eventData.extraResponse = extra_data ~= nil + self:responseCard(eventData) + if eventData.nullified then + response = false + elseif eventData.breakEvent ~= true then + response = eventData + end + end + local data = { card_name, pattern, prompt, cancelable, extra_data } + while response == nil do Fk.currentResponsePattern = pattern local result = self:doRequest(player, command, json.encode(data)) Fk.currentResponsePattern = nil - if result ~= "" then - local use = self:handleUseCardReply(player, result) - if use then - return use.card + result = self:handleUseCardReply(player, result) + if result then + result.skipDrop = true + result.retrial = retrial + result.responseToEvent = effectData + result.extraResponse = extra_data ~= nil + self:responseCard(result) + if result.nullified then + response = false + elseif result.breakEvent ~= true then + response = result + end + else + break end + else + break end end - return nil + return response end --- 同时询问多名玩家是否使用某一张牌。 @@ -1891,35 +2087,45 @@ end ---@param extra_data any|nil @ 额外信息 ---@return CardUseStruct | nil @ 最终决胜出的卡牌使用信息 function Room:askForNullification(players, card_name, pattern, prompt, cancelable, extra_data) - if #players == 0 then - return nil - end - local command = "AskForUseCard" card_name = card_name or "nullification" - cancelable = (cancelable == nil) and true or cancelable - extra_data = extra_data or Util.DummyTable + cancelable = cancelable ~= false prompt = prompt or "" pattern = pattern or card_name self:notifyMoveFocus(self.alive_players, card_name) self:doBroadcastNotify("WaitForNullification", "") - - local data = {card_name, pattern, prompt, cancelable, extra_data} - - Fk.currentResponsePattern = pattern - local winner = self:doRaceRequest(command, players, json.encode(data)) - - if winner then - local result = winner.client_reply - return self:handleUseCardReply(winner, result) + local use = nil + local data = { card_name, pattern, prompt, cancelable, extra_data or Util.DummyTable } + while use == nil do + Fk.currentResponsePattern = pattern + local winner = self:doRaceRequest(command, players, json.encode(data)) + Fk.currentResponsePattern = nil + if winner then + local reply = self:handleUseCardReply(winner, winner.client_reply) + if reply then + local event = self.logic:getCurrentEvent():findParent(GameEvent.CardEffect, true).data[1] + reply.responseToEvent = event + reply.toCard = event.card + reply.extraUse = extra_data ~= nil + self:useCard(reply) + if reply.nullified then + use = false + elseif reply.breakEvent ~= true then + use = reply + end + else + break + end + else + break + end end - Fk.currentResponsePattern = nil - return nil + return use end -- AG(a.k.a. Amazing Grace) functions --- Popup a box that contains many cards, then ask player to choose one +-- Popup a box that contains many cards, then ask player:delay(1200) to choose one --- 询问玩家从AG中选择一张牌。 ---@param player ServerPlayer @ 要询问的玩家 @@ -1950,7 +2156,7 @@ end function Room:fillAG(player, id_list, disable_ids) id_list = Card:getIdList(id_list) -- disable_ids = Card:getIdList(disable_ids) - player:doNotify("FillAG", json.encode{ id_list, disable_ids }) + player:doNotify("FillAG", json.encode { id_list, disable_ids }) end --- 告诉一些玩家,AG中的牌被taker取走了。 @@ -1958,7 +2164,7 @@ end ---@param id integer @ 被拿走的牌 ---@param notify_list ServerPlayer[]|nil @ 要告知的玩家,默认为全员 function Room:takeAG(taker, id, notify_list) - self:doBroadcastNotify("TakeAG", json.encode{ taker.id, id }, notify_list) + self:doBroadcastNotify("TakeAG", json.encode { taker.id, id }, notify_list) end --- 关闭player那侧显示的AG。 @@ -1966,8 +2172,11 @@ end --- 若不传参(即player为nil),那么关闭所有玩家的AG。 ---@param player ServerPlayer|nil @ 要关闭AG的玩家 function Room:closeAG(player) - if player then player:doNotify("CloseAG", "") - else self:doBroadcastNotify("CloseAG", "") end + if player then + player:doNotify("CloseAG", "") + else + self:doBroadcastNotify("CloseAG", "") + end end -- Show a qml dialog and return qml's ClientInstance.replyToServer @@ -1981,10 +2190,14 @@ end function Room:askForCustomDialog(player, focustxt, qmlPath, extra_data) local command = "CustomDialog" self:notifyMoveFocus(player, focustxt) - return self:doRequest(player, command, json.encode{ - path = qmlPath, - data = extra_data, - }) + return self:doRequest( + player, + command, + json.encode { + path = qmlPath, + data = extra_data + } + ) end ---@param player ServerPlayer @ 移动的操作 @@ -2022,12 +2235,15 @@ function Room:askForMoveCardInBoard(player, targetOne, targetTwo, skillName, fla end if #cards > 0 then - table.sort(cards, function(prev, next) - local prevSubType = Fk:getCardById(prev).sub_type - local nextSubType = Fk:getCardById(next).sub_type + table.sort( + cards, + function(prev, next) + local prevSubType = Fk:getCardById(prev).sub_type + local nextSubType = Fk:getCardById(next).sub_type - return prevSubType < nextSubType - end) + return prevSubType < nextSubType + end + ) for _, id in ipairs(cards) do table.insert(cardsPosition, self:getCardOwner(id) == targetOne and 0 or 1) @@ -2058,7 +2274,8 @@ function Room:askForMoveCardInBoard(player, targetOne, targetTwo, skillName, fla return end - local firstGeneralName = targetOne.general + (targetOne.deputyGeneral ~= "" and ("/" .. targetOne.deputyGeneral) or "") + local firstGeneralName = + targetOne.general + (targetOne.deputyGeneral ~= "" and ("/" .. targetOne.deputyGeneral) or "") local secGeneralName = targetTwo.general + (targetTwo.deputyGeneral ~= "" and ("/" .. targetTwo.deputyGeneral) or "") local data = { @@ -2073,12 +2290,15 @@ function Room:askForMoveCardInBoard(player, targetOne, targetTwo, skillName, fla if result == "" then local randomIndex = math.random(1, #cards) - result = { cardId = cards[randomIndex], pos = cardsPosition[randomIndex] } + result = { + cardId = cards[randomIndex], + pos = cardsPosition[randomIndex] + } else result = json.decode(result) end - local from, to = result.pos == 0 and targetOne, targetTwo or targetTwo, targetOne + local from, to = result.pos == 0 and targetOne, targetTwo and targetTwo or targetOne local cardToMove = self:getCardOwner(result.cardId):getVirualEquip(result.cardId) or Fk:getCardById(result.cardId) self:moveCardTo( cardToMove, @@ -2091,7 +2311,11 @@ function Room:askForMoveCardInBoard(player, targetOne, targetTwo, skillName, fla player.id ) - return { card = cardToMove, from = from.id, to = to.id } + return { + card = cardToMove, + from = from.id, + to = to.id + } end --- 询问一名玩家从targets中选择出若干名玩家来移动场上的牌。 @@ -2113,16 +2337,17 @@ function Room:askForChooseToMoveCardInBoard(player, prompt, skillName, cancelabl local data = { flag = flag, skillName = skillName, - excludeIds = excludeIds, + excludeIds = excludeIds } - local _, ret = self:askForUseActiveSkill( - player, - "choose_players_to_move_card_in_board", - prompt or "", - cancelable, - data, - no_indicate - ) + local _, ret = + self:askForUseActiveSkill( + player, + "choose_players_to_move_card_in_board", + prompt or "", + cancelable, + data, + no_indicate + ) if ret then return ret.targets @@ -2151,8 +2376,7 @@ end ---@param aimEventCollaborators table ---@return boolean local onAim = function(room, cardUseEvent, aimEventCollaborators) - local eventStages = { fk.TargetSpecifying, fk.TargetConfirming, fk.TargetSpecified, fk.TargetConfirmed } - for _, stage in ipairs(eventStages) do + for _, stage in ipairs({ fk.TargetSpecifying, fk.TargetConfirming, fk.TargetSpecified, fk.TargetConfirmed }) do if (not cardUseEvent.tos) or #cardUseEvent.tos == 0 then return false end @@ -2180,7 +2404,7 @@ local onAim = function(room, cardUseEvent, aimEventCollaborators) firstTarget = firstTarget, additionalDamage = cardUseEvent.additionalDamage, additionalRecover = cardUseEvent.additionalRecover, - extra_data = cardUseEvent.extra_data, + extra_data = cardUseEvent.extra_data } local index = 1 @@ -2212,7 +2436,14 @@ local onAim = function(room, cardUseEvent, aimEventCollaborators) firstTarget = false - if room.logic:trigger(stage, (stage == fk.TargetSpecifying or stage == fk.TargetSpecified) and room:getPlayerById(aimStruct.from) or room:getPlayerById(aimStruct.to), aimStruct) then + if + room.logic:trigger( + stage, + (stage == fk.TargetSpecifying or stage == fk.TargetSpecified) and room:getPlayerById(aimStruct.from) or + room:getPlayerById(aimStruct.to), + aimStruct + ) + then return false end AimGroup:removeDeadTargets(room, aimStruct) @@ -2264,7 +2495,7 @@ end function Room:doCardUseEffect(cardUseEvent) ---@type table local aimEventCollaborators = {} - if cardUseEvent.tos and not onAim(self, cardUseEvent, aimEventCollaborators) then + if cardUseEvent.tos and not onAim(self, cardUseEvent, aimEventCollaborators) or cardUseEvent.nullified then return end @@ -2278,11 +2509,13 @@ function Room:doCardUseEffect(cardUseEvent) end if self:getPlayerById(TargetGroup:getRealTargets(cardUseEvent.tos)[1]).dead then - self:moveCards({ - ids = realCardIds, - toArea = Card.DiscardPile, - moveReason = fk.ReasonPutIntoDiscardPile, - }) + self:moveCards( + { + ids = realCardIds, + toArea = Card.DiscardPile, + moveReason = fk.ReasonPutIntoDiscardPile + } + ) else local target = TargetGroup:getRealTargets(cardUseEvent.tos)[1] local existingEquipId = self:getPlayerById(target):getEquipment(cardUseEvent.card.sub_type) @@ -2292,22 +2525,24 @@ function Room:doCardUseEffect(cardUseEvent) ids = { existingEquipId }, from = target, toArea = Card.DiscardPile, - moveReason = fk.ReasonPutIntoDiscardPile, + moveReason = fk.ReasonPutIntoDiscardPile }, { ids = realCardIds, to = target, toArea = Card.PlayerEquip, - moveReason = fk.ReasonUse, + moveReason = fk.ReasonUse } ) else - self:moveCards({ - ids = realCardIds, - to = target, - toArea = Card.PlayerEquip, - moveReason = fk.ReasonUse, - }) + self:moveCards( + { + ids = realCardIds, + to = target, + toArea = Card.PlayerEquip, + moveReason = fk.ReasonUse + } + ) end end @@ -2331,22 +2566,26 @@ function Room:doCardUseEffect(cardUseEvent) self:getPlayerById(target):addVirtualEquip(cardUseEvent.card) end - self:moveCards({ - ids = realCardIds, - to = target, - toArea = Card.PlayerJudge, - moveReason = fk.ReasonUse, - }) + self:moveCards( + { + ids = realCardIds, + to = target, + toArea = Card.PlayerJudge, + moveReason = fk.ReasonUse + } + ) return end end - self:moveCards({ - ids = realCardIds, - toArea = Card.DiscardPile, - moveReason = fk.ReasonPutIntoDiscardPile, - }) + self:moveCards( + { + ids = realCardIds, + toArea = Card.DiscardPile, + moveReason = fk.ReasonPutIntoDiscardPile + } + ) return end @@ -2369,7 +2608,7 @@ function Room:doCardUseEffect(cardUseEvent) additionalRecover = cardUseEvent.additionalRecover, cardsResponded = cardUseEvent.cardsResponded, prohibitedCardNames = cardUseEvent.prohibitedCardNames, - extra_data = cardUseEvent.extra_data, + extra_data = cardUseEvent.extra_data } -- If using card to other card (like jink or nullification), simply effect and return @@ -2441,10 +2680,13 @@ end ---@param cardEffectEvent CardEffectEvent function Room:handleCardEffect(event, cardEffectEvent) if event == fk.PreCardEffect then - if cardEffectEvent.card.skill:aboutToEffect(self, cardEffectEvent) then return end + if cardEffectEvent.card.skill:aboutToEffect(self, cardEffectEvent) then + return + end if - cardEffectEvent.card.trueName == "slash" and - not (cardEffectEvent.unoffsetable or table.contains(cardEffectEvent.unoffsetableList or Util.DummyTable, cardEffectEvent.to)) + cardEffectEvent.card.trueName == "slash" and + not (cardEffectEvent.unoffsetable or + table.contains(cardEffectEvent.unoffsetableList or Util.DummyTable, cardEffectEvent.to)) then local loopTimes = 1 if cardEffectEvent.fixedResponseTimes then @@ -2455,51 +2697,36 @@ function Room:handleCardEffect(event, cardEffectEvent) end end Fk.currentResponsePattern = "jink" - for i = 1, loopTimes do local to = self:getPlayerById(cardEffectEvent.to) - local prompt = "" + local prompt = nil if cardEffectEvent.from then - prompt = "#slash-jink:" .. cardEffectEvent.from .. "::" .. 1 + if loopTimes > 1 then + prompt = "#slash-jinks:" .. cardEffectEvent.from .. "::" .. loopTimes + 1 - i .. ":" .. loopTimes + else + prompt = "#slash-jink:" .. cardEffectEvent.from .. "::1" + end end - - local use = self:askForUseCard( - to, - "jink", - nil, - prompt, - true, - nil, - cardEffectEvent - ) - if use then - use.toCard = cardEffectEvent.card - use.responseToEvent = cardEffectEvent - self:useCard(use) - end - - if not cardEffectEvent.isCancellOut then + if self:askForUseCard(to, "jink", nil, prompt, true, nil, cardEffectEvent) then + cardEffectEvent.isCancellOut = true + else + cardEffectEvent.isCancellOut = false break end - - cardEffectEvent.isCancellOut = i == loopTimes end elseif - cardEffectEvent.card.type == Card.TypeTrick and - not (cardEffectEvent.disresponsive or cardEffectEvent.unoffsetable) and - not table.contains(cardEffectEvent.prohibitedCardNames or Util.DummyTable, "nullification") + cardEffectEvent.card.type == Card.TypeTrick and + not (cardEffectEvent.disresponsive or cardEffectEvent.unoffsetable) and + not table.contains(cardEffectEvent.prohibitedCardNames or Util.DummyTable, "nullification") then local players = {} Fk.currentResponsePattern = "nullification" for _, p in ipairs(self.alive_players) do - local cards = p:getHandlyIds(true) - for _, cid in ipairs(cards) do + for _, cid in ipairs(p:getHandlyIds(true)) do if - Fk:getCardById(cid).trueName == "nullification" and - not ( - table.contains(cardEffectEvent.disresponsiveList or Util.DummyTable, p.id) or - table.contains(cardEffectEvent.unoffsetableList or Util.DummyTable, p.id) - ) + Fk:getCardById(cid).trueName == "nullification" and + not (table.contains(cardEffectEvent.disresponsiveList or Util.DummyTable, p.id) or + table.contains(cardEffectEvent.unoffsetableList or Util.DummyTable, p.id)) then table.insert(players, p) break @@ -2509,13 +2736,10 @@ function Room:handleCardEffect(event, cardEffectEvent) Self = p -- for enabledAtResponse for _, s in ipairs(table.connect(p.player_skills, p._fake_skills)) do if - s.pattern and - Exppattern:Parse("nullification"):matchExp(s.pattern) and - not (s.enabledAtResponse and not s:enabledAtResponse(p)) and - not ( - table.contains(cardEffectEvent.disresponsiveList or Util.DummyTable, p.id) or - table.contains(cardEffectEvent.unoffsetableList or Util.DummyTable, p.id) - ) + s.pattern and Exppattern:Parse("nullification"):matchExp(s.pattern) and + not (s.enabledAtResponse and not s:enabledAtResponse(p)) and + not (table.contains(cardEffectEvent.disresponsiveList or Util.DummyTable, p.id) or + table.contains(cardEffectEvent.unoffsetableList or Util.DummyTable, p.id)) then table.insert(players, p) break @@ -2535,22 +2759,25 @@ function Room:handleCardEffect(event, cardEffectEvent) if #TargetGroup:getRealTargets(cardEffectEvent.tos) > 1 then local parentUseEvent = self.logic:getCurrentEvent():findParent(GameEvent.UseCard) if parentUseEvent then - extra_data = { useEventId = parentUseEvent.id, effectTo = cardEffectEvent.to } + extra_data = { + useEventId = parentUseEvent.id, + effectTo = cardEffectEvent.to + } end end - local use = self:askForNullification(players, nil, nil, prompt, true, extra_data) - if use then - use.toCard = cardEffectEvent.card - use.responseToEvent = cardEffectEvent - self:useCard(use) - end + self:askForNullification(players, nil, nil, prompt, true, extra_data) end Fk.currentResponsePattern = nil elseif event == fk.CardEffecting then if cardEffectEvent.card.skill then - execGameEvent(GameEvent.SkillEffect, function () - cardEffectEvent.card.skill:onEffect(self, cardEffectEvent) - end, self:getPlayerById(cardEffectEvent.from), cardEffectEvent.card.skill) + execGameEvent( + GameEvent.SkillEffect, + function() + cardEffectEvent.card.skill:onEffect(self, cardEffectEvent) + end, + self:getPlayerById(cardEffectEvent.from), + cardEffectEvent.card.skill + ) end end end @@ -2571,22 +2798,36 @@ function Room:useVirtualCard(card_name, subcards, from, tos, skillName, extra) local card = Fk:cloneCard(card_name) card.skillName = skillName - if from:prohibitUse(card) then return false end + if from:prohibitUse(card) then + return false + end - if tos.class then tos = { tos } end + if tos.class then + tos = { tos } + end for i, p in ipairs(tos) do if from:isProhibited(p, card) then table.remove(tos, i) end end - if #tos == 0 then return false end + if #tos == 0 then + return false + end - if subcards then card:addSubcards(Card:getIdList(subcards)) end + if subcards then + card:addSubcards(Card:getIdList(subcards)) + end local use = {} ---@type CardUseStruct use.from = from.id - use.tos = table.map(tos, function(p) return { p.id } end) + use.tos = + table.map( + tos, + function(p) + return { p.id } + end + ) use.card = card use.extraUse = extra self:useCard(use) @@ -2613,25 +2854,29 @@ end function Room:obtainCard(player, cid, unhide, reason) if type(cid) ~= "number" then assert(cid and cid:isInstanceOf(Card)) - cid = cid:isVirtual() and cid.subcards or {cid.id} + cid = cid:isVirtual() and cid.subcards or { cid.id } else - cid = {cid} + cid = { cid } + end + if #cid == 0 then + return end - if #cid == 0 then return end if type(player) == "table" then player = player.id end - self:moveCards({ - ids = cid, - from = self.owner_map[cid[1]], - to = player, - toArea = Card.PlayerHand, - moveReason = reason or fk.ReasonJustMove, - proposer = player, - moveVisible = unhide or false, - }) + self:moveCards( + { + ids = cid, + from = self.owner_map[cid[1]], + to = player, + toArea = Card.PlayerHand, + moveReason = reason or fk.ReasonJustMove, + proposer = player, + moveVisible = unhide or false + } + ) end --- 让玩家摸牌 @@ -2645,7 +2890,7 @@ function Room:drawCards(player, num, skillName, fromPlace) who = player, num = num, skillName = skillName, - fromPlace = fromPlace, + fromPlace = fromPlace } if self.logic:trigger(fk.BeforeDrawCard, player, drawData) then self.logic:breakEvent(false) @@ -2655,14 +2900,16 @@ function Room:drawCards(player, num, skillName, fromPlace) fromPlace = drawData.fromPlace local topCards = self:getNCards(num, fromPlace) - self:moveCards({ - ids = topCards, - to = player.id, - toArea = Card.PlayerHand, - moveReason = fk.ReasonDraw, - proposer = player.id, - skillName = skillName, - }) + self:moveCards( + { + ids = topCards, + to = player.id, + toArea = Card.PlayerHand, + moveReason = fk.ReasonDraw, + proposer = player.id, + skillName = skillName + } + ) return { table.unpack(topCards) } end @@ -2684,32 +2931,37 @@ function Room:moveCardTo(card, to_place, target, reason, skill_name, special_nam local ids = Card:getIdList(card) local to - if table.contains( - {Card.PlayerEquip, Card.PlayerHand, - Card.PlayerJudge, Card.PlayerSpecial}, to_place) then + if table.contains({ Card.PlayerEquip, Card.PlayerHand, Card.PlayerJudge, Card.PlayerSpecial }, to_place) then to = target.id end local movesSplitedByOwner = {} for _, cardId in ipairs(ids) do - local moveFound = table.find(movesSplitedByOwner, function(move) - return move.from == self.owner_map[cardId] - end) + local moveFound = + table.find( + movesSplitedByOwner, + function(move) + return move.from == self.owner_map[cardId] + end + ) if moveFound then table.insert(moveFound.ids, cardId) else - table.insert(movesSplitedByOwner, { - ids = { cardId }, - from = self.owner_map[cardId], - to = to, - toArea = to_place, - moveReason = reason, - skillName = skill_name, - specialName = special_name, - moveVisible = visible, - proposer = proposer, - }) + table.insert( + movesSplitedByOwner, + { + ids = { cardId }, + from = self.owner_map[cardId], + to = to, + toArea = to_place, + moveReason = reason, + skillName = skill_name, + specialName = special_name, + moveVisible = visible, + proposer = proposer + } + ) end end @@ -2737,7 +2989,9 @@ end ---@param player ServerPlayer ---@param num integer @ 变化量 function Room:changeShield(player, num) - if num == 0 then return end + if num == 0 then + return + end player.shield = math.max(player.shield + num, 0) player.shield = math.min(player.shield, 5) self:broadcastProperty(player, "shield") @@ -2802,10 +3056,14 @@ function Room:handleAddLoseSkills(player, skill_names, source_skill, sendlog, no skill_names = skill_names:split("|") end - if sendlog == nil then sendlog = true end + if sendlog == nil then + sendlog = true + end - if #skill_names == 0 then return end - local losts = {} ---@type boolean[] + if #skill_names == 0 then + return + end + local losts = {} ---@type boolean[] local triggers = {} ---@type Skill[] for _, skill in ipairs(skill_names) do if string.sub(skill, 1, 1) == "-" then @@ -2813,13 +3071,10 @@ function Room:handleAddLoseSkills(player, skill_names, source_skill, sendlog, no if player:hasSkill(actual_skill, true, true) then local lost_skills = player:loseSkill(actual_skill, source_skill) for _, s in ipairs(lost_skills) do - self:doBroadcastNotify("LoseSkill", json.encode{ - player.id, - s.name - }) + self:doBroadcastNotify("LoseSkill", json.encode { player.id, s.name }) if sendlog and s.visible then - self:sendLog{ + self:sendLog { type = "#LoseSkill", from = player.id, arg = s.name @@ -2838,13 +3093,10 @@ function Room:handleAddLoseSkills(player, skill_names, source_skill, sendlog, no for _, s in ipairs(got_skills) do -- TODO: limit skill mark - self:doBroadcastNotify("AddSkill", json.encode{ - player.id, - s.name - }) + self:doBroadcastNotify("AddSkill", json.encode { player.id, s.name }) if sendlog and s.visible then - self:sendLog{ + self:sendLog { type = "#AcquireSkill", from = player.id, arg = s.name @@ -2881,7 +3133,9 @@ end ---@param skillName string|nil @ 技能名 ---@param exchange bool @ 是否要替换原有判定牌(即类似鬼道那样) function Room:retrial(card, player, judge, skillName, exchange) - if not card then return end + if not card then + return + end local triggerResponded = self.owner_map[card:getEffectiveId()] == player local isHandcard = (triggerResponded and self:getCardArea(card:getEffectiveId()) == Card.PlayerHand) @@ -2911,12 +3165,12 @@ function Room:retrial(card, player, judge, skillName, exchange) move2.moveReason = fk.ReasonJustMove move2.to = exchange and player.id or nil - self:sendLog{ + self:sendLog { type = "#ChangedJudge", from = player.id, to = { judge.who.id }, card = { card:getEffectiveId() }, - arg = skillName, + arg = skillName } self:moveCards(move1, move2) @@ -2933,18 +3187,20 @@ end ---@param thrower ServerPlayer|nil @ 弃别人牌的人 function Room:throwCard(card_ids, skillName, who, thrower) if type(card_ids) == "number" then - card_ids = {card_ids} + card_ids = { card_ids } end skillName = skillName or "" thrower = thrower or who - self:moveCards({ - ids = card_ids, - from = who.id, - toArea = Card.DiscardPile, - moveReason = fk.ReasonDiscard, - proposer = thrower.id, - skillName = skillName - }) + self:moveCards( + { + ids = card_ids, + from = who.id, + toArea = Card.DiscardPile, + moveReason = fk.ReasonDiscard, + proposer = thrower.id, + skillName = skillName + } + ) end --- 重铸一名角色的牌。 @@ -2953,22 +3209,24 @@ end ---@param skillName string|nil @ 技能名,默认为“重铸” function Room:recastCard(card_ids, who, skillName) if type(card_ids) == "number" then - card_ids = {card_ids} + card_ids = { card_ids } end skillName = skillName or "recast" - self:moveCards({ - ids = card_ids, - from = who.id, - toArea = Card.DiscardPile, - skillName = skillName, - moveReason = fk.ReasonPutIntoDiscardPile, - proposer = who.id - }) - self:sendLog{ + self:moveCards( + { + ids = card_ids, + from = who.id, + toArea = Card.DiscardPile, + skillName = skillName, + moveReason = fk.ReasonPutIntoDiscardPile, + proposer = who.id + } + ) + self:sendLog { type = skillName == "recast" and "#Recast" or "#RecastBySkill", from = who.id, card = card_ids, - arg = skillName, + arg = skillName } self:drawCards(who, #card_ids, skillName) end @@ -3015,8 +3273,12 @@ function Room:swapSeat(a, b) local ai, bi local players = self.players for i, v in ipairs(self.players) do - if v == a then ai = i end - if v == b then bi = i end + if v == a then + ai = i + end + if v == b then + bi = i + end end players[ai] = b @@ -3088,7 +3350,9 @@ end ---@param player ServerPlayer ---@param sendLog bool function Room:revivePlayer(player, sendLog) - if not player.dead then return end + if not player.dead then + return + end self:setPlayerProperty(player, "dead", false) player._splayer:setDied(false) self:setPlayerProperty(player, "dying", false) @@ -3097,7 +3361,10 @@ function Room:revivePlayer(player, sendLog) sendLog = (sendLog == nil) and true or sendLog if sendLog then - self:sendLog { type = "#Revive", from = player.id } + self:sendLog { + type = "#Revive", + from = player.id + } end end @@ -3110,7 +3377,9 @@ local function shouldUpdateWinRate(room) return false end for _, p in ipairs(room.players) do - if p.id < 0 then return false end + if p.id < 0 then + return false + end end return Fk.game_modes[room.settings.gameMode]:countInFunc(room) end @@ -3118,7 +3387,9 @@ end --- 结束一局游戏。 ---@param winner string @ 获胜的身份,空字符串表示平局 function Room:gameOver(winner) - if not self.game_started then return end + if not self.game_started then + return + end self.logic:trigger(fk.GameFinished, nil, winner) self.game_started = false @@ -3149,10 +3420,7 @@ function Room:gameOver(winner) self.room:gameOver() - if table.contains( - { "running", "normal" }, - coroutine.status(self.main_co) - ) then + if table.contains({ "running", "normal" }, coroutine.status(self.main_co)) then coroutine.yield("__handleRequest", "over") else coroutine.close(self.main_co) @@ -3251,16 +3519,23 @@ function Room:canMoveCardInBoard(flag, players, excludeIds) excludeIds = type(excludeIds) == "table" and excludeIds or {} local targets = {} - table.find(players, function(p) - local canMoveTo = table.find(players, function(another) - return p ~= another and p:canMoveCardsInBoardTo(another, flag, excludeIds) - end) + table.find( + players, + function(p) + local canMoveTo = + table.find( + players, + function(another) + return p ~= another and p:canMoveCardsInBoardTo(another, flag, excludeIds) + end + ) - if canMoveTo then - targets = {p.id, canMoveTo.id} + if canMoveTo then + targets = { p.id, canMoveTo.id } + end + return canMoveTo end - return canMoveTo - end) + ) return targets end @@ -3275,7 +3550,7 @@ function Room:printCard(name, suit, number) Fk:_addPrintedCard(cd) table.insert(self.void, cd.id) self:setCardArea(cd.id, Card.Void, nil) - self:doBroadcastNotify("PrintCard", json.encode{ name, suit, number }) + self:doBroadcastNotify("PrintCard", json.encode { name, suit, number }) return cd end @@ -3285,11 +3560,7 @@ function Room:updateQuestSkillState(player, skillName, failed) self:setPlayerMark(player, MarkEnum.QuestSkillPreName .. skillName, failed and "failed" or "succeed") local updateValue = failed and 2 or 1 - self:doBroadcastNotify("UpdateQuestSkillUI", json.encode{ - player.id, - skillName, - updateValue, - }) + self:doBroadcastNotify("UpdateQuestSkillUI", json.encode { player.id, skillName, updateValue }) end function Room:abortPlayerArea(player, playerSlots) @@ -3331,17 +3602,25 @@ function Room:abortPlayerArea(player, playerSlots) return end - self:moveCards({ - ids = cardsToDrop, - from = player.id, - toArea = Card.DiscardPile, - moveReason = fk.ReasonPutIntoDiscardPile, - }) + self:moveCards( + { + ids = cardsToDrop, + from = player.id, + toArea = Card.DiscardPile, + moveReason = fk.ReasonPutIntoDiscardPile + } + ) table.insertTable(player.sealedSlots, slotsToSeal) self:broadcastProperty(player, "sealedSlots") - self.logic:trigger(fk.AreaAborted, player, { slots = slotsSealed }) + self.logic:trigger( + fk.AreaAborted, + player, + { + slots = slotsSealed + } + ) end function Room:resumePlayerArea(player, playerSlots) @@ -3363,7 +3642,13 @@ function Room:resumePlayerArea(player, playerSlots) if #slotsToResume > 0 then self:broadcastProperty(player, "sealedSlots") - self.logic:trigger(fk.AreaResumed, player, { slots = slotsToResume }) + self.logic:trigger( + fk.AreaResumed, + player, + { + slots = slotsToResume + } + ) end end diff --git a/lua/server/room.lua.rej b/lua/server/room.lua.rej new file mode 100644 index 00000000..db77f6ab --- /dev/null +++ b/lua/server/room.lua.rej @@ -0,0 +1,46 @@ +diff a/lua/server/room.lua b/lua/server/room.lua (rejected hunks) +@@ -1482,41 +1482,18 @@ + local result = self:doRequest(chooser, command, json.encode(data)) + + if result == "" then +- local areas = {} +- local handcards ++ local handcards = {} + if type(flag) == "string" then +- if string.find(flag, "h") then +- table.insert(areas, Player.Hand) +- end +- if string.find(flag, "e") then +- table.insert(areas, Player.Equip) +- end +- if string.find(flag, "j") then +- table.insert(areas, Player.Judge) +- end +- handcards = target:getCardIds(areas) ++ handcards = target:getCardIds(flag) + else +- handcards = {} + for _, t in ipairs(flag.card_data) do + table.insertTable(handcards, t[2]) + end + end +- if #handcards == 0 then +- return +- end +- result = handcards[math.random(1, #handcards)] ++ result = handcards[math.random(1, #handcards)] or -1 + else + result = tonumber(result) + end +- +- if result == -1 then +- local handcards = target:getCardIds(Player.Hand) +- if #handcards == 0 then +- return +- end +- result = table.random(handcards) +- end +- + return result + end + diff --git a/lua/server/serverplayer.lua b/lua/server/serverplayer.lua index 4f253350..26dab31b 100644 --- a/lua/server/serverplayer.lua +++ b/lua/server/serverplayer.lua @@ -45,7 +45,7 @@ function ServerPlayer:initialize(_self) self._prelighted_skills = {} self._timewaste_count = 0 - self.ai = RandomAI:new(self) + self.ai = TrustAI:new(self) end ---@param command string @@ -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:prependExitFunc(function() self:gainAnExtraPhase(phase, false) end) + turn:addExitFunc(function() self:gainAnExtraPhase(phase, false) end) return end end @@ -484,6 +484,7 @@ function ServerPlayer:gainAnExtraPhase(phase, delay) arg = phase_name_table[phase], } + GameEvent(GameEvent.Phase, self, self.phase):exec() self.phase = current @@ -579,7 +580,6 @@ 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:prependExitFunc(function() self:gainAnExtraTurn(false) end) + turn:addExitFunc(function() self:gainAnExtraTurn(false) end) return end end @@ -604,32 +604,10 @@ 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 @@ -1013,4 +991,8 @@ function ServerPlayer:removeBuddy(other) self:doNotify("RmBuddy", tostring(other.id)) end +function ServerPlayer:getAI() + return self.ai +end + return ServerPlayer diff --git a/packages/maneuvering/init.lua b/packages/maneuvering/init.lua index 1792f34f..8c2d993d 100644 --- a/packages/maneuvering/init.lua +++ b/packages/maneuvering/init.lua @@ -1,481 +1,448 @@ -- SPDX-License-Identifier: GPL-3.0-or-later - local extension = Package:new("maneuvering", Package.CardPack) local slash = Fk:cloneCard("slash") -local thunderSlashSkill = fk.CreateActiveSkill{ - name = "thunder__slash_skill", - max_phase_use_time = 1, - target_num = 1, - can_use = slash.skill.canUse, - mod_target_filter = slash.skill.modTargetFilter, - target_filter = slash.skill.targetFilter, - on_effect = function(self, room, effect) - local to = effect.to - local from = effect.from +local thunderSlashSkill = fk.CreateActiveSkill { + name = "thunder__slash_skill", + max_phase_use_time = 1, + target_num = 1, + can_use = slash.skill.canUse, + mod_target_filter = slash.skill.modTargetFilter, + target_filter = slash.skill.targetFilter, + on_effect = function(self, room, effect) + local to = effect.to + local from = effect.from - room:damage({ - from = room:getPlayerById(from), - to = room:getPlayerById(to), - card = effect.card, - damage = 1, - damageType = fk.ThunderDamage, - skillName = self.name - }) - end -} -local thunderSlash = fk.CreateBasicCard{ - name = "thunder__slash", - skill = thunderSlashSkill, - is_damage_card = true, -} - -extension:addCards{ - thunderSlash:clone(Card.Club, 5), - thunderSlash:clone(Card.Club, 6), - thunderSlash:clone(Card.Club, 7), - thunderSlash:clone(Card.Club, 8), - thunderSlash:clone(Card.Spade, 4), - thunderSlash:clone(Card.Spade, 5), - thunderSlash:clone(Card.Spade, 6), - thunderSlash:clone(Card.Spade, 7), - thunderSlash:clone(Card.Spade, 8), -} - -local fireSlashSkill = fk.CreateActiveSkill{ - name = "fire__slash_skill", - max_phase_use_time = 1, - target_num = 1, - can_use = slash.skill.canUse, - mod_target_filter = slash.skill.modTargetFilter, - target_filter = slash.skill.targetFilter, - on_effect = function(self, room, effect) - local to = effect.to - local from = effect.from - - room:damage({ - from = room:getPlayerById(from), - to = room:getPlayerById(to), - card = effect.card, - damage = 1, - damageType = fk.FireDamage, - skillName = self.name - }) - end -} -local fireSlash = fk.CreateBasicCard{ - name = "fire__slash", - skill = fireSlashSkill, - is_damage_card = true, -} - -extension:addCards{ - fireSlash:clone(Card.Heart, 4), - fireSlash:clone(Card.Heart, 7), - fireSlash:clone(Card.Heart, 10), - fireSlash:clone(Card.Diamond, 4), - fireSlash:clone(Card.Diamond, 5), -} - -local analepticSkill = fk.CreateActiveSkill{ - name = "analeptic_skill", - max_turn_use_time = 1, - mod_target_filter = function(self, to_select, selected, user, card, distance_limited) - return self:withinTimesLimit(Fk:currentRoom():getPlayerById(to_select), Player.HistoryTurn, card, "analeptic", Fk:currentRoom():getPlayerById(to_select)) and - not table.find(Fk:currentRoom().alive_players, function(p) - return p.dying - end) - end, - can_use = function(self, player, card) - return self:withinTimesLimit(player, Player.HistoryTurn, card, "analeptic", player) - end, - on_use = function(self, room, use) - if not use.tos or #TargetGroup:getRealTargets(use.tos) == 0 then - use.tos = { { use.from } } + room:damage({ + from = room:getPlayerById(from), + to = room:getPlayerById(to), + card = effect.card, + damage = 1, + damageType = fk.ThunderDamage, + skillName = self.name + }) end - - if use.extra_data and use.extra_data.analepticRecover then - use.extraUse = true - end - end, - on_effect = function(self, room, effect) - local to = room:getPlayerById(effect.to) - if effect.extra_data and effect.extra_data.analepticRecover then - room:recover({ - who = to, - num = 1, - recoverBy = room:getPlayerById(effect.from), - card = effect.card, - }) - else - to.drank = to.drank + 1 - room:broadcastProperty(to, "drank") - end - end +} +local thunderSlash = fk.CreateBasicCard { + name = "thunder__slash", + skill = thunderSlashSkill, + is_damage_card = true } -local analepticEffect = fk.CreateTriggerSkill{ - name = "analeptic_effect", - global = true, - priority = 0, -- game rule - events = { fk.PreCardUse, fk.EventPhaseStart }, - can_trigger = function(self, event, target, player, data) - if target ~= player then - return false - end +extension:addCards{thunderSlash:clone(Card.Club, 5), thunderSlash:clone(Card.Club, 6), thunderSlash:clone(Card.Club, 7), + thunderSlash:clone(Card.Club, 8), thunderSlash:clone(Card.Spade, 4), + thunderSlash:clone(Card.Spade, 5), thunderSlash:clone(Card.Spade, 6), + thunderSlash:clone(Card.Spade, 7), thunderSlash:clone(Card.Spade, 8)} - if event == fk.PreCardUse then - return data.card.trueName == "slash" and player.drank > 0 - else - return target.phase == Player.NotActive +local fireSlashSkill = fk.CreateActiveSkill { + name = "fire__slash_skill", + max_phase_use_time = 1, + target_num = 1, + can_use = slash.skill.canUse, + mod_target_filter = slash.skill.modTargetFilter, + target_filter = slash.skill.targetFilter, + on_effect = function(self, room, effect) + local to = effect.to + local from = effect.from + + room:damage({ + from = room:getPlayerById(from), + to = room:getPlayerById(to), + card = effect.card, + damage = 1, + damageType = fk.FireDamage, + skillName = self.name + }) end - end, - on_trigger = function(self, event, target, player, data) - local room = player.room - if event == fk.PreCardUse then - data.additionalDamage = (data.additionalDamage or 0) + player.drank - data.extra_data = data.extra_data or {} - data.extra_data.drankBuff = player.drank - player.drank = 0 - room:broadcastProperty(player, "drank") - else - for _, p in ipairs(room:getAlivePlayers(true)) do - if p.drank > 0 then - p.drank = 0 - room:broadcastProperty(p, "drank") +} +local fireSlash = fk.CreateBasicCard { + name = "fire__slash", + skill = fireSlashSkill, + is_damage_card = true +} + +extension:addCards{fireSlash:clone(Card.Heart, 4), fireSlash:clone(Card.Heart, 7), fireSlash:clone(Card.Heart, 10), + fireSlash:clone(Card.Diamond, 4), fireSlash:clone(Card.Diamond, 5)} + +local analepticSkill = fk.CreateActiveSkill { + name = "analeptic_skill", + max_turn_use_time = 1, + mod_target_filter = function(self, to_select, selected, user, card, distance_limited) + local to = Fk:currentRoom():getPlayerById(to_select) + local from = Fk:currentRoom():getPlayerById(user) + return self:withinTimesLimit(from, Player.HistoryTurn, card, "analeptic", to) and + not table.find(Fk:currentRoom().alive_players, function(p) + return p.dying + end) and not (card and from:isProhibited(to, card)) + end, + can_use = function(self, player, card) + return self:withinTimesLimit(player, Player.HistoryTurn, card, "analeptic", player) and + not player:isProhibited(player, card) + end, + + on_use = function(self, room, use) + if not use.tos or #TargetGroup:getRealTargets(use.tos) == 0 then + use.tos = {{use.from}} + end + + if use.extra_data and use.extra_data.analepticRecover then + use.extraUse = true + end + end, + on_effect = function(self, room, effect) + local to = room:getPlayerById(effect.to) + if effect.extra_data and effect.extra_data.analepticRecover then + room:recover({ + who = to, + num = 1, + recoverBy = room:getPlayerById(effect.from), + card = effect.card + }) + else + to.drank = to.drank + 1 + room:broadcastProperty(to, "drank") + end + end +} + +local analepticEffect = fk.CreateTriggerSkill { + name = "analeptic_effect", + global = true, + priority = 0, -- game rule + events = {fk.PreCardUse, fk.EventPhaseStart}, + can_trigger = function(self, event, target, player, data) + if target ~= player then + return false + end + if event == fk.PreCardUse then + return data.card.trueName == "slash" and player.drank > 0 + else + return target.phase == Player.NotActive + end + end, + on_trigger = function(self, event, target, player, data) + local room = player.room + if event == fk.PreCardUse then + data.additionalDamage = (data.additionalDamage or 0) + player.drank + data.extra_data = data.extra_data or {} + data.extra_data.drankBuff = player.drank + player.drank = 0 + room:broadcastProperty(player, "drank") + else + for _, p in ipairs(room:getAlivePlayers(true)) do + if p.drank > 0 then + p.drank = 0 + room:broadcastProperty(p, "drank") + end + end end - end end - end, } Fk:addSkill(analepticEffect) -local analeptic = fk.CreateBasicCard{ - name = "analeptic", - suit = Card.Spade, - number = 3, - skill = analepticSkill, +local analeptic = fk.CreateBasicCard { + name = "analeptic", + suit = Card.Spade, + number = 3, + skill = analepticSkill } -extension:addCards({ - analeptic, - analeptic:clone(Card.Spade, 9), - analeptic:clone(Card.Club, 3), - analeptic:clone(Card.Club, 9), - analeptic:clone(Card.Diamond, 9), -}) +extension:addCards({analeptic, analeptic:clone(Card.Spade, 9), analeptic:clone(Card.Club, 3), + analeptic:clone(Card.Club, 9), analeptic:clone(Card.Diamond, 9)}) -local recast = fk.CreateActiveSkill{ - name = "recast", - target_num = 0, - on_use = function(self, room, effect) - room:recastCard(effect.cards, room:getPlayerById(effect.from)) - end +local recast = fk.CreateActiveSkill { + name = "recast", + target_num = 0, + on_use = function(self, room, effect) + room:recastCard(effect.cards, room:getPlayerById(effect.from)) + end } Fk:addSkill(recast) -local ironChainCardSkill = fk.CreateActiveSkill{ - name = "iron_chain_skill", - min_target_num = 1, - max_target_num = 2, - mod_target_filter = function(self, to_select, selected, user, card, distance_limited) - return true - end, - target_filter = function() return true end, - on_effect = function(self, room, cardEffectEvent) - local to = room:getPlayerById(cardEffectEvent.to) - to:setChainState(not to.chained) - end, -} - -local ironChain = fk.CreateTrickCard{ - name = "iron_chain", - skill = ironChainCardSkill, - special_skills = { "recast" }, - multiple_targets = true, -} -extension:addCards{ - ironChain:clone(Card.Spade, 11), - ironChain:clone(Card.Spade, 12), - ironChain:clone(Card.Club, 10), - ironChain:clone(Card.Club, 11), - ironChain:clone(Card.Club, 12), - ironChain:clone(Card.Club, 13), -} - -local fireAttackSkill = fk.CreateActiveSkill{ - name = "fire_attack_skill", - target_num = 1, - mod_target_filter = function(self, to_select, selected, user, card, distance_limited) - return not Fk:currentRoom():getPlayerById(to_select):isKongcheng() - end, - target_filter = function(self, to_select) - return self:modTargetFilter(to_select) - end, - on_effect = function(self, room, cardEffectEvent) - local from = room:getPlayerById(cardEffectEvent.from) - local to = room:getPlayerById(cardEffectEvent.to) - if to:isKongcheng() then return end - - local showCard = room:askForCard(to, 1, 1, false, self.name, false, ".|.|.|hand", "#fire_attack-show:" .. from.id)[1] - to:showCards(showCard) - - showCard = Fk:getCardById(showCard) - local cards = room:askForDiscard(from, 1, 1, false, self.name, true, - ".|.|" .. showCard:getSuitString(), "#fire_attack-discard:" .. to.id .. "::" .. showCard:getSuitString()) - if #cards > 0 then - room:damage({ - from = from, - to = to, - card = cardEffectEvent.card, - damage = 1, - damageType = fk.FireDamage, - skillName = self.name - }) +local ironChainCardSkill = fk.CreateActiveSkill { + name = "iron_chain_skill", + min_target_num = 1, + max_target_num = 2, + mod_target_filter = function(self, to_select, selected, user, card, distance_limited) + local to = Fk:currentRoom():getPlayerById(to_select) + local from = Fk:currentRoom():getPlayerById(user) + return not (card and from:isProhibited(to, card)) + end, + target_filter = function(self, to_select, selected, _, card) + if #selected < self:getMaxTargetNum(Self, card) then + return self:modTargetFilter(to_select, selected, Self.id, card) + end + end, + on_effect = function(self, room, cardEffectEvent) + local to = room:getPlayerById(cardEffectEvent.to) + to:setChainState(not to.chained) end - end, -} -local fireAttack = fk.CreateTrickCard{ - name = "fire_attack", - skill = fireAttackSkill, - is_damage_card = true, -} -extension:addCards{ - fireAttack:clone(Card.Heart, 2), - fireAttack:clone(Card.Heart, 3), - fireAttack:clone(Card.Diamond, 12), } -local supplyShortageSkill = fk.CreateActiveSkill{ - name = "supply_shortage_skill", - distance_limit = 1, - mod_target_filter = function(self, to_select, selected, user, card, distance_limited) - local player = Fk:currentRoom():getPlayerById(to_select) - local from = Fk:currentRoom():getPlayerById(user) - return from ~= player and not (distance_limited and not self:withinDistanceLimit(from, false, card, player)) - end, - target_filter = function(self, to_select, selected, _, card) - return #selected == 0 and self:modTargetFilter(to_select, selected, Self.id, card, true) - end, - target_num = 1, - on_effect = function(self, room, effect) - local to = room:getPlayerById(effect.to) - local judge = { - who = to, - reason = "supply_shortage", - pattern = ".|.|spade,heart,diamond", - } - room:judge(judge) - local result = judge.card - if result.suit ~= Card.Club then - to:skip(Player.Draw) +local ironChain = fk.CreateTrickCard { + name = "iron_chain", + skill = ironChainCardSkill, + special_skills = {"recast"}, + multiple_targets = true +} +extension:addCards{ironChain:clone(Card.Spade, 11), ironChain:clone(Card.Spade, 12), ironChain:clone(Card.Club, 10), + ironChain:clone(Card.Club, 11), ironChain:clone(Card.Club, 12), ironChain:clone(Card.Club, 13)} + +local fireAttackSkill = fk.CreateActiveSkill { + name = "fire_attack_skill", + target_num = 1, + mod_target_filter = function(self, to_select, selected, user, card, distance_limited) + local to = Fk:currentRoom():getPlayerById(to_select) + local from = Fk:currentRoom():getPlayerById(user) + return not (card and from:isProhibited(to, card)) and not to:isKongcheng() + end, + target_filter = function(self, to_select, selected, _, card) + if #selected < self:getMaxTargetNum(Self, card) then + return self:modTargetFilter(to_select, selected, Self.id, card) + end + end, + on_effect = function(self, room, cardEffectEvent) + local from = room:getPlayerById(cardEffectEvent.from) + local to = room:getPlayerById(cardEffectEvent.to) + if to:isKongcheng() then + return + end + + local showCard = room:askForCard(to, 1, 1, false, self.name, false, ".|.|.|hand", + "#fire_attack-show:" .. from.id)[1] + to:showCards(showCard) + + showCard = Fk:getCardById(showCard) + local cards = room:askForDiscard(from, 1, 1, false, self.name, true, ".|.|" .. showCard:getSuitString(), + "#fire_attack-discard:" .. to.id .. "::" .. showCard:getSuitString()) + if #cards > 0 then + room:damage({ + from = from, + to = to, + card = cardEffectEvent.card, + damage = 1, + damageType = fk.FireDamage, + skillName = self.name + }) + end end - self:onNullified(room, effect) - end, - on_nullified = function(self, room, effect) - room:moveCards{ - ids = room:getSubcardsByRule(effect.card, { Card.Processing }), - toArea = Card.DiscardPile, - moveReason = fk.ReasonUse - } - end, } -local supplyShortage = fk.CreateDelayedTrickCard{ - name = "supply_shortage", - skill = supplyShortageSkill, -} -extension:addCards{ - supplyShortage:clone(Card.Spade, 10), - supplyShortage:clone(Card.Club, 4), +local fireAttack = fk.CreateTrickCard { + name = "fire_attack", + skill = fireAttackSkill, + is_damage_card = true } +extension:addCards{fireAttack:clone(Card.Heart, 2), fireAttack:clone(Card.Heart, 3), fireAttack:clone(Card.Diamond, 12)} -local gudingSkill = fk.CreateTriggerSkill{ - name = "#guding_blade_skill", - attached_equip = "guding_blade", - frequency = Skill.Compulsory, - events = {fk.DamageCaused}, - can_trigger = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name) and - data.to:isKongcheng() and data.card and data.card.trueName == "slash" and - not data.chain - end, - on_use = function(_, _, _, _, data) - data.damage = data.damage + 1 - end, +local supplyShortageSkill = fk.CreateActiveSkill { + name = "supply_shortage_skill", + distance_limit = 1, + mod_target_filter = function(self, to_select, selected, user, card, distance_limited) + local player = Fk:currentRoom():getPlayerById(to_select) + local from = Fk:currentRoom():getPlayerById(user) + return + user ~= to_select and not (distance_limited and not self:withinDistanceLimit(from, false, card, player)) and + not (card and from:isProhibited(player, card)) + end, + target_filter = function(self, to_select, selected, _, card) + return #selected < 1 and self:modTargetFilter(to_select, selected, Self.id, card, true) + end, + target_num = 1, + on_effect = function(self, room, effect) + local to = room:getPlayerById(effect.to) + local judge = { + who = to, + reason = "supply_shortage", + pattern = ".|.|club" + } + room:judge(judge) + if judge.card.suit ~= Card.Club then + to:skip(Player.Draw) + end + self:onNullified(room, effect) + end, + on_nullified = function(self, room, effect) + room:moveCards{ + ids = room:getSubcardsByRule(effect.card, {Card.Processing}), + toArea = Card.DiscardPile, + moveReason = fk.ReasonUse + } + end +} +local supplyShortage = fk.CreateDelayedTrickCard { + name = "supply_shortage", + skill = supplyShortageSkill +} +extension:addCards{supplyShortage:clone(Card.Spade, 10), supplyShortage:clone(Card.Club, 4)} + +local gudingSkill = fk.CreateTriggerSkill { + name = "#guding_blade_skill", + attached_equip = "guding_blade", + frequency = Skill.Compulsory, + events = {fk.DamageCaused}, + can_trigger = function(self, event, target, player, data) + return target == player and player:hasSkill(self.name) and data.to:isKongcheng() and data.card and + data.card.trueName == "slash" and not data.chain + end, + on_use = function(_, _, _, _, data) + data.damage = data.damage + 1 + end } Fk:addSkill(gudingSkill) -local gudingBlade = fk.CreateWeapon{ - name = "guding_blade", - suit = Card.Spade, - number = 1, - attack_range = 2, - equip_skill = gudingSkill, +local gudingBlade = fk.CreateWeapon { + name = "guding_blade", + suit = Card.Spade, + number = 1, + attack_range = 2, + equip_skill = gudingSkill } extension:addCard(gudingBlade) -local fanSkill = fk.CreateTriggerSkill{ - name = "#fan_skill", - attached_equip = "fan", - events = { fk.AfterCardUseDeclared }, - can_trigger = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name) and data.card.name == "slash" - end, - on_use = function(_, _, _, _, data) - local card = Fk:cloneCard("fire__slash") - card.skillName = "fan" - card:addSubcard(data.card) - data.card = card - end, +local fanSkill = fk.CreateTriggerSkill { + name = "#fan_skill", + attached_equip = "fan", + events = {fk.AfterCardUseDeclared}, + can_trigger = function(self, event, target, player, data) + return target == player and player:hasSkill(self.name) and data.card.name == "slash" + end, + on_use = function(_, _, _, _, data) + local card = Fk:cloneCard("fire__slash") + card.skillName = "fan" + card:addSubcard(data.card) + data.card = card + end } Fk:addSkill(fanSkill) -local fan = fk.CreateWeapon{ - name = "fan", - suit = Card.Diamond, - number = 1, - attack_range = 4, - equip_skill = fanSkill, +local fan = fk.CreateWeapon { + name = "fan", + suit = Card.Diamond, + number = 1, + attack_range = 4, + equip_skill = fanSkill } extension:addCard(fan) -local vineSkill = fk.CreateTriggerSkill{ - name = "#vine_skill", - attached_equip = "vine", - mute = true, - frequency = Skill.Compulsory, +local vineSkill = fk.CreateTriggerSkill { + name = "#vine_skill", + attached_equip = "vine", + mute = true, + frequency = Skill.Compulsory, - events = {fk.PreCardEffect, fk.DamageInflicted}, - can_trigger = function(self, event, target, player, data) - if event == fk.DamageInflicted then - return target == player and player:hasSkill(self.name) and - data.damageType == fk.FireDamage + events = {fk.PreCardEffect, fk.DamageInflicted}, + can_trigger = function(self, event, target, player, data) + if event == fk.DamageInflicted then + return target == player and player:hasSkill(self.name) and data.damageType == fk.FireDamage + end + local effect = data ---@type CardEffectEvent + return player.id == effect.to and player:hasSkill(self.name) and + (effect.card.name == "slash" or effect.card.name == "savage_assault" or effect.card.name == + "archery_attack") + end, + on_use = function(self, event, target, player, data) + local room = player.room + if event == fk.DamageInflicted then + room:broadcastPlaySound("./packages/maneuvering/audio/card/vineburn") + room:setEmotion(player, "./packages/maneuvering/image/anim/vineburn") + data.damage = data.damage + 1 + else + room:broadcastPlaySound("./packages/maneuvering/audio/card/vine") + room:setEmotion(player, "./packages/maneuvering/image/anim/vine") + return true + end end - local effect = data ---@type CardEffectEvent - return player.id == effect.to and player:hasSkill(self.name) and - (effect.card.name == "slash" or effect.card.name == "savage_assault" or - effect.card.name == "archery_attack") - end, - on_use = function(self, event, target, player, data) - local room = player.room - if event == fk.DamageInflicted then - room:broadcastPlaySound("./packages/maneuvering/audio/card/vineburn") - room:setEmotion(player, "./packages/maneuvering/image/anim/vineburn") - data.damage = data.damage + 1 - else - room:broadcastPlaySound("./packages/maneuvering/audio/card/vine") - room:setEmotion(player, "./packages/maneuvering/image/anim/vine") - return true - end - end, } Fk:addSkill(vineSkill) -local vine = fk.CreateArmor{ - name = "vine", - equip_skill = vineSkill, -} -extension:addCards{ - vine:clone(Card.Spade, 2), - vine:clone(Card.Club, 2), +local vine = fk.CreateArmor { + name = "vine", + equip_skill = vineSkill } +extension:addCards{vine:clone(Card.Spade, 2), vine:clone(Card.Club, 2)} -local silverLionSkill = fk.CreateTriggerSkill{ - name = "#silver_lion_skill", - attached_equip = "silver_lion", - frequency = Skill.Compulsory, - events = {fk.DamageInflicted}, - can_trigger = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name) and data.damage > 1 - end, - on_use = function(_, _, _, _, data) - data.damage = 1 - end, +local silverLionSkill = fk.CreateTriggerSkill { + name = "#silver_lion_skill", + attached_equip = "silver_lion", + frequency = Skill.Compulsory, + events = {fk.DamageInflicted}, + can_trigger = function(self, event, target, player, data) + return target == player and player:hasSkill(self.name) and data.damage > 1 + end, + on_use = function(_, _, _, _, data) + data.damage = 1 + end } Fk:addSkill(silverLionSkill) -local silverLion = fk.CreateArmor{ - name = "silver_lion", - suit = Card.Club, - number = 1, - equip_skill = silverLionSkill, - on_uninstall = function(self, room, player) - Armor.onUninstall(self, room, player) - if player:isAlive() and player:isWounded() and self.equip_skill:isEffectable(player) then - room:broadcastPlaySound("./packages/maneuvering/audio/card/silver_lion") - room:setEmotion(player, "./packages/maneuvering/image/anim/silver_lion") - room:recover{ - who = player, - num = 1, - skillName = self.name - } +local silverLion = fk.CreateArmor { + name = "silver_lion", + suit = Card.Club, + number = 1, + equip_skill = silverLionSkill, + on_uninstall = function(self, room, player) + Armor.onUninstall(self, room, player) + if player:isAlive() and player:isWounded() and self.equip_skill:isEffectable(player) then + room:broadcastPlaySound("./packages/maneuvering/audio/card/silver_lion") + room:setEmotion(player, "./packages/maneuvering/image/anim/silver_lion") + room:recover{ + who = player, + num = 1, + skillName = self.name + } + end end - end, } extension:addCard(silverLion) -local huaLiu = fk.CreateDefensiveRide{ - name = "hualiu", - suit = Card.Diamond, - number = 13, +local huaLiu = fk.CreateDefensiveRide { + name = "hualiu", + suit = Card.Diamond, + number = 13 } -extension:addCards({ - huaLiu, -}) +extension:addCards({huaLiu}) -extension:addCards{ - Fk:cloneCard("jink", Card.Heart, 8), - Fk:cloneCard("jink", Card.Heart, 9), - Fk:cloneCard("jink", Card.Heart, 11), - Fk:cloneCard("jink", Card.Heart, 12), - Fk:cloneCard("jink", Card.Diamond, 6), - Fk:cloneCard("jink", Card.Diamond, 7), - Fk:cloneCard("jink", Card.Diamond, 8), - Fk:cloneCard("jink", Card.Diamond, 10), - Fk:cloneCard("jink", Card.Diamond, 11), - - Fk:cloneCard("peach", Card.Heart, 5), - Fk:cloneCard("peach", Card.Heart, 6), - Fk:cloneCard("peach", Card.Diamond, 2), - Fk:cloneCard("peach", Card.Diamond, 3), - - Fk:cloneCard("nullification", Card.Heart, 1), - Fk:cloneCard("nullification", Card.Heart, 13), - Fk:cloneCard("nullification", Card.Spade, 13), -} +extension:addCards{Fk:cloneCard("jink", Card.Heart, 8), Fk:cloneCard("jink", Card.Heart, 9), + Fk:cloneCard("jink", Card.Heart, 11), Fk:cloneCard("jink", Card.Heart, 12), + Fk:cloneCard("jink", Card.Diamond, 6), Fk:cloneCard("jink", Card.Diamond, 7), + Fk:cloneCard("jink", Card.Diamond, 8), Fk:cloneCard("jink", Card.Diamond, 10), + Fk:cloneCard("jink", Card.Diamond, 11), Fk:cloneCard("peach", Card.Heart, 5), + Fk:cloneCard("peach", Card.Heart, 6), Fk:cloneCard("peach", Card.Diamond, 2), + Fk:cloneCard("peach", Card.Diamond, 3), Fk:cloneCard("nullification", Card.Heart, 1), + Fk:cloneCard("nullification", Card.Heart, 13), Fk:cloneCard("nullification", Card.Spade, 13)} Fk:loadTranslationTable{ - ["maneuvering"] = "军争", + ["maneuvering"] = "军争", - ["thunder__slash"] = "雷杀", - [":thunder__slash"] = "基本牌
时机:出牌阶段
目标:攻击范围内的一名角色
效果:对目标角色造成1点雷电伤害。", - ["fire__slash"] = "火杀", - [":fire__slash"] = "基本牌
时机:出牌阶段
目标:攻击范围内的一名角色
效果:对目标角色造成1点火焰伤害。", - ["analeptic"] = "酒", - [":analeptic"] = "基本牌
时机:出牌阶段/你处于濒死状态时
目标:你
效果:目标角色本回合使用的下一张【杀】将要造成的伤害+1/目标角色回复1点体力。", - ["iron_chain"] = "铁锁连环", - [":iron_chain"] = "锦囊牌
时机:出牌阶段
目标:一至两名角色
效果:横置或重置目标角色的武将牌。", - ["_normal_use"] = "正常使用", - ["recast"] = "重铸", - [":recast"] = "你可以将此牌置入弃牌堆,然后摸一张牌。", - ["fire_attack"] = "火攻", - ["fire_attack_skill"] = "火攻", - [":fire_attack"] = "锦囊牌
时机:出牌阶段
目标:一名有手牌的角色
效果:目标角色展示一张手牌,然后你可以弃置一张与所展示牌花色相同的手牌令其受到1点火焰伤害。", - ["#fire_attack-show"] = "%src 对你使用了火攻,请展示一张手牌", - ["#fire_attack-discard"] = "你可弃置一张 %arg 手牌,对 %src 造成1点火属性伤害", - ["supply_shortage"] = "兵粮寸断", - [":supply_shortage"] = "延时锦囊牌
时机:出牌阶段
目标:距离1的一名其他角色
效果:将此牌置于目标角色判定区内。其判定阶段进行判定:若结果不为梅花,其跳过摸牌阶段。然后将【兵粮寸断】置入弃牌堆。", - ["guding_blade"] = "古锭刀", - [":guding_blade"] = "装备牌·武器
攻击范围:2
武器技能:锁定技。每当你使用【杀】对目标角色造成伤害时,若该角色没有手牌,此伤害+1。", - ["fan"] = "朱雀羽扇", - [":fan"] = "装备牌·武器
攻击范围:4
武器技能:你可以将一张普通【杀】当火【杀】使用。", - ["#fan_skill"] = "朱雀羽扇", - ["vine"] = "藤甲", - [":vine"] = "装备牌·防具
防具技能:锁定技。【南蛮入侵】、【万箭齐发】和普通【杀】对你无效。每当你受到火焰伤害时,此伤害+1。", - ["silver_lion"] = "白银狮子", - [":silver_lion"] = "装备牌·防具
防具技能:锁定技。每当你受到伤害时,若此伤害大于1点,防止多余的伤害。每当你失去装备区里的【白银狮子】后,你回复1点体力。", - ["hualiu"] = "骅骝", - [":hualiu"] = "装备牌·坐骑
坐骑技能:其他角色与你的距离+1。", + ["thunder__slash"] = "雷杀", + [":thunder__slash"] = "基本牌
时机:出牌阶段
目标:攻击范围内的一名角色
效果:对目标角色造成1点雷电伤害。", + ["fire__slash"] = "火杀", + [":fire__slash"] = "基本牌
时机:出牌阶段
目标:攻击范围内的一名角色
效果:对目标角色造成1点火焰伤害。", + ["analeptic"] = "酒", + [":analeptic"] = "基本牌
时机:出牌阶段/你处于濒死状态时
目标:你
效果:目标角色本回合使用的下一张【杀】将要造成的伤害+1/目标角色回复1点体力。", + ["iron_chain"] = "铁锁连环", + [":iron_chain"] = "锦囊牌
时机:出牌阶段
目标:一至两名角色
效果:横置或重置目标角色的武将牌。", + ["_normal_use"] = "正常使用", + ["recast"] = "重铸", + [":recast"] = "你可以将此牌置入弃牌堆,然后摸一张牌。", + ["fire_attack"] = "火攻", + ["fire_attack_skill"] = "火攻", + [":fire_attack"] = "锦囊牌
时机:出牌阶段
目标:一名有手牌的角色
效果:目标角色展示一张手牌,然后你可以弃置一张与所展示牌花色相同的手牌令其受到1点火焰伤害。", + ["#fire_attack-show"] = "%src 对你使用了火攻,请展示一张手牌", + ["#fire_attack-discard"] = "你可弃置一张 %arg 手牌,对 %src 造成1点火属性伤害", + ["supply_shortage"] = "兵粮寸断", + [":supply_shortage"] = "延时锦囊牌
时机:出牌阶段
目标:距离1的一名其他角色
效果:将此牌置于目标角色判定区内。其判定阶段进行判定:若结果不为梅花,其跳过摸牌阶段。然后将【兵粮寸断】置入弃牌堆。", + ["guding_blade"] = "古锭刀", + [":guding_blade"] = "装备牌·武器
攻击范围:2
武器技能:锁定技。每当你使用【杀】对目标角色造成伤害时,若该角色没有手牌,此伤害+1。", + ["fan"] = "朱雀羽扇", + [":fan"] = "装备牌·武器
攻击范围:4
武器技能:你可以将一张普通【杀】当火【杀】使用。", + ["#fan_skill"] = "朱雀羽扇", + ["vine"] = "藤甲", + [":vine"] = "装备牌·防具
防具技能:锁定技。【南蛮入侵】、【万箭齐发】和普通【杀】对你无效。每当你受到火焰伤害时,此伤害+1。", + ["silver_lion"] = "白银狮子", + [":silver_lion"] = "装备牌·防具
防具技能:锁定技。每当你受到伤害时,若此伤害大于1点,防止多余的伤害。每当你失去装备区里的【白银狮子】后,你回复1点体力。", + ["hualiu"] = "骅骝", + [":hualiu"] = "装备牌·坐骑
坐骑技能:其他角色与你的距离+1。" } return extension diff --git a/packages/maneuvering/maneuvering_ai.lua b/packages/maneuvering/maneuvering_ai.lua new file mode 100644 index 00000000..ce89f11d --- /dev/null +++ b/packages/maneuvering/maneuvering_ai.lua @@ -0,0 +1,140 @@ +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 -- 使用优先值 +} + +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_dis_card.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:isEnemie(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_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_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/game_rule.lua b/packages/standard/game_rule.lua index ca4a826a..006f8552 100644 --- a/packages/standard/game_rule.lua +++ b/packages/standard/game_rule.lua @@ -10,7 +10,7 @@ local function rewardAndPunish(killer, victim) end end -GameRule = fk.CreateTriggerSkill{ +GameRule = fk.CreateTriggerSkill { name = "game_rule", events = { fk.GamePrepared, @@ -37,71 +37,96 @@ GameRule = fk.CreateTriggerSkill{ end switch(event, { - [fk.AskForPeaches] = function() - local dyingPlayer = room:getPlayerById(data.who) - while dyingPlayer.hp < 1 do - local cardNames = {"peach"} - local prompt = "#AskForPeaches:" .. dyingPlayer.id .. "::" .. tostring(1 - dyingPlayer.hp) - if player == dyingPlayer then - table.insert(cardNames, "analeptic") - prompt = "#AskForPeachesSelf:::" .. tostring(1 - dyingPlayer.hp) - end + [fk.AskForPeaches] = function() + local dyingPlayer = room:getPlayerById(data.who) + while dyingPlayer.hp < 1 do + local cardNames = { "peach" } + local prompt = "#AskForPeaches:" .. data.who .. "::" .. tostring(1 - dyingPlayer.hp) + if player == dyingPlayer then + table.insert(cardNames, "analeptic") + prompt = "#AskForPeachesSelf:::" .. tostring(1 - dyingPlayer.hp) + end - cardNames = table.filter(cardNames, function (cardName) - local cardCloned = Fk:cloneCard(cardName) - return not (player:prohibitUse(cardCloned) or player:isProhibited(dyingPlayer, cardCloned)) - end) - if #cardNames == 0 then return end - - local peach_use = room:askForUseCard(player, "peach", table.concat(cardNames, ",") , prompt) - if not peach_use then break end - peach_use.tos = { {dyingPlayer.id} } - if peach_use.card.trueName == "analeptic" then - peach_use.extra_data = peach_use.extra_data or {} - peach_use.extra_data.analepticRecover = true + cardNames = table.filter(cardNames, function(cardName) + local cardCloned = Fk:cloneCard(cardName) + return not (player:prohibitUse(cardCloned) or player:isProhibited(dyingPlayer, cardCloned)) + end) + if #cardNames < 1 then return end + room:notifyMoveFocus(player, "peach") + local useData = { + user = player, + cardName = "peach", + pattern = table.concat(cardNames, ","), + extraData = Util.DummyTable + } + room.logic:trigger(fk.AskForCardUse, player, useData) + if type(useData.result) == "table" then + useData = useData.result + useData.tos = { { data.who } } + if useData.card.trueName == "analeptic" then + useData.extra_data = useData.extra_data or {} + useData.extra_data.analepticRecover = true + end + room:useCard(useData) + end + useData = { "peach", table.concat(cardNames, ","), prompt, true, Util.DummyTable } + while dyingPlayer.hp < 1 do + Fk.currentResponsePattern = table.concat(cardNames, ",") + local result = room:doRequest(player, "AskForUseCard", json.encode(useData)) + Fk.currentResponsePattern = nil + if result ~= "" then + result = room:handleUseCardReply(player, result) + result.tos = { { data.who } } + if result.card.trueName == "analeptic" then + result.extra_data = result.extra_data or {} + result.extra_data.analepticRecover = true + end + room:useCard(result) + else + return + end + end end - room:useCard(peach_use) - end - end, - [fk.AskForPeachesDone] = function() - if player.hp < 1 and not data.ignoreDeath then - ---@type DeathStruct - local deathData = { - who = player.id, - damage = data.damage, - } - room:killPlayer(deathData) - end - end, - [fk.GameOverJudge] = function() - local winner = Fk.game_modes[room.settings.gameMode]:getWinner(player) - if winner ~= "" then - room:gameOver(winner) - return true - end - end, - [fk.BuryVictim] = function() - player:bury() - if room.tag["SkipNormalDeathProcess"] then - return false - end - local damage = data.damage - if damage and damage.from then - local killer = damage.from - rewardAndPunish(killer, player); - end - end, - default = function() - print("game_rule: Event=" .. event) - room:askForSkillInvoke(player, "rule") - end, + end, + [fk.AskForPeachesDone] = function() + if player.hp < 1 and not data.ignoreDeath then + ---@type DeathStruct + local deathData = { + who = player.id, + damage = data.damage, + } + room:killPlayer(deathData) + end + end, + [fk.GameOverJudge] = function() + local winner = Fk.game_modes[room.settings.gameMode]:getWinner(player) + if winner ~= "" then + room:gameOver(winner) + return true + end + end, + [fk.BuryVictim] = function() + player:bury() + if room.tag["SkipNormalDeathProcess"] then + return false + end + local damage = data.damage + if damage and damage.from then + local killer = damage.from + rewardAndPunish(killer, player); + end + end, + default = function() + print("game_rule: Event=" .. event) + room:askForSkillInvoke(player, "rule") + end, }) return false end, } -local fastchat_m = fk.CreateActiveSkill{ name = "fastchat_m" } -local fastchat_f = fk.CreateActiveSkill{ name = "fastchat_f" } +local fastchat_m = fk.CreateActiveSkill { name = "fastchat_m" } +local fastchat_f = fk.CreateActiveSkill { name = "fastchat_f" } Fk:addSkill(fastchat_m) Fk:addSkill(fastchat_f) diff --git a/packages/standard/init.lua b/packages/standard/init.lua index cf7b4daa..62d9c5a1 100644 --- a/packages/standard/init.lua +++ b/packages/standard/init.lua @@ -1,25 +1,25 @@ -- SPDX-License-Identifier: GPL-3.0-or-later - local extension = Package:new("standard") extension.metadata = require "packages.standard.metadata" dofile "packages/standard/game_rule.lua" dofile "packages/standard/aux_skills.lua" -local jianxiong = fk.CreateTriggerSkill{ +local jianxiong = fk.CreateTriggerSkill { name = "jianxiong", anim_type = "masochism", - events = {fk.Damaged}, + events = { fk.Damaged }, can_trigger = function(self, event, target, player, data) local room = target.room return target == player and player:hasSkill(self.name) and data.card and - table.every(data.card:isVirtual() and data.card.subcards or {data.card.id}, function(id) return room:getCardArea(id) == Card.Processing end) + table.every(data.card:isVirtual() and data.card.subcards or { data.card.id }, function(id) + return room:getCardArea(id) == Card.Processing + end) end, on_use = function(self, event, target, player, data) player.room:obtainCard(player.id, data.card, true, fk.ReasonJustMove) - end, + end } - -local hujia = fk.CreateViewAsSkill{ +local hujia = fk.CreateViewAsSkill { name = "hujia$", anim_type = "defensive", pattern = "jink", @@ -38,14 +38,14 @@ local hujia = fk.CreateViewAsSkill{ return false end, enabled_at_response = function(self, player) - return not table.every(Fk:currentRoom().alive_players, function(p) + return player:getMark("hujia-failed-phase") == 0 and not table.every(Fk:currentRoom().alive_players, function(p) return p == player or p.kingdom ~= "wei" end) - end, + end } -local hujiaResponse = fk.CreateTriggerSkill{ +local hujiaResponse = fk.CreateTriggerSkill { name = "#hujiaResponse", - events = {fk.PreCardUse, fk.PreCardRespond}, + events = { fk.PreCardUse, fk.PreCardRespond }, mute = true, priority = 10, can_trigger = function(self, event, target, player, data) @@ -58,34 +58,34 @@ local hujiaResponse = fk.CreateTriggerSkill{ end, on_use = function(self, event, target, player, data) local room = player.room - for _, p in ipairs(room:getOtherPlayers(player)) do - if p.kingdom == "wei" then - local cardResponded = room:askForResponse(p, "jink", "jink", "#hujia-ask:" .. player.id, true) - if cardResponded then - room:responseCard({ - from = p.id, - card = cardResponded, - skipDrop = true, - }) - - data.card = cardResponded - return false - end + local weis = table.filter(room:getOtherPlayers(player), function(p) + return p.kingdom == "wei" + end) + room:doIndicate(player.id, table.map(weis, function(p) + return p.id + end)) + for _, p in ipairs(weis) do + local cardResponded = room:askForResponse(p, "jink", "jink", "#hujia-ask:" .. player.id, true) + if cardResponded then --[[ + room:responseCard({ + from = p.id, + card = cardResponded, + skipDrop = true + })--]] + data.card = cardResponded.card + return false end end - - if event == fk.PreCardUse and player.phase == Player.Play then - room:setPlayerMark(player, "hujia-failed-phase", 1) - end + room:setPlayerMark(player, "hujia-failed-phase", 1) return true end, - refresh_events = {fk.CardUsing}, + refresh_events = { fk.CardUsing, fk.CardResponding }, can_refresh = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name, true) and player:getMark("hujia-failed-phase") > 0 + return player:hasSkill(self.name, true) and player:getMark("hujia-failed-phase") > 0 end, on_refresh = function(self, event, target, player, data) player.room:setPlayerMark(player, "hujia-failed-phase", 0) - end, + end } hujia:addRelatedSkill(hujiaResponse) @@ -93,31 +93,31 @@ local caocao = General:new(extension, "caocao", "wei", 4) caocao:addSkill(jianxiong) caocao:addSkill(hujia) -local guicai = fk.CreateTriggerSkill{ +local guicai = fk.CreateTriggerSkill { name = "guicai", anim_type = "control", - events = {fk.AskForRetrial}, + events = { fk.AskForRetrial }, can_trigger = function(self, event, target, player, data) return player:hasSkill(self.name) and not player:isKongcheng() end, on_cost = function(self, event, target, player, data) local room = player.room local prompt = "#guicai-ask::" .. target.id - local card = room:askForResponse(player, self.name, ".|.|.|hand", prompt, true) - if card ~= nil then - self.cost_data = card + local Response = room:askForResponse(player, self.name, ".|.|.|hand", prompt, true, nil, nil, true) + if Response then + self.cost_data = Response.card return true end end, on_use = function(self, event, target, player, data) local room = player.room room:retrial(self.cost_data, player, data, self.name) - end, + end } -local fankui = fk.CreateTriggerSkill{ +local fankui = fk.CreateTriggerSkill { name = "fankui", anim_type = "masochism", - events = {fk.Damaged}, + events = { fk.Damaged }, can_trigger = function(self, event, target, player, data) if target == player and player:hasSkill(self.name) and data.from and not data.from.dead then if data.from == player then @@ -130,7 +130,7 @@ local fankui = fk.CreateTriggerSkill{ on_use = function(self, event, target, player, data) local room = player.room local from = data.from - local flag = from == player and "e" or "he" + local flag = from == player and "e" or "he" local card = room:askForCardChosen(player, from, flag, self.name) room:obtainCard(player.id, card, false, fk.ReasonPrey) end @@ -139,51 +139,58 @@ local simayi = General:new(extension, "simayi", "wei", 3) simayi:addSkill(guicai) simayi:addSkill(fankui) -local ganglie = fk.CreateTriggerSkill{ +local ganglie = fk.CreateTriggerSkill { name = "ganglie", anim_type = "masochism", - events = {fk.Damaged}, + events = { fk.Damaged }, can_trigger = function(self, event, target, player, data) return target == player and player:hasSkill(self.name) end, on_use = function(self, event, target, player, data) local room = player.room local from = data.from - if from and not from.dead then room:doIndicate(player.id, {from.id}) end + if from and not from.dead then + room:doIndicate(player.id, { from.id }) + end local judge = { who = player, reason = self.name, - pattern = ".|.|^heart", + pattern = ".|.|^heart" } room:judge(judge) if judge.card.suit ~= Card.Heart and from and not from.dead then local discards = room:askForDiscard(from, 2, 2, false, self.name, true) if #discards == 0 then - room:damage{ + room:damage { from = player, to = from, damage = 1, - skillName = self.name, + skillName = self.name } end end - end, + end } local xiahoudun = General:new(extension, "xiahoudun", "wei", 4) xiahoudun:addSkill(ganglie) -local tuxi = fk.CreateTriggerSkill{ +local tuxi = fk.CreateTriggerSkill { name = "tuxi", anim_type = "control", - events = {fk.EventPhaseStart}, + events = { fk.EventPhaseStart }, can_trigger = function(self, event, target, player, data) return target == player and player:hasSkill(self.name) and player.phase == Player.Draw and - table.find(player.room:getOtherPlayers(player), function(p) return not p:isKongcheng() end) + table.find(player.room:getOtherPlayers(player), function(p) + return not p:isKongcheng() + end) end, on_cost = function(self, event, target, player, data) local room = player.room local targets = table.map(table.filter(room:getOtherPlayers(player), function(p) - return not p:isKongcheng() end), function (p) return p.id end) + return not p:isKongcheng() + end), function(p) + return p.id + end) local result = room:askForChoosePlayers(player, targets, 1, 2, "#tuxi-ask", self.name) if #result > 0 then @@ -194,7 +201,9 @@ local tuxi = fk.CreateTriggerSkill{ on_use = function(self, event, target, player, data) local room = player.room for _, id in ipairs(self.cost_data) do - if player.dead then return end + if player.dead then + return + end local p = room:getPlayerById(id) if not p.dead then local c = room:askForCardChosen(player, p, "h", self.name) @@ -202,29 +211,29 @@ local tuxi = fk.CreateTriggerSkill{ end end return true - end, + end } local zhangliao = General:new(extension, "zhangliao", "wei", 4) zhangliao:addSkill(tuxi) -local luoyi = fk.CreateTriggerSkill{ +local luoyi = fk.CreateTriggerSkill { name = "luoyi", anim_type = "offensive", - events = {fk.DrawNCards}, + events = { fk.DrawNCards }, can_trigger = function(self, event, target, player, data) return target == player and player:hasSkill(self.name) and data.n > 0 end, on_use = function(self, event, target, player, data) data.n = data.n - 1 - end, + end } -local luoyi_trigger = fk.CreateTriggerSkill{ +local luoyi_trigger = fk.CreateTriggerSkill { name = "#luoyi_trigger", mute = true, - events = {fk.DamageCaused}, + events = { fk.DamageCaused }, can_trigger = function(self, event, target, player, data) - return target == player and player:usedSkillTimes("luoyi", Player.HistoryTurn) > 0 and - not data.chain and data.card and (data.card.trueName == "slash" or data.card.name == "duel") + return target == player and player:usedSkillTimes("luoyi", Player.HistoryTurn) > 0 and not data.chain and + data.card and (data.card.trueName == "slash" or data.card.name == "duel") end, on_cost = function(self, event, target, player, data) return true @@ -234,37 +243,39 @@ local luoyi_trigger = fk.CreateTriggerSkill{ player:broadcastSkillInvoke("luoyi") room:notifySkillInvoked(player, "luoyi") data.damage = data.damage + 1 - end, + end } local xuchu = General:new(extension, "xuchu", "wei", 4) luoyi:addRelatedSkill(luoyi_trigger) xuchu:addSkill(luoyi) -local tiandu = fk.CreateTriggerSkill{ +local tiandu = fk.CreateTriggerSkill { name = "tiandu", anim_type = "drawcard", - events = {fk.FinishJudge}, + events = { fk.FinishJudge }, can_trigger = function(self, event, target, player, data) return target == player and player:hasSkill(self.name) and player.room:getCardArea(data.card) == Card.Processing end, on_use = function(self, event, target, player, data) player.room:obtainCard(player.id, data.card, true, fk.ReasonJustMove) - end, + end } -local yiji = fk.CreateTriggerSkill{ +local yiji = fk.CreateTriggerSkill { name = "yiji", anim_type = "masochism", - events = {fk.Damaged}, + events = { fk.Damaged }, on_trigger = function(self, event, target, player, data) self.cancel_cost = false for i = 1, data.damage do - if self.cancel_cost then break end + if self.cancel_cost then + break + end self:doCost(event, target, player, data) end end, on_cost = function(self, event, target, player, data) local room = player.room - if room:askForSkillInvoke(player, self.name, data) then + if room:askForSkillInvoke(player, self.name) then return true end self.cancel_cost = true @@ -275,39 +286,54 @@ local yiji = fk.CreateTriggerSkill{ local fakemove = { toArea = Card.PlayerHand, to = player.id, - moveInfo = table.map(ids, function(id) return {cardId = id, fromArea = Card.Void} end), - moveReason = fk.ReasonJustMove, + moveInfo = table.map(ids, function(id) + return { + cardId = id, + fromArea = Card.Void + } + end), + moveReason = fk.ReasonJustMove } - room:notifyMoveCards({player}, {fakemove}) + room:notifyMoveCards({ player }, { fakemove }) for _, id in ipairs(ids) do room:setCardMark(Fk:getCardById(id), "yiji", 1) end - while table.find(ids, function(id) return Fk:getCardById(id):getMark("yiji") > 0 end) do + player.yiji_ids = ids + while table.find(ids, function(id) + return Fk:getCardById(id):getMark("yiji") > 0 + end) do if not room:askForUseActiveSkill(player, "yiji_active", "#yiji-give", true) then for _, id in ipairs(ids) do room:setCardMark(Fk:getCardById(id), "yiji", 0) end - ids = table.filter(ids, function(id) return room:getCardArea(id) ~= Card.PlayerHand end) + ids = table.filter(ids, function(id) + return room:getCardArea(id) ~= Card.PlayerHand + end) fakemove = { from = player.id, toArea = Card.Void, - moveInfo = table.map(ids, function(id) return {cardId = id, fromArea = Card.PlayerHand} end), - moveReason = fk.ReasonGive, + moveInfo = table.map(ids, function(id) + return { + cardId = id, + fromArea = Card.PlayerHand + } + end), + moveReason = fk.ReasonGive } - room:notifyMoveCards({player}, {fakemove}) + room:notifyMoveCards({ player }, { fakemove }) room:moveCards({ fromArea = Card.Void, ids = ids, to = player.id, toArea = Card.PlayerHand, moveReason = fk.ReasonGive, - skillName = self.name, + skillName = self.name }) end end - end, + end } -local yiji_active = fk.CreateActiveSkill{ +local yiji_active = fk.CreateActiveSkill { name = "yiji_active", mute = true, min_card_num = 1, @@ -321,36 +347,41 @@ local yiji_active = fk.CreateActiveSkill{ on_use = function(self, room, effect) local player = room:getPlayerById(effect.from) local target = room:getPlayerById(effect.tos[1]) - room:doIndicate(player.id, {target.id}) + room:doIndicate(player.id, { target.id }) for _, id in ipairs(effect.cards) do room:setCardMark(Fk:getCardById(id), "yiji", 0) end local fakemove = { from = player.id, toArea = Card.Void, - moveInfo = table.map(effect.cards, function(id) return {cardId = id, fromArea = Card.PlayerHand} end), - moveReason = fk.ReasonGive, + moveInfo = table.map(effect.cards, function(id) + return { + cardId = id, + fromArea = Card.PlayerHand + } + end), + moveReason = fk.ReasonGive } - room:notifyMoveCards({player}, {fakemove}) + room:notifyMoveCards({ player }, { fakemove }) room:moveCards({ fromArea = Card.Void, ids = effect.cards, to = target.id, toArea = Card.PlayerHand, moveReason = fk.ReasonGive, - skillName = self.name, + skillName = self.name }) - end, + end } local guojia = General:new(extension, "guojia", "wei", 3) Fk:addSkill(yiji_active) guojia:addSkill(tiandu) guojia:addSkill(yiji) -local luoshen = fk.CreateTriggerSkill{ +local luoshen = fk.CreateTriggerSkill { name = "luoshen", anim_type = "drawcard", - events = {fk.EventPhaseStart}, + events = { fk.EventPhaseStart }, can_trigger = function(self, event, target, player, data) return target == player and player:hasSkill(self.name) and player.phase == Player.Start end, @@ -360,35 +391,37 @@ local luoshen = fk.CreateTriggerSkill{ local judge = { who = player, reason = self.name, - pattern = ".|.|spade,club", + pattern = ".|.|spade,club" } room:judge(judge) if judge.card.color ~= Card.Black or player.dead or not room:askForSkillInvoke(player, self.name) then break end end - end, + end } -local luoshen_obtain = fk.CreateTriggerSkill{ +local luoshen_obtain = fk.CreateTriggerSkill { name = "#luoshen_obtain", mute = true, frequency = Skill.Compulsory, - events = {fk.FinishJudge}, + events = { fk.FinishJudge }, can_trigger = function(self, event, target, player, data) - return target == player and data.reason == "luoshen" and data.card.color == Card.Black end, + return target == player and data.reason == "luoshen" and data.card.color == Card.Black + end, on_use = function(self, event, target, player, data) player.room:obtainCard(player.id, data.card) - end, + end } luoshen:addRelatedSkill(luoshen_obtain) -local qingguo = fk.CreateViewAsSkill{ +local qingguo = fk.CreateViewAsSkill { name = "qingguo", anim_type = "defensive", pattern = "jink", card_filter = function(self, to_select, selected) - if #selected == 1 then return false end - return Fk:getCardById(to_select).color == Card.Black - and Fk:currentRoom():getCardArea(to_select) == Player.Hand + if #selected == 1 then + return false + end + return Fk:getCardById(to_select).color == Card.Black and Fk:currentRoom():getCardArea(to_select) == Player.Hand end, view_as = function(self, cards) if #cards ~= 1 then @@ -398,13 +431,13 @@ local qingguo = fk.CreateViewAsSkill{ c.skillName = self.name c:addSubcard(cards[1]) return c - end, + end } local zhenji = General:new(extension, "zhenji", "wei", 3, 3, General.Female) zhenji:addSkill(luoshen) zhenji:addSkill(qingguo) -local rende = fk.CreateActiveSkill{ +local rende = fk.CreateActiveSkill { name = "rende", anim_type = "support", card_filter = function(self, to_select, selected) @@ -423,17 +456,17 @@ local rende = fk.CreateActiveSkill{ room:moveCardTo(cards, Player.Hand, target, fk.ReasonGive, self.name, nil, false, player.id) room:addPlayerMark(player, "_rende_cards-phase", #cards) if marks < 2 and marks + #cards >= 2 and player:isWounded() then - room:recover{ + room:recover { who = player, num = 1, recoverBy = player, skillName = self.name } end - end, + end } -local jijiang = fk.CreateViewAsSkill{ +local jijiang = fk.CreateViewAsSkill { name = "jijiang$", anim_type = "offensive", pattern = "slash", @@ -449,59 +482,55 @@ local jijiang = fk.CreateViewAsSkill{ return c end, enabled_at_play = function(self, player) - return player:getMark("jijiang-failed-phase") == 0 and not table.every(Fk:currentRoom().alive_players, function(p) - return p == player or p.kingdom ~= "shu" - end) + return player:getMark("jijiang-failed-phase") == 0 and + not table.every(Fk:currentRoom().alive_players, function(p) + return p == player or p.kingdom ~= "shu" + end) end, enabled_at_response = function(self, player) - return not table.every(Fk:currentRoom().alive_players, function(p) - return p == player or p.kingdom ~= "shu" - end) - end, + return player:getMark("jijiang-failed-phase") == 0 and + not table.every(Fk:currentRoom().alive_players, function(p) + return p == player or p.kingdom ~= "shu" + end) + end } -local jijiangResponse = fk.CreateTriggerSkill{ +local jijiangResponse = fk.CreateTriggerSkill { name = "#jijiangResponse", - events = {fk.PreCardUse, fk.PreCardRespond}, + events = { fk.PreCardUse, fk.PreCardRespond }, mute = true, priority = 10, can_trigger = function(self, event, target, player, data) return target == player and player:hasSkill(self.name, true) and table.contains(data.card.skillNames, "jijiang") end, on_cost = function(self, event, target, player, data) - local room = player.room - room:doIndicate(player.id, TargetGroup:getRealTargets(data.tos)) + player.room:doIndicate(player.id, TargetGroup:getRealTargets(data.tos)) return true end, on_use = function(self, event, target, player, data) local room = player.room - for _, p in ipairs(room:getOtherPlayers(player)) do - if p.kingdom == "shu" then - local cardResponded = room:askForResponse(p, "slash", "slash", "#jijiang-ask:" .. player.id, true) - if cardResponded then - room:responseCard({ - from = p.id, - card = cardResponded, - skipDrop = true, - }) - - data.card = cardResponded - return false - end + local shus = table.filter(room:getOtherPlayers(player), function(p) + return p.kingdom == "shu" + end) + room:doIndicate(player.id, table.map(shus, function(p) + return p.id + end)) + for _, p in ipairs(shus) do + local cardResponded = room:askForResponse(p, "slash", "slash", "#jijiang-ask:" .. player.id, true) + if cardResponded then + data.card = cardResponded.card + return false end end - - if event == fk.PreCardUse and player.phase == Player.Play then - room:setPlayerMark(player, "jijiang-failed-phase", 1) - end + room:setPlayerMark(player, "jijiang-failed-phase", 1) return true end, - refresh_events = {fk.CardUsing}, + refresh_events = { fk.CardUsing, fk.CardResponding }, can_refresh = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name, true) and player:getMark("jijiang-failed-phase") > 0 + return player:hasSkill(self.name, true) and player:getMark("jijiang-failed-phase") > 0 end, on_refresh = function(self, event, target, player, data) player.room:setPlayerMark(player, "jijiang-failed-phase", 0) - end, + end } jijiang:addRelatedSkill(jijiangResponse) @@ -509,12 +538,14 @@ local liubei = General:new(extension, "liubei", "shu", 4) liubei:addSkill(rende) liubei:addSkill(jijiang) -local wusheng = fk.CreateViewAsSkill{ +local wusheng = fk.CreateViewAsSkill { name = "wusheng", anim_type = "offensive", pattern = "slash", card_filter = function(self, to_select, selected) - if #selected == 1 then return false end + if #selected == 1 then + return false + end return Fk:getCardById(to_select).color == Card.Red end, view_as = function(self, cards) @@ -525,62 +556,63 @@ local wusheng = fk.CreateViewAsSkill{ c.skillName = self.name c:addSubcard(cards[1]) return c - end, + end } local guanyu = General:new(extension, "guanyu", "shu", 4) guanyu:addSkill(wusheng) -local paoxiaoAudio = fk.CreateTriggerSkill{ +local paoxiaoAudio = fk.CreateTriggerSkill { name = "#paoxiaoAudio", visible = false, - refresh_events = {fk.CardUsing}, + refresh_events = { fk.CardUsing }, can_refresh = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name) and - data.card.trueName == "slash" and - player:usedCardTimes("slash") > 1 + return target == player and player:hasSkill(self.name) and data.card.trueName == "slash" and + player:usedCardTimes("slash") > 1 end, on_refresh = function(self, event, target, player, data) player:broadcastSkillInvoke("paoxiao") player.room:doAnimate("InvokeSkill", { name = "paoxiao", player = player.id, - skill_type = "offensive", + skill_type = "offensive" }) - end, + end } -local paoxiao = fk.CreateTargetModSkill{ +local paoxiao = fk.CreateTargetModSkill { name = "paoxiao", frequency = Skill.Compulsory, bypass_times = function(self, player, skill, scope) - if player:hasSkill(self.name) and skill.trueName == "slash_skill" - and scope == Player.HistoryPhase then + if player:hasSkill(self.name) and skill.trueName == "slash_skill" and scope == Player.HistoryPhase then return true end - end, + end } paoxiao:addRelatedSkill(paoxiaoAudio) local zhangfei = General:new(extension, "zhangfei", "shu", 4) zhangfei:addSkill(paoxiao) -local guanxing = fk.CreateTriggerSkill{ +local guanxing = fk.CreateTriggerSkill { name = "guanxing", anim_type = "control", - events = {fk.EventPhaseStart}, + events = { fk.EventPhaseStart }, can_trigger = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name) and - player.phase == Player.Start + return target == player and player:hasSkill(self.name) and player.phase == Player.Start end, on_use = function(self, event, target, player, data) local room = player.room room:askForGuanxing(player, room:getNCards(math.min(5, #room.alive_players))) - end, + end } -local kongchengAudio = fk.CreateTriggerSkill{ +local kongchengAudio = fk.CreateTriggerSkill { name = "#kongchengAudio", - refresh_events = {fk.AfterCardsMove}, + refresh_events = { fk.AfterCardsMove }, can_refresh = function(self, event, target, player, data) - if not player:hasSkill(self.name) then return end - if not player:isKongcheng() then return end + if not player:hasSkill(self.name) then + return + end + if not player:isKongcheng() then + return + end for _, move in ipairs(data) do if move.from == player.id then for _, info in ipairs(move.moveInfo) do @@ -594,27 +626,29 @@ local kongchengAudio = fk.CreateTriggerSkill{ on_refresh = function(self, event, target, player, data) player:broadcastSkillInvoke("kongcheng") player.room:notifySkillInvoked(player, "kongcheng", "defensive") - end, + end } -local kongcheng = fk.CreateProhibitSkill{ +local kongcheng = fk.CreateProhibitSkill { name = "kongcheng", frequency = Skill.Compulsory, is_prohibited = function(self, from, to, card) if to:hasSkill(self.name) and to:isKongcheng() then return card.trueName == "slash" or card.trueName == "duel" end - end, + end } kongcheng:addRelatedSkill(kongchengAudio) local zhugeliang = General:new(extension, "zhugeliang", "shu", 3) zhugeliang:addSkill(guanxing) zhugeliang:addSkill(kongcheng) -local longdan = fk.CreateViewAsSkill{ +local longdan = fk.CreateViewAsSkill { name = "longdan", pattern = "slash,jink", card_filter = function(self, to_select, selected) - if #selected == 1 then return false end + if #selected == 1 then + return false + end local _c = Fk:getCardById(to_select) local c if _c.trueName == "slash" then @@ -624,7 +658,8 @@ local longdan = fk.CreateViewAsSkill{ else return false end - return (Fk.currentResponsePattern == nil and Self:canUse(c)) or (Fk.currentResponsePattern and Exppattern:Parse(Fk.currentResponsePattern):match(c)) + return (Fk.currentResponsePattern == nil and Self:canUse(c)) or + (Fk.currentResponsePattern and Exppattern:Parse(Fk.currentResponsePattern):match(c)) end, view_as = function(self, cards) if #cards ~= 1 then @@ -640,69 +675,69 @@ local longdan = fk.CreateViewAsSkill{ c.skillName = self.name c:addSubcard(cards[1]) return c - end, + end } local zhaoyun = General:new(extension, "zhaoyun", "shu", 4) zhaoyun:addSkill(longdan) -local mashu = fk.CreateDistanceSkill{ +local mashu = fk.CreateDistanceSkill { name = "mashu", frequency = Skill.Compulsory, correct_func = function(self, from, to) if from:hasSkill(self.name) then return -1 end - end, + end } -local tieqi = fk.CreateTriggerSkill{ +local tieqi = fk.CreateTriggerSkill { name = "tieqi", anim_type = "offensive", - events = {fk.TargetSpecified}, + events = { fk.TargetSpecified }, can_trigger = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name) and - data.card.trueName == "slash" + player.tieqi_tos = data.tos + return target == player and player:hasSkill(self.name) and data.card.trueName == "slash" end, on_use = function(self, event, target, player, data) local room = player.room local judge = { who = player, reason = self.name, - pattern = ".|.|heart,diamond", + pattern = ".|.|heart,diamond" } room:judge(judge) if judge.card.color == Card.Red then data.disresponsive = true end - end, + end } local machao = General:new(extension, "machao", "shu", 4) machao:addSkill(mashu) machao:addSkill(tieqi) -local jizhi = fk.CreateTriggerSkill{ +local jizhi = fk.CreateTriggerSkill { name = "jizhi", anim_type = "drawcard", - events = {fk.CardUsing}, + events = { fk.CardUsing }, can_trigger = function(self, event, target, player, data) return target == player and player:hasSkill(self.name) and data.card:isCommonTrick() and - (not data.card:isVirtual() or #data.card.subcards == 0) + (not data.card:isVirtual() or #data.card.subcards == 0) end, on_use = function(self, event, target, player, data) player:drawCards(1, self.name) - end, + end } -local qicai = fk.CreateTargetModSkill{ +local qicai = fk.CreateTargetModSkill { name = "qicai", frequency = Skill.Compulsory, bypass_distances = function(self, player, skill, card) return player:hasSkill(self.name) and card and card.type == Card.TypeTrick - end, + end } local huangyueying = General:new(extension, "huangyueying", "shu", 3, 3, General.Female) huangyueying:addSkill(jizhi) huangyueying:addSkill(qicai) -local zhiheng = fk.CreateActiveSkill{ +local zhiheng = fk.CreateActiveSkill { name = "zhiheng", anim_type = "drawcard", can_use = function(self, player) @@ -717,36 +752,32 @@ local zhiheng = fk.CreateActiveSkill{ end } -local jiuyuan = fk.CreateTriggerSkill{ +local jiuyuan = fk.CreateTriggerSkill { name = "jiuyuan$", anim_type = "support", frequency = Skill.Compulsory, - events = {fk.PreHpRecover}, + events = { fk.PreHpRecover }, can_trigger = function(self, event, target, player, data) - return - target == player and - player:hasSkill(self.name) and - data.card and - data.card.trueName == "peach" and - data.recoverBy and - data.recoverBy.kingdom == "wu" and - data.recoverBy ~= player + return target == player and player:hasSkill(self.name) and data.card and data.card.trueName == "peach" and + data.recoverBy and data.recoverBy.kingdom == "wu" and data.recoverBy ~= player end, on_use = function(self, event, target, player, data) data.num = data.num + 1 - end, + end } local sunquan = General:new(extension, "sunquan", "wu", 4) sunquan:addSkill(zhiheng) sunquan:addSkill(jiuyuan) -local qixi = fk.CreateViewAsSkill{ +local qixi = fk.CreateViewAsSkill { name = "qixi", anim_type = "control", pattern = "dismantlement", card_filter = function(self, to_select, selected) - if #selected == 1 then return false end + if #selected == 1 then + return false + end return Fk:getCardById(to_select).color == Card.Black end, view_as = function(self, cards) @@ -757,26 +788,24 @@ local qixi = fk.CreateViewAsSkill{ c.skillName = self.name c:addSubcard(cards[1]) return c - end, + end } local ganning = General:new(extension, "ganning", "wu", 4) ganning:addSkill(qixi) -local keji = fk.CreateTriggerSkill{ +local keji = fk.CreateTriggerSkill { name = "keji", anim_type = "defensive", - events = {fk.EventPhaseChanging}, + events = { fk.EventPhaseChanging }, can_trigger = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name) and - data.to == Player.Discard and - player:usedCardTimes("slash") < 1 and - player:getMark("_keji_played_slash") == 0 + return target == player and player:hasSkill(self.name) and data.to == Player.Discard and + player:usedCardTimes("slash") < 1 and player:getMark("_keji_played_slash") == 0 end, on_use = function(self, event, target, player, data) return true end, - refresh_events = {fk.CardResponding, fk.EventPhaseStart}, + refresh_events = { fk.CardResponding, fk.EventPhaseStart }, can_refresh = function(self, event, target, player, data) if not (target == player and player:hasSkill(self.name)) then return false @@ -799,7 +828,7 @@ local keji = fk.CreateTriggerSkill{ local lvmeng = General:new(extension, "lvmeng", "wu", 4) lvmeng:addSkill(keji) -local kurou = fk.CreateActiveSkill{ +local kurou = fk.CreateActiveSkill { name = "kurou", anim_type = "drawcard", card_filter = function(self, to_select, selected, selected_targets) @@ -816,20 +845,22 @@ local kurou = fk.CreateActiveSkill{ local huanggai = General:new(extension, "huanggai", "wu", 4) huanggai:addSkill(kurou) -local yingzi = fk.CreateTriggerSkill{ +local yingzi = fk.CreateTriggerSkill { name = "yingzi", anim_type = "drawcard", - events = {fk.DrawNCards}, + events = { fk.DrawNCards }, on_use = function(self, event, target, player, data) data.n = data.n + 1 - end, + end } -local fanjian = fk.CreateActiveSkill{ +local fanjian = fk.CreateActiveSkill { name = "fanjian", can_use = function(self, player) return player:usedSkillTimes(self.name, Player.HistoryPhase) == 0 and not player:isKongcheng() end, - card_filter = function() return false end, + card_filter = function() + return false + end, target_filter = function(self, to_select, selected) return #selected == 0 and to_select ~= Self.id end, @@ -837,29 +868,31 @@ local fanjian = fk.CreateActiveSkill{ on_use = function(self, room, effect) local player = room:getPlayerById(effect.from) local target = room:getPlayerById(effect.tos[1]) - local choice = room:askForChoice(target, {"spade", "heart", "club", "diamond"}, self.name) + local choice = room:askForChoice(target, { "spade", "heart", "club", "diamond" }, self.name) local card = room:askForCardChosen(target, player, 'h', self.name) room:obtainCard(target.id, card, true, fk.ReasonPrey) if Fk:getCardById(card):getSuitString() ~= choice then - room:damage{ + room:damage { from = player, to = target, damage = 1, - skillName = self.name, + skillName = self.name } end - end, + end } local zhouyu = General:new(extension, "zhouyu", "wu", 3) zhouyu:addSkill(yingzi) zhouyu:addSkill(fanjian) -local guose = fk.CreateViewAsSkill{ +local guose = fk.CreateViewAsSkill { name = "guose", anim_type = "control", pattern = "indulgence", card_filter = function(self, to_select, selected) - if #selected == 1 then return false end + if #selected == 1 then + return false + end return Fk:getCardById(to_select).suit == Card.Diamond end, view_as = function(self, cards) @@ -870,20 +903,20 @@ local guose = fk.CreateViewAsSkill{ c.skillName = self.name c:addSubcard(cards[1]) return c - end, + end } -local liuli = fk.CreateTriggerSkill{ +local liuli = fk.CreateTriggerSkill { name = "liuli", anim_type = "defensive", - events = {fk.TargetConfirming}, + events = { fk.TargetConfirming }, can_trigger = function(self, event, target, player, data) - local ret = target == player and player:hasSkill(self.name) and - data.card.trueName == "slash" + local ret = target == player and player:hasSkill(self.name) and data.card.trueName == "slash" if ret then local room = player.room local from = room:getPlayerById(data.from) for _, p in ipairs(room.alive_players) do - if p ~= player and p.id ~= data.from and player:inMyAttackRange(p) and not from:isProhibited(p, data.card) then + if p ~= player and p.id ~= data.from and player:inMyAttackRange(p) and + not from:isProhibited(p, data.card) then return true end end @@ -899,10 +932,12 @@ local liuli = fk.CreateTriggerSkill{ table.insert(targets, p.id) end end - if #targets == 0 then return false end + if #targets == 0 then + return false + end local plist, cid = room:askForChooseCardAndPlayers(player, targets, 1, 1, nil, prompt, self.name, true) if #plist > 0 then - self.cost_data = {plist[1], cid} + self.cost_data = { plist[1], cid } return true end end, @@ -913,28 +948,32 @@ local liuli = fk.CreateTriggerSkill{ room:throwCard(self.cost_data[2], self.name, player, player) TargetGroup:removeTarget(data.targetGroup, player.id) TargetGroup:pushTargets(data.targetGroup, to) - end, + end } local daqiao = General:new(extension, "daqiao", "wu", 3, 3, General.Female) daqiao:addSkill(guose) daqiao:addSkill(liuli) -local qianxun = fk.CreateProhibitSkill{ +local qianxun = fk.CreateProhibitSkill { name = "qianxun", frequency = Skill.Compulsory, is_prohibited = function(self, from, to, card) if to:hasSkill(self.name) then return card.name == "indulgence" or card.name == "snatch" end - end, + end } -local lianying = fk.CreateTriggerSkill{ +local lianying = fk.CreateTriggerSkill { name = "lianying", anim_type = "drawcard", - events = {fk.AfterCardsMove}, + events = { fk.AfterCardsMove }, can_trigger = function(self, event, target, player, data) - if not player:hasSkill(self.name) then return end - if not player:isKongcheng() then return end + if not player:hasSkill(self.name) then + return + end + if not player:isKongcheng() then + return + end for _, move in ipairs(data) do if move.from == player.id then for _, info in ipairs(move.moveInfo) do @@ -947,18 +986,20 @@ local lianying = fk.CreateTriggerSkill{ end, on_use = function(self, event, target, player, data) player:drawCards(1, self.name) - end, + end } local luxun = General:new(extension, "luxun", "wu", 3) luxun:addSkill(qianxun) luxun:addSkill(lianying) -local xiaoji = fk.CreateTriggerSkill{ +local xiaoji = fk.CreateTriggerSkill { name = "xiaoji", anim_type = "drawcard", - events = {fk.AfterCardsMove}, + events = { fk.AfterCardsMove }, can_trigger = function(self, event, target, player, data) - if not player:hasSkill(self.name) then return end + if not player:hasSkill(self.name) then + return + end for _, move in ipairs(data) do if move.from == player.id then for _, info in ipairs(move.moveInfo) do @@ -982,7 +1023,9 @@ local xiaoji = fk.CreateTriggerSkill{ end self.cancel_cost = false for i = 1, i do - if self.cancel_cost or not player:hasSkill(self.name) then break end + if self.cancel_cost or not player:hasSkill(self.name) then + break + end self:doCost(event, target, player, data) end end, @@ -994,9 +1037,9 @@ local xiaoji = fk.CreateTriggerSkill{ end, on_use = function(self, event, target, player, data) player:drawCards(2, self.name) - end, + end } -local jieyin = fk.CreateActiveSkill{ +local jieyin = fk.CreateActiveSkill { name = "jieyin", anim_type = "support", can_use = function(self, player) @@ -1007,9 +1050,7 @@ local jieyin = fk.CreateActiveSkill{ end, target_filter = function(self, to_select, selected) local target = Fk:currentRoom():getPlayerById(to_select) - return target:isWounded() and - target.gender == General.Male - and #selected < 1 and to_select ~= Self.id + return target:isWounded() and target.gender == General.Male and #selected < 1 and to_select ~= Self.id end, target_num = 1, card_num = 2, @@ -1036,7 +1077,7 @@ local sunshangxiang = General:new(extension, "sunshangxiang", "wu", 3, 3, Genera sunshangxiang:addSkill(xiaoji) sunshangxiang:addSkill(jieyin) -local qingnang = fk.CreateActiveSkill{ +local qingnang = fk.CreateActiveSkill { name = "qingnang", anim_type = "support", can_use = function(self, player) @@ -1059,14 +1100,16 @@ local qingnang = fk.CreateActiveSkill{ recoverBy = from, skillName = self.name }) - end, + end } -local jijiu = fk.CreateViewAsSkill{ +local jijiu = fk.CreateViewAsSkill { name = "jijiu", anim_type = "support", pattern = "peach", card_filter = function(self, to_select, selected) - if #selected == 1 then return false end + if #selected == 1 then + return false + end return Fk:getCardById(to_select).color == Card.Red end, view_as = function(self, cards) @@ -1083,17 +1126,17 @@ local jijiu = fk.CreateViewAsSkill{ end, enabled_at_response = function(self, player) return player.phase == Player.NotActive - end, + end } local huatuo = General:new(extension, "huatuo", "qun", 3) huatuo:addSkill(qingnang) huatuo:addSkill(jijiu) -local wushuang = fk.CreateTriggerSkill{ +local wushuang = fk.CreateTriggerSkill { name = "wushuang", anim_type = "offensive", frequency = Skill.Compulsory, - events = {fk.TargetSpecified, fk.TargetConfirmed}, + events = { fk.TargetSpecified, fk.TargetConfirmed }, can_trigger = function(self, event, target, player, data) if not player:hasSkill(self.name) then return false @@ -1114,12 +1157,12 @@ local wushuang = fk.CreateTriggerSkill{ data.fixedAddTimesResponsors = data.fixedAddTimesResponsors or {} table.insert(data.fixedAddTimesResponsors, (event == fk.TargetSpecified and data.to or data.from)) end - end, + end } local lvbu = General:new(extension, "lvbu", "qun", 4) lvbu:addSkill(wushuang) -local lijian = fk.CreateActiveSkill{ +local lijian = fk.CreateActiveSkill { name = "lijian", anim_type = "offensive", can_use = function(self, player) @@ -1129,8 +1172,8 @@ local lijian = fk.CreateActiveSkill{ return #selected == 0 end, target_filter = function(self, to_select, selected) - return #selected < 2 and to_select ~= Self.id and - Fk:currentRoom():getPlayerById(to_select).gender == General.Male + return #selected < 2 and to_select ~= Self.id and Fk:currentRoom():getPlayerById(to_select).gender == + General.Male end, target_num = 2, min_card_num = 1, @@ -1143,29 +1186,28 @@ local lijian = fk.CreateActiveSkill{ from = use.tos[2], tos = { { use.tos[1] } }, card = duel, - prohibitedCardNames = { "nullification" }, + prohibitedCardNames = { "nullification" } } room:useCard(new_use) - end, + end } -local biyue = fk.CreateTriggerSkill{ +local biyue = fk.CreateTriggerSkill { name = "biyue", anim_type = "drawcard", - events = {fk.EventPhaseStart}, + events = { fk.EventPhaseStart }, can_trigger = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name) - and player.phase == Player.Finish + return target == player and player:hasSkill(self.name) and player.phase == Player.Finish end, on_use = function(self, event, target, player, data) player:drawCards(1, self.name) - end, + end } local diaochan = General:new(extension, "diaochan", "qun", 3, 3, General.Female) diaochan:addSkill(lijian) diaochan:addSkill(biyue) -local role_mode = fk.CreateGameMode{ - name = "aaa_role_mode", -- just to let it at the top of list +local role_mode = fk.CreateGameMode { + name = "aaa_role_mode", -- just to let it at the top of list minPlayer = 2, maxPlayer = 8, is_counted = function(self, room) @@ -1174,16 +1216,12 @@ local role_mode = fk.CreateGameMode{ surrender_func = function(self, playedTime) local roleCheck = false local roleText = "" - local roleTable = { - { "lord" }, - { "lord", "rebel" }, - { "lord", "rebel", "renegade" }, - { "lord", "loyalist", "rebel", "renegade" }, - { "lord", "loyalist", "rebel", "rebel", "renegade" }, - { "lord", "loyalist", "rebel", "rebel", "rebel", "renegade" }, - { "lord", "loyalist", "loyalist", "rebel", "rebel", "rebel", "renegade" }, - { "lord", "loyalist", "loyalist", "rebel", "rebel", "rebel", "rebel", "renegade" }, - } + local roleTable = { { "lord" }, { "lord", "rebel" }, { "lord", "rebel", "renegade" }, + { "lord", "loyalist", "rebel", "renegade" }, + { "lord", "loyalist", "rebel", "rebel", "renegade" }, + { "lord", "loyalist", "rebel", "rebel", "rebel", "renegade" }, + { "lord", "loyalist", "loyalist", "rebel", "rebel", "rebel", "renegade" }, + { "lord", "loyalist", "loyalist", "rebel", "rebel", "rebel", "rebel", "renegade" } } roleTable = roleTable[#Fk:currentRoom().players] @@ -1230,7 +1268,10 @@ local role_mode = fk.CreateGameMode{ roleText = "left one rebel alive" else if Self.role == "loyalist" then - return { { text = "loyalist never surrender", passed = false } } + return { { + text = "loyalist never surrender", + passed = false + } } else if #Fk:currentRoom().alive_players == 2 then roleCheck = true @@ -1263,27 +1304,30 @@ local role_mode = fk.CreateGameMode{ roleText = "left you alive" end - return { - { text = "time limitation: 5 min", passed = playedTime >= 300 }, - { text = roleText, passed = roleCheck }, - } - end, + return { { + text = "time limitation: 5 min", + passed = playedTime >= 300 + }, { + text = roleText, + passed = roleCheck + } } + end } extension:addGameMode(role_mode) -Fk:loadTranslationTable{ +Fk:loadTranslationTable { ["time limitation: 5 sec"] = "游戏时长达到5秒(测试用)", ["left lord and loyalist alive"] = "仅剩你和主忠方存活", ["left one rebel alive"] = "反贼仅剩你存活且不存在存活内奸", ["left you alive"] = "主忠方仅剩你存活且其他阵营仅剩一方", - ["loyalist never surrender"] = "忠臣永不投降!", + ["loyalist never surrender"] = "忠臣永不投降!" } local anjiang = General(extension, "anjiang", "unknown", 5) anjiang.gender = General.Agender anjiang.total_hidden = true -Fk:loadTranslationTable{ - ["anjiang"] = "暗将", +Fk:loadTranslationTable { + ["anjiang"] = "暗将" } -- load translations of this package diff --git a/packages/standard/standard_ai.lua b/packages/standard/standard_ai.lua new file mode 100644 index 00000000..af9b0a20 --- /dev/null +++ b/packages/standard/standard_ai.lua @@ -0,0 +1,227 @@ +fk.ai_use_play.rende = function(self, skill) + for _, p in ipairs(self.friends_noself) do + if p.kingdom == "shu" and #self.player:getCardIds("h") >= self.player.hp then + self.use_id = {} + for _, cid in ipairs(self.player:getCardIds("h")) do + if #self.use_id < #self.player:getCardIds("h") / 2 then + table.insert(self.use_id, cid) + end + end + self.use_tos = { p.id } + return + end + end + for _, p in ipairs(self.friends_noself) do + if #self.player:getCardIds("h") >= self.player.hp then + self.use_id = {} + for _, cid in ipairs(self.player:getCardIds("h")) do + if #self.use_id < #self.player:getCardIds("h") / 2 then + table.insert(self.use_id, cid) + end + end + self.use_tos = { p.id } + return + end + end +end + +fk.ai_card.jijiang = { priority = 10 } + +fk.ai_use_play.lijian = function(self, skill) + local c = Fk:cloneCard("duel") + c.skillName = "lijian" + for _, p in ipairs(self.enemies) do + for _, pt in ipairs(self.enemies) do + if p.gender == General.Male and pt.gender == General.Male and p.id ~= pt.id and + c.skill:targetFilter(pt.id, {}, p.id, c) then + self.use_id = { self.player:getCardIds("he")[1] } + self.use_tos = { pt.id, p.id } + break + end + end + end + for _, p in ipairs(self.friends_noself) do + for _, pt in ipairs(self.enemies) do + if p.gender == General.Male and pt.gender == General.Male and p.id ~= pt.id and + c.skill:targetFilter(pt.id, {}, p.id, c) then + self.use_id = { self.player:getCardIds("he")[1] } + self.use_tos = { pt.id, p.id } + break + end + end + end +end + +fk.ai_card.lijian = { priority = 2 } + +fk.ai_use_play.zhiheng = function(self, skill) + local card_ids = {} + for _, h in ipairs(self.player:getCardIds("he")) do + if #card_ids < #self.player:getCardIds("he") / 2 then + table.insert(card_ids, h) + end + end + if #card_ids > 0 then + self.use_id = card_ids + end +end + +fk.ai_use_play.kurou = function(self, skill) + if #self:getActives("peach") + self.player.hp > 1 then + local slash = Fk:cloneCard("slash") + if slash.skill:canUse(self.player, slash) and not self.player:prohibitUse(slash) then + fk.ai_use_play.slash(self, slash) + if self.use_id then + self.use_id = {} + self.use_tos = {} + end + end + end +end + +fk.ai_use_play.fanjian = function(self, skill) + for _, p in ipairs(self.enemies) do + if #self.player:getCardIds("h") > 0 then + self.use_id = {} + table.insert(self.use_tos, p.id) + break + end + end +end + +fk.ai_use_play.jieyin = function(self, skill) + for cs, p in ipairs(self.friends_noself) do + cs = self.player:getCardIds("h") + if #cs > 1 and p.gender == General.Male and p:isWounded() then + self.use_id = { cs[1], cs[2] } + table.insert(self.use_tos, p.id) + break + end + end +end + +fk.ai_use_play.qingnang = function(self, skill) + for cs, p in ipairs(self.friends) do + cs = self.player:getCardIds("h") + if #cs > 0 and p:isWounded() then + self.use_id = { cs[1] } + table.insert(self.use_tos, p.id) + break + end + end +end + +fk.ai_skill_invoke.jianxiong = true + +fk.ai_card.hujia = { priority = 10 } + +fk.ai_response_card["#hujia-ask"] = function(self, pattern, prompt, cancelable, data) + local to = self.room:getPlayerById(tonumber(prompt:split(":")[2])) + if to and self:isFriend(to) then + self:setUseId(pattern) + end +end + +fk.ai_response_card["#jijiang-ask"] = fk.ai_response_card["#hujia-ask"] + +fk.ai_skill_invoke.fankui = function(self, data, prompt) + local damage = self:eventData("Damage") + return damage and damage.from and not self:isFriend(damage.from) +end + +fk.ai_response_card["#guicai-ask"] = function(self, pattern, prompt, cancelable, data) + local cards = table.map(self.player:getHandlyIds(true), function(id) + return Fk:getCardById(id) + end) + local id = self:getRetrialCardId(cards) + if id then + self.use_id = id + end +end + +fk.ai_skill_invoke.ganglie = function(self, data, prompt) + local damage = self:eventData("Damage") + return damage and damage.from and not self:isFriend(damage.from) +end + +fk.ai_skill_invoke.luoyi = function(self, data, prompt) + for _, p in ipairs(self.enemies) do + if #self:getActives("slash") > 0 and not self:isWeak() then + return true + end + end +end + +fk.ai_skill_invoke.tiandu = true + +fk.ai_skill_invoke.yiji = true + +fk.ai_skill_invoke.luoshen = true + +fk.ai_skill_invoke.guanxing = true + +fk.ai_skill_invoke.tieqi = function(self, data, prompt) + local use = self:eventData("UseCard") + for _, p in ipairs(TargetGroup:getRealTargets(use.tos)) do + p = self.room:getPlayerById(p) + if self:isEnemie(p) then + return true + end + end +end + +fk.ai_skill_invoke.jizhi = true + +fk.ai_skill_invoke.keji = true + +fk.ai_skill_invoke.yingzi = true + +fk.ai_skill_invoke.lianying = true + +fk.ai_skill_invoke.xiaoji = true + +fk.ai_skill_invoke.biyue = true + +fk.ai_choose_players.tuxi = function(self, targets, min_num, num, cancelable) + for _, pid in ipairs(targets) do + local p = self.room:getPlayerById(pid) + if self:isEnemie(p) and #self.use_tos < num then + table.insert(self.use_tos, pid) + end + end +end + +fk.ai_use_skill.yiji_active = function(self, prompt, cancelable, data) + for _, p in ipairs(self.friends_noself) do + for c, cid in ipairs(self.player.yiji_ids) do + c = Fk:getCardById(cid) + if c:getMark("yiji") > 0 and c.skill:canUse(p, c) then + self.use_tos = { p.id } + self.use_id = json.encode { + skill = "yiji_active", + subcards = { cid } + } + return + end + end + end +end + +fk.ai_choose_players.liuli = function(self, targets, min_num, num, cancelable) + for _, pid in ipairs(targets) do + local p = self.room:getPlayerById(pid) + if self:isEnemie(p) and #self.use_tos < num and #self.player:getCardIds("he") > 0 then + table.insert(self.use_tos, pid) + self.use_id = { self.player:getCardIds("he")[1] } + return + end + end + for _, pid in ipairs(targets) do + local p = self.room:getPlayerById(pid) + if not self:isFriend(p) and #self.use_tos < num and #self.player:getCardIds("he") > 0 then + table.insert(self.use_tos, pid) + self.use_id = { self.player:getCardIds("he")[1] } + return + end + end +end diff --git a/packages/standard_cards/i18n/zh_CN.lua b/packages/standard_cards/i18n/zh_CN.lua index 8bd9ec4d..763cbc3d 100644 --- a/packages/standard_cards/i18n/zh_CN.lua +++ b/packages/standard_cards/i18n/zh_CN.lua @@ -1,6 +1,6 @@ -- SPDX-License-Identifier: GPL-3.0-or-later -Fk:loadTranslationTable{ +Fk:loadTranslationTable { ["standard_cards"] = "标+EX", ["unknown_card"] = '未知牌', @@ -50,13 +50,12 @@ Fk:loadTranslationTable{ ["slash"] = "杀", [":slash"] = "基本牌
时机:出牌阶段
目标:攻击范围内的一名其他角色
效果:对目标角色造成1点伤害。", ["#slash-jink"] = "%src 对你使用了杀,请使用 %arg 张闪", + ["#slash-jinks"] = "%src 对你使用了杀,需 %arg2 张闪,你还需使用 %arg 张闪", ["jink"] = "闪", [":jink"] = "基本牌
时机:【杀】对你生效时
目标:此【杀】
效果:抵消此【杀】的效果。", - ["peach"] = "桃", [":peach"] = "基本牌
时机:出牌阶段/一名角色处于濒死状态时
目标:已受伤的你/处于濒死状态的角色
效果:目标角色回复1点体力。", - ["dismantlement"] = "过河拆桥", [":dismantlement"] = "锦囊牌
时机:出牌阶段
目标:一名区域内有牌的其他角色。
效果:你弃置目标角色区域内的一张牌。", ["dismantlement_skill"] = "过河拆桥", @@ -67,26 +66,19 @@ Fk:loadTranslationTable{ ["duel"] = "决斗", [":duel"] = "锦囊牌
时机:出牌阶段
目标:一名其他角色
效果:由目标角色开始,你与其轮流:打出一张【杀】,否则受到对方的1点伤害并结束此牌结算。", - ["collateral"] = "借刀杀人", [":collateral"] = "锦囊牌
时机:出牌阶段
目标:装备区内有武器牌且攻击范围内有【杀】的合法目标的一名其他角色A(你需要选择一名A攻击范围内的【杀】的合法目标B)
效果:A须对B使用一张【杀】,否则你获得A装备区内的武器牌。", ["#collateral-slash"] = "借刀杀人:你需对 %dest 使用【杀】,否则 %src 获得你的武器", - ["ex_nihilo"] = "无中生有", [":ex_nihilo"] = "锦囊牌
时机:出牌阶段
目标:你
效果:目标角色摸两张牌。", - ["nullification"] = "无懈可击", [":nullification"] = "锦囊牌
时机:锦囊牌对目标角色生效前,或一张【无懈可击】生效前
目标:该锦囊牌
效果:抵消该锦囊牌对该角色产生的效果,或抵消另一张【无懈可击】产生的效果。", - ["savage_assault"] = "南蛮入侵", [":savage_assault"] = "锦囊牌
时机:出牌阶段
目标:所有其他角色
效果:每名目标角色须打出一张【杀】,否则受到1点伤害。", - ["archery_attack"] = "万箭齐发", [":archery_attack"] = "锦囊牌
时机:出牌阶段
目标:所有其他角色
效果:每名目标角色须打出一张【闪】,否则受到1点伤害。", - ["god_salvation"] = "桃园结义", [":god_salvation"] = "锦囊牌
时机:出牌阶段
目标:所有角色
效果:每名目标角色回复1点体力。", - ["amazing_grace"] = "五谷丰登", [":amazing_grace"] = "锦囊牌
时机:出牌阶段
目标:所有角色
效果:你亮出牌堆顶等于角色数的牌,每名目标角色获得其中一张牌,然后将其余的牌置入弃牌堆。", ["amazing_grace_skill"] = "五谷选牌", @@ -94,16 +86,12 @@ Fk:loadTranslationTable{ ["lightning"] = "闪电", [":lightning"] = "延时锦囊牌
时机:出牌阶段
目标:你
效果:将此牌置于目标角色判定区内。其判定阶段进行判定:若结果为黑桃2-9,其受到3点雷电伤害并将【闪电】置入弃牌堆,否则将【闪电】移动至其下家判定区内。", - ["indulgence"] = "乐不思蜀", [":indulgence"] = "延时锦囊牌
时机:出牌阶段
目标:一名其他角色
效果:将此牌置于目标角色判定区内。其判定阶段进行判定:若结果不为红桃,其跳过出牌阶段。然后将【乐不思蜀】置入弃牌堆。", - ["crossbow"] = "诸葛连弩", [":crossbow"] = "装备牌·武器
攻击范围:1
武器技能:锁定技。你于出牌阶段内使用【杀】无次数限制。", - ["qinggang_sword"] = "青釭剑", [":qinggang_sword"] = "装备牌·武器
攻击范围:2
武器技能:锁定技。你的【杀】无视目标角色的防具。", - ["ice_sword"] = "寒冰剑", [":ice_sword"] = "装备牌·武器
攻击范围:2
武器技能:每当你使用【杀】对目标角色造成伤害时,若该角色有牌,你可以防止此伤害,然后依次弃置其两张牌。", ["#ice_sword_skill"] = "寒冰剑", @@ -130,7 +118,6 @@ Fk:loadTranslationTable{ ["halberd"] = "方天画戟", [":halberd"] = "装备牌·武器
攻击范围:4
武器技能:锁定技。你使用最后的手牌【杀】可以额外选择至多两名目标。", - ["kylin_bow"] = "麒麟弓", [":kylin_bow"] = "装备牌·武器
攻击范围:5
武器技能:每当你使用【杀】对目标角色造成伤害时,你可以弃置其装备区内的一张坐骑牌。", ["#kylin_bow_skill"] = "麒麟弓", diff --git a/packages/standard_cards/init.lua b/packages/standard_cards/init.lua index 5e9b2a92..b752c50c 100644 --- a/packages/standard_cards/init.lua +++ b/packages/standard_cards/init.lua @@ -1,28 +1,28 @@ -- SPDX-License-Identifier: GPL-3.0-or-later - local extension = Package:new("standard_cards", Package.CardPack) extension.metadata = require "packages.standard_cards.metadata" -local slashSkill = fk.CreateActiveSkill{ +local slashSkill = fk.CreateActiveSkill { name = "slash_skill", max_phase_use_time = 1, target_num = 1, can_use = function(self, player, card) - return - table.find(Fk:currentRoom().alive_players, function(p) - return self:withinTimesLimit(player, Player.HistoryPhase, card, "slash", p) - end) + return table.find(Fk:currentRoom().alive_players, function(p) + return self:withinTimesLimit(player, Player.HistoryPhase, card, "slash", p) + end) end, mod_target_filter = function(self, to_select, selected, user, card, distance_limited) local player = Fk:currentRoom():getPlayerById(to_select) local from = Fk:currentRoom():getPlayerById(user) - return from ~= player and not (distance_limited and not self:withinDistanceLimit(from, true, card, player)) + return + user ~= to_select and not (distance_limited and not self:withinDistanceLimit(from, true, card, player)) and + not (card and from:isProhibited(player, card)) end, target_filter = function(self, to_select, selected, _, card) if #selected < self:getMaxTargetNum(Self, card) then local player = Fk:currentRoom():getPlayerById(to_select) return self:modTargetFilter(to_select, selected, Self.id, card, true) and - (#selected > 0 or self:withinTimesLimit(Self, Player.HistoryPhase, card, "slash", player)) + (#selected > 0 or self:withinTimesLimit(Self, Player.HistoryPhase, card, "slash", player)) end end, on_effect = function(self, room, effect) @@ -40,51 +40,27 @@ local slashSkill = fk.CreateActiveSkill{ end end } -local slash = fk.CreateBasicCard{ +local slash = fk.CreateBasicCard { name = "slash", number = 7, suit = Card.Spade, is_damage_card = true, - skill = slashSkill, + skill = slashSkill } -extension:addCards({ - slash, - slash:clone(Card.Spade, 8), - slash:clone(Card.Spade, 8), - slash:clone(Card.Spade, 9), - slash:clone(Card.Spade, 9), - slash:clone(Card.Spade, 10), - slash:clone(Card.Spade, 10), +extension:addCards({ slash, slash:clone(Card.Spade, 8), slash:clone(Card.Spade, 8), slash:clone(Card.Spade, 9), + slash:clone(Card.Spade, 9), slash:clone(Card.Spade, 10), slash:clone(Card.Spade, 10), - slash:clone(Card.Club, 2), - slash:clone(Card.Club, 3), - slash:clone(Card.Club, 4), - slash:clone(Card.Club, 5), - slash:clone(Card.Club, 6), - slash:clone(Card.Club, 7), - slash:clone(Card.Club, 8), - slash:clone(Card.Club, 8), - slash:clone(Card.Club, 9), - slash:clone(Card.Club, 9), - slash:clone(Card.Club, 10), - slash:clone(Card.Club, 10), - slash:clone(Card.Club, 11), - slash:clone(Card.Club, 11), + slash:clone(Card.Club, 2), slash:clone(Card.Club, 3), slash:clone(Card.Club, 4), + slash:clone(Card.Club, 5), slash:clone(Card.Club, 6), slash:clone(Card.Club, 7), + slash:clone(Card.Club, 8), slash:clone(Card.Club, 8), slash:clone(Card.Club, 9), + slash:clone(Card.Club, 9), slash:clone(Card.Club, 10), slash:clone(Card.Club, 10), + slash:clone(Card.Club, 11), slash:clone(Card.Club, 11), slash:clone(Card.Heart, 10), + slash:clone(Card.Heart, 10), slash:clone(Card.Heart, 11), slash:clone(Card.Diamond, 6), + slash:clone(Card.Diamond, 7), slash:clone(Card.Diamond, 8), slash:clone(Card.Diamond, 9), + slash:clone(Card.Diamond, 10), slash:clone(Card.Diamond, 13) }) - slash:clone(Card.Heart, 10), - slash:clone(Card.Heart, 10), - slash:clone(Card.Heart, 11), - - slash:clone(Card.Diamond, 6), - slash:clone(Card.Diamond, 7), - slash:clone(Card.Diamond, 8), - slash:clone(Card.Diamond, 9), - slash:clone(Card.Diamond, 10), - slash:clone(Card.Diamond, 13), -}) - -local jinkSkill = fk.CreateActiveSkill{ +local jinkSkill = fk.CreateActiveSkill { name = "jink_skill", can_use = function() return false @@ -95,39 +71,26 @@ local jinkSkill = fk.CreateActiveSkill{ end end } -local jink = fk.CreateBasicCard{ +local jink = fk.CreateBasicCard { name = "jink", suit = Card.Heart, number = 2, - skill = jinkSkill, + skill = jinkSkill } -extension:addCards({ - jink, - jink:clone(Card.Heart, 2), - jink:clone(Card.Heart, 13), +extension:addCards({ jink, jink:clone(Card.Heart, 2), jink:clone(Card.Heart, 13), jink:clone(Card.Diamond, 2), + jink:clone(Card.Diamond, 2), jink:clone(Card.Diamond, 3), jink:clone(Card.Diamond, 4), + jink:clone(Card.Diamond, 5), jink:clone(Card.Diamond, 6), jink:clone(Card.Diamond, 7), + jink:clone(Card.Diamond, 8), jink:clone(Card.Diamond, 9), jink:clone(Card.Diamond, 10), + jink:clone(Card.Diamond, 11), jink:clone(Card.Diamond, 11) }) - jink:clone(Card.Diamond, 2), - jink:clone(Card.Diamond, 2), - jink:clone(Card.Diamond, 3), - jink:clone(Card.Diamond, 4), - jink:clone(Card.Diamond, 5), - jink:clone(Card.Diamond, 6), - jink:clone(Card.Diamond, 7), - jink:clone(Card.Diamond, 8), - jink:clone(Card.Diamond, 9), - jink:clone(Card.Diamond, 10), - jink:clone(Card.Diamond, 11), - jink:clone(Card.Diamond, 11), -}) - -local peachSkill = fk.CreateActiveSkill{ +local peachSkill = fk.CreateActiveSkill { name = "peach_skill", mod_target_filter = function(self, to_select) return Fk:currentRoom():getPlayerById(to_select):isWounded() and - not table.find(Fk:currentRoom().alive_players, function(p) - return p.dying - end) + not table.find(Fk:currentRoom().alive_players, function(p) + return p.dying + end) end, can_use = function(self, player, card) return player:isWounded() and not player:isProhibited(player, card) @@ -151,72 +114,65 @@ local peachSkill = fk.CreateActiveSkill{ end end } -local peach = fk.CreateBasicCard{ +local peach = fk.CreateBasicCard { name = "peach", suit = Card.Heart, number = 3, - skill = peachSkill, + skill = peachSkill } -extension:addCards({ - peach, - peach:clone(Card.Heart, 4), - peach:clone(Card.Heart, 6), - peach:clone(Card.Heart, 7), - peach:clone(Card.Heart, 8), - peach:clone(Card.Heart, 9), - peach:clone(Card.Heart, 12), - peach:clone(Card.Heart, 12), -}) +extension:addCards({ peach, peach:clone(Card.Heart, 4), peach:clone(Card.Heart, 6), peach:clone(Card.Heart, 7), + peach:clone(Card.Heart, 8), peach:clone(Card.Heart, 9), peach:clone(Card.Heart, 12), + peach:clone(Card.Heart, 12) }) -local dismantlementSkill = fk.CreateActiveSkill{ +local dismantlementSkill = fk.CreateActiveSkill { name = "dismantlement_skill", target_num = 1, - mod_target_filter = function(self, to_select, selected, user) + mod_target_filter = function(self, to_select, selected, user, card) local player = Fk:currentRoom():getPlayerById(to_select) - return Fk:currentRoom():getPlayerById(user) ~= player and not player:isAllNude() + local from = Fk:currentRoom():getPlayerById(user) + return user ~= to_select and not player:isAllNude() and not (card and from:isProhibited(player, card)) end, - target_filter = function(self, to_select, selected) - if #selected < self:getMaxTargetNum(Self) then - return self:modTargetFilter(to_select, selected, Self.id) + target_filter = function(self, to_select, selected, _, card) + if #selected < self:getMaxTargetNum(Self, card) then + return self:modTargetFilter(to_select, selected, Self.id, card) end end, on_effect = function(self, room, effect) local from = room:getPlayerById(effect.from) local to = room:getPlayerById(effect.to) - if to.dead or to:isAllNude() then return end + if to.dead or to:isAllNude() then + return + end local cid = room:askForCardChosen(from, to, "hej", self.name) - room:throwCard({cid}, self.name, to, from) + room:throwCard({ cid }, self.name, to, from) end } -local dismantlement = fk.CreateTrickCard{ +local dismantlement = fk.CreateTrickCard { name = "dismantlement", suit = Card.Spade, number = 3, - skill = dismantlementSkill, + skill = dismantlementSkill } -extension:addCards({ - dismantlement, - dismantlement:clone(Card.Spade, 4), - dismantlement:clone(Card.Spade, 12), +extension:addCards({ dismantlement, dismantlement:clone(Card.Spade, 4), dismantlement:clone(Card.Spade, 12), + dismantlement:clone(Card.Club, 3), dismantlement:clone(Card.Club, 4), - dismantlement:clone(Card.Club, 3), - dismantlement:clone(Card.Club, 4), + dismantlement:clone(Card.Heart, 12) }) - dismantlement:clone(Card.Heart, 12), -}) - -local snatchSkill = fk.CreateActiveSkill{ +local snatchSkill = fk.CreateActiveSkill { name = "snatch_skill", distance_limit = 1, mod_target_filter = function(self, to_select, selected, user, card, distance_limited) local player = Fk:currentRoom():getPlayerById(to_select) local from = Fk:currentRoom():getPlayerById(user) - return from ~= player and not (player:isAllNude() or (distance_limited and not self:withinDistanceLimit(from, false, card, player))) + return user ~= to_select and + not (player:isAllNude() or + (distance_limited and not self:withinDistanceLimit(from, false, card, player))) and + not (card and from:isProhibited(player, card)) end, target_filter = function(self, to_select, selected, _, card) - if #selected == 0 then + if #selected < self:getMaxTargetNum(Self, card) then return self:modTargetFilter(to_select, selected, Self.id, card, true) end end, @@ -224,35 +180,33 @@ local snatchSkill = fk.CreateActiveSkill{ on_effect = function(self, room, effect) local from = room:getPlayerById(effect.from) local to = room:getPlayerById(effect.to) - if to.dead or to:isAllNude() then return end + if to.dead or to:isAllNude() then + return + end local cid = room:askForCardChosen(from, to, "hej", self.name) room:obtainCard(from, cid, false, fk.ReasonPrey) end } -local snatch = fk.CreateTrickCard{ +local snatch = fk.CreateTrickCard { name = "snatch", suit = Card.Spade, number = 3, - skill = snatchSkill, + skill = snatchSkill } -extension:addCards({ - snatch, - snatch:clone(Card.Spade, 4), - snatch:clone(Card.Spade, 11), +extension:addCards({ snatch, snatch:clone(Card.Spade, 4), snatch:clone(Card.Spade, 11), snatch:clone(Card.Diamond, 3), + snatch:clone(Card.Diamond, 4) }) - snatch:clone(Card.Diamond, 3), - snatch:clone(Card.Diamond, 4), -}) - -local duelSkill = fk.CreateActiveSkill{ +local duelSkill = fk.CreateActiveSkill { name = "duel_skill", - mod_target_filter = function(self, to_select, selected, user) - return user ~= to_select + mod_target_filter = function(self, to_select, selected, user, card) + local player = Fk:currentRoom():getPlayerById(to_select) + local from = Fk:currentRoom():getPlayerById(user) + return user ~= to_select and not (card and from:isProhibited(player, card)) end, - target_filter = function(self, to_select, selected) - if #selected == 0 then - return self:modTargetFilter(to_select, selected, Self.id) + target_filter = function(self, to_select, selected, _, card) + if #selected < self:getMaxTargetNum(Self, card) then + return self:modTargetFilter(to_select, selected, Self.id, card) end end, target_num = 1, @@ -270,7 +224,6 @@ local duelSkill = fk.CreateActiveSkill{ if effect.fixedAddTimesResponsors then canFix = table.contains(effect.fixedAddTimesResponsors, currentResponser.id) end - if canFix then if type(effect.fixedResponseTimes) == 'table' then loopTimes = effect.fixedResponseTimes["slash"] or 1 @@ -279,25 +232,17 @@ local duelSkill = fk.CreateActiveSkill{ end end end - - local cardResponded + local can for i = 1, loopTimes do - cardResponded = room:askForResponse(currentResponser, 'slash', nil, nil, false, nil, effect) - if cardResponded then - room:responseCard({ - from = currentResponser.id, - card = cardResponded, - responseToEvent = effect, - }) + if room:askForResponse(currentResponser, 'slash', nil, nil, false, nil, effect) then else + can = true break end end - - if not cardResponded then + if can then break end - currentTurn = currentTurn % 2 + 1 currentResponser = responsers[currentTurn] end @@ -309,79 +254,90 @@ local duelSkill = fk.CreateActiveSkill{ card = effect.card, damage = 1, damageType = fk.NormalDamage, - skillName = self.name, + skillName = self.name }) end end } -local duel = fk.CreateTrickCard{ +local duel = fk.CreateTrickCard { name = "duel", suit = Card.Spade, number = 1, is_damage_card = true, - skill = duelSkill, + skill = duelSkill } -extension:addCards({ - duel, +extension:addCards({ duel, duel:clone(Card.Club, 1), duel:clone(Card.Diamond, 1) }) - duel:clone(Card.Club, 1), - - duel:clone(Card.Diamond, 1), -}) - -local collateralSkill = fk.CreateActiveSkill{ +local collateralSkill = fk.CreateActiveSkill { name = "collateral_skill", - mod_target_filter = function(self, to_select, selected, user, card, distance_limited) + mod_target_filter = function(self, to_select, selected, user, card) local player = Fk:currentRoom():getPlayerById(to_select) - return user ~= player.id and player:getEquipment(Card.SubtypeWeapon) + local from = Fk:currentRoom():getPlayerById(user) + return user ~= to_select and player:getEquipment(Card.SubtypeWeapon) and + not (card and from:isProhibited(player, card)) end, - target_filter = function(self, to_select, selected) - local player = Fk:currentRoom():getPlayerById(to_select) - if #selected == 0 then - return Self ~= player and player:getEquipment(Card.SubtypeWeapon) - elseif #selected == 1 then - return Fk:currentRoom():getPlayerById(selected[1]):inMyAttackRange(player) + target_filter = function(self, to_select, selected, _, card) + if #selected >= (self:getMaxTargetNum(Self, card) - 1) * 2 then + return false + elseif #selected % 2 == 0 then + return self:modTargetFilter(to_select, selected, Self.id, card) + elseif #selected > 0 then + return Fk:currentRoom():getPlayerById(selected[#selected]):inMyAttackRange( + Fk:currentRoom():getPlayerById(to_select)) end end, target_num = 2, on_use = function(self, room, cardUseEvent) - cardUseEvent.tos = { { cardUseEvent.tos[1][1], cardUseEvent.tos[2][1] } } + -- cardUseEvent.tos = {{cardUseEvent.tos[1][1], cardUseEvent.tos[2][1]}} + local tos = {} + local exclusive = {} + for i, pid in ipairs(TargetGroup:getRealTargets(cardUseEvent.tos)) do + if i % 2 == 1 then + exclusive = { pid } + else + table.insert(exclusive, pid) + table.insert(tos, exclusive) + end + end + cardUseEvent.tos = tos end, on_effect = function(self, room, effect) local to = room:getPlayerById(effect.to) - if to.dead or not to:getEquipment(Card.SubtypeWeapon) then return end - local prompt = "#collateral-slash:"..effect.from..":"..effect.subTargets[1] + if to.dead or not to:getEquipment(Card.SubtypeWeapon) then + return + end + local prompt = "#collateral-slash:" .. effect.from .. ":" .. effect.subTargets[1] if #effect.subTargets > 1 then prompt = nil end - local use = room:askForUseCard(to, "slash", nil, prompt, nil, { must_targets = effect.subTargets }, effect) - if use then - use.extraUse = true - room:useCard(use) + if room:askForUseCard(to, "slash", nil, prompt, nil, { + must_targets = effect.subTargets, + exclusive_targets = effect.subTargets, + bypass_distances = true, + bypass_times = true + }, effect) then else - room:obtainCard(effect.from, - room:getPlayerById(effect.to):getEquipment(Card.SubtypeWeapon), - true, fk.ReasonGive) + room:obtainCard(effect.from, room:getPlayerById(effect.to):getEquipment(Card.SubtypeWeapon), true, + fk.ReasonGive) end end } -local collateral = fk.CreateTrickCard{ +local collateral = fk.CreateTrickCard { name = "collateral", suit = Card.Club, number = 12, - skill = collateralSkill, + skill = collateralSkill } -extension:addCards({ - collateral, - collateral:clone(Card.Club, 13), -}) +extension:addCards({ collateral, collateral:clone(Card.Club, 13) }) -local exNihiloSkill = fk.CreateActiveSkill{ +local exNihiloSkill = fk.CreateActiveSkill { name = "ex_nihilo_skill", mod_target_filter = function(self, to_select, selected, user, card, distance_limited) - return true + local player = Fk:currentRoom():getPlayerById(to_select) + local from = Fk:currentRoom():getPlayerById(user) + return not (card and from:isProhibited(player, card)) end, can_use = function(self, player, card) return not player:isProhibited(player, card) @@ -393,68 +349,57 @@ local exNihiloSkill = fk.CreateActiveSkill{ end, on_effect = function(self, room, effect) local target = room:getPlayerById(effect.to) - if target.dead then return end + if target.dead then + return + end target:drawCards(2, "ex_nihilo") end } -local exNihilo = fk.CreateTrickCard{ +local exNihilo = fk.CreateTrickCard { name = "ex_nihilo", suit = Card.Heart, number = 7, - skill = exNihiloSkill, + skill = exNihiloSkill } -extension:addCards({ - exNihilo, - exNihilo:clone(Card.Heart, 8), - exNihilo:clone(Card.Heart, 9), - exNihilo:clone(Card.Heart, 11), -}) +extension:addCards({ exNihilo, exNihilo:clone(Card.Heart, 8), exNihilo:clone(Card.Heart, 9), + exNihilo:clone(Card.Heart, 11) }) -local nullificationSkill = fk.CreateActiveSkill{ +local nullificationSkill = fk.CreateActiveSkill { name = "nullification_skill", can_use = function() return false end, - on_use = function() RoomInstance:delay(1200) end, + on_use = function() + RoomInstance:delay(1200) + end, on_effect = function(self, room, effect) if effect.responseToEvent then effect.responseToEvent.isCancellOut = true end end } -local nullification = fk.CreateTrickCard{ +local nullification = fk.CreateTrickCard { name = "nullification", suit = Card.Spade, number = 11, - skill = nullificationSkill, + skill = nullificationSkill } -extension:addCards({ - nullification, +extension:addCards({ nullification, nullification:clone(Card.Club, 12), nullification:clone(Card.Club, 13), + nullification:clone(Card.Diamond, 12) }) - nullification:clone(Card.Club, 12), - nullification:clone(Card.Club, 13), - - nullification:clone(Card.Diamond, 12), -}) - -local savageAssaultSkill = fk.CreateActiveSkill{ +local savageAssaultSkill = fk.CreateActiveSkill { name = "savage_assault_skill", can_use = Util.AoeCanUse, on_use = Util.AoeOnUse, mod_target_filter = function(self, to_select, selected, user, card, distance_limited) - return user ~= to_select + local player = Fk:currentRoom():getPlayerById(to_select) + local from = Fk:currentRoom():getPlayerById(user) + return user ~= to_select and not (card and from:isProhibited(player, card)) end, on_effect = function(self, room, effect) - local cardResponded = room:askForResponse(room:getPlayerById(effect.to), 'slash', nil, nil, false, nil, effect) - - if cardResponded then - room:responseCard({ - from = effect.to, - card = cardResponded, - responseToEvent = effect, - }) + if room:askForResponse(room:getPlayerById(effect.to), 'slash', nil, nil, false, nil, effect) then else room:damage({ from = room:getPlayerById(effect.from), @@ -462,42 +407,33 @@ local savageAssaultSkill = fk.CreateActiveSkill{ card = effect.card, damage = 1, damageType = fk.NormalDamage, - skillName = self.name, + skillName = self.name }) end end } -local savageAssault = fk.CreateTrickCard{ +local savageAssault = fk.CreateTrickCard { name = "savage_assault", suit = Card.Spade, number = 7, is_damage_card = true, multiple_targets = true, - skill = savageAssaultSkill, + skill = savageAssaultSkill } -extension:addCards({ - savageAssault, - savageAssault:clone(Card.Spade, 13), - savageAssault:clone(Card.Club, 7), -}) +extension:addCards({ savageAssault, savageAssault:clone(Card.Spade, 13), savageAssault:clone(Card.Club, 7) }) -local archeryAttackSkill = fk.CreateActiveSkill{ +local archeryAttackSkill = fk.CreateActiveSkill { name = "archery_attack_skill", can_use = Util.AoeCanUse, on_use = Util.AoeOnUse, mod_target_filter = function(self, to_select, selected, user, card, distance_limited) - return user ~= to_select + local player = Fk:currentRoom():getPlayerById(to_select) + local from = Fk:currentRoom():getPlayerById(user) + return user ~= to_select and not (card and from:isProhibited(player, card)) end, on_effect = function(self, room, effect) - local cardResponded = room:askForResponse(room:getPlayerById(effect.to), 'jink', nil, nil, false, nil, effect) - - if cardResponded then - room:responseCard({ - from = effect.to, - card = cardResponded, - responseToEvent = effect, - }) + if room:askForResponse(room:getPlayerById(effect.to), 'jink', nil, nil, false, nil, effect) then else room:damage({ from = room:getPlayerById(effect.from), @@ -505,30 +441,30 @@ local archeryAttackSkill = fk.CreateActiveSkill{ card = effect.card, damage = 1, damageType = fk.NormalDamage, - skillName = self.name, + skillName = self.name }) end end } -local archeryAttack = fk.CreateTrickCard{ +local archeryAttack = fk.CreateTrickCard { name = "archery_attack", suit = Card.Heart, number = 1, is_damage_card = true, multiple_targets = true, - skill = archeryAttackSkill, + skill = archeryAttackSkill } -extension:addCards({ - archeryAttack, -}) +extension:addCards({ archeryAttack }) -local godSalvationSkill = fk.CreateActiveSkill{ +local godSalvationSkill = fk.CreateActiveSkill { name = "god_salvation_skill", can_use = Util.GlobalCanUse, on_use = Util.GlobalOnUse, mod_target_filter = function(self, to_select, selected, user, card, distance_limited) - return true + local player = Fk:currentRoom():getPlayerById(to_select) + local from = Fk:currentRoom():getPlayerById(user) + return not (card and from:isProhibited(player, card)) end, about_to_effect = function(self, room, effect) if not room:getPlayerById(effect.to):isWounded() then @@ -544,29 +480,29 @@ local godSalvationSkill = fk.CreateActiveSkill{ num = 1, recoverBy = player, card = effect.card, - skillName = self.name, + skillName = self.name }) end end } -local godSalvation = fk.CreateTrickCard{ +local godSalvation = fk.CreateTrickCard { name = "god_salvation", suit = Card.Heart, number = 1, multiple_targets = true, - skill = godSalvationSkill, + skill = godSalvationSkill } -extension:addCards({ - godSalvation, -}) +extension:addCards({ godSalvation }) -local amazingGraceSkill = fk.CreateActiveSkill{ +local amazingGraceSkill = fk.CreateActiveSkill { name = "amazing_grace_skill", can_use = Util.GlobalCanUse, on_use = Util.GlobalOnUse, mod_target_filter = function(self, to_select, selected, user, card, distance_limited) - return true + local player = Fk:currentRoom():getPlayerById(to_select) + local from = Fk:currentRoom():getPlayerById(user) + return not (card and from:isProhibited(player, card)) end, on_effect = function(self, room, effect) local to = room:getPlayerById(effect.to) @@ -581,10 +517,13 @@ local amazingGraceSkill = fk.CreateActiveSkill{ end } -local amazingGraceAction = fk.CreateTriggerSkill{ +local amazingGraceAction = fk.CreateTriggerSkill { name = "amazing_grace_action", global = true, - priority = { [fk.BeforeCardUseEffect] = 0, [fk.CardUseFinished] = 10 }, -- game rule + priority = { + [fk.BeforeCardUseEffect] = 0, + [fk.CardUseFinished] = 10 + }, -- game rule events = { fk.BeforeCardUseEffect, fk.CardUseFinished }, can_trigger = function(self, event, target, player, data) local frameFilled = data.extra_data and data.extra_data.AGFilled @@ -601,7 +540,7 @@ local amazingGraceAction = fk.CreateTriggerSkill{ room:moveCards({ ids = toDisplay, toArea = Card.Processing, - moveReason = fk.ReasonPut, + moveReason = fk.ReasonPut }) table.forEach(room.players, function(p) @@ -624,34 +563,33 @@ local amazingGraceAction = fk.CreateTriggerSkill{ room:moveCards({ ids = toDiscard, toArea = Card.DiscardPile, - moveReason = fk.ReasonPutIntoDiscardPile, + moveReason = fk.ReasonPutIntoDiscardPile }) end end data.extra_data.AGFilled = nil end - end, + end } Fk:addSkill(amazingGraceAction) -local amazingGrace = fk.CreateTrickCard{ +local amazingGrace = fk.CreateTrickCard { name = "amazing_grace", suit = Card.Heart, number = 3, multiple_targets = true, - skill = amazingGraceSkill, + skill = amazingGraceSkill } -extension:addCards({ - amazingGrace, - amazingGrace:clone(Card.Heart, 4), -}) +extension:addCards({ amazingGrace, amazingGrace:clone(Card.Heart, 4) }) -local lightningSkill = fk.CreateActiveSkill{ +local lightningSkill = fk.CreateActiveSkill { name = "lightning_skill", mod_target_filter = function(self, to_select, selected, user, card, distance_limited) - return true + local player = Fk:currentRoom():getPlayerById(to_select) + local from = Fk:currentRoom():getPlayerById(user) + return not (card and from:isProhibited(player, card)) end, can_use = function(self, player, card) return not player:isProhibited(player, card) @@ -666,20 +604,19 @@ local lightningSkill = fk.CreateActiveSkill{ local judge = { who = to, reason = "lightning", - pattern = ".|2~9|spade", + pattern = ".|.|^spade;.|1,10,11,12,13|spade" } room:judge(judge) - local result = judge.card - if result.suit == Card.Spade and result.number >= 2 and result.number <= 9 then - room:damage{ + if judge.card.suit == Card.Spade and judge.card.number >= 2 and judge.card.number <= 9 then + room:damage { to = to, damage = 3, card = effect.card, damageType = fk.ThunderDamage, - skillName = self.name, + skillName = self.name } - room:moveCards{ + room:moveCards { ids = { effect.cardId }, toArea = Card.DiscardPile, moveReason = fk.ReasonUse @@ -693,41 +630,41 @@ local lightningSkill = fk.CreateActiveSkill{ local nextp = to repeat nextp = nextp:getNextAlive(true) - if nextp == to then break end + if nextp == to then + break + end until not nextp:hasDelayedTrick("lightning") and not nextp:isProhibited(nextp, effect.card) - if effect.card:isVirtual() then nextp:addVirtualEquip(effect.card) end - room:moveCards{ + room:moveCards { ids = room:getSubcardsByRule(effect.card, { Card.Processing }), to = nextp.id, toArea = Card.PlayerJudge, moveReason = fk.ReasonPut } - end, + end } -local lightning = fk.CreateDelayedTrickCard{ +local lightning = fk.CreateDelayedTrickCard { name = "lightning", suit = Card.Spade, number = 1, - skill = lightningSkill, + skill = lightningSkill } -extension:addCards({ - lightning, - lightning:clone(Card.Heart, 12), -}) +extension:addCards({ lightning, lightning:clone(Card.Heart, 12) }) -local indulgenceSkill = fk.CreateActiveSkill{ +local indulgenceSkill = fk.CreateActiveSkill { name = "indulgence_skill", mod_target_filter = function(self, to_select, selected, user, card, distance_limited) - return user ~= to_select + local player = Fk:currentRoom():getPlayerById(to_select) + local from = Fk:currentRoom():getPlayerById(user) + return user ~= to_select and not (card and from:isProhibited(player, card)) end, target_filter = function(self, to_select, selected, _, card) - return #selected == 0 and self:modTargetFilter(to_select, selected, Self.id, card, true) + return #selected < 1 and self:modTargetFilter(to_select, selected, Self.id, card, true) end, target_num = 1, on_effect = function(self, room, effect) @@ -735,74 +672,65 @@ local indulgenceSkill = fk.CreateActiveSkill{ local judge = { who = to, reason = "indulgence", - pattern = ".|.|spade,club,diamond", + pattern = ".|.|heart" } room:judge(judge) - local result = judge.card - if result.suit ~= Card.Heart then + if judge.card.suit ~= Card.Heart then to:skip(Player.Play) end self:onNullified(room, effect) end, on_nullified = function(self, room, effect) - room:moveCards{ + room:moveCards { ids = room:getSubcardsByRule(effect.card, { Card.Processing }), toArea = Card.DiscardPile, moveReason = fk.ReasonUse } - end, + end } -local indulgence = fk.CreateDelayedTrickCard{ +local indulgence = fk.CreateDelayedTrickCard { name = "indulgence", suit = Card.Spade, number = 6, - skill = indulgenceSkill, + skill = indulgenceSkill } -extension:addCards({ - indulgence, - indulgence:clone(Card.Club, 6), - indulgence:clone(Card.Heart, 6), -}) +extension:addCards({ indulgence, indulgence:clone(Card.Club, 6), indulgence:clone(Card.Heart, 6) }) -local crossbowAudio = fk.CreateTriggerSkill{ +local crossbowAudio = fk.CreateTriggerSkill { name = "#crossbowAudio", - refresh_events = {fk.CardUsing}, + refresh_events = { fk.CardUsing }, can_refresh = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name) and player.phase == Player.Play and - data.card.trueName == "slash" and player:usedCardTimes("slash", Player.HistoryPhase) > 1 + return target == player and player:hasSkill(self.name) and player.phase == Player.Play and data.card.trueName == + "slash" and player:usedCardTimes("slash", Player.HistoryPhase) > 1 end, on_refresh = function(self, event, target, player, data) local room = player.room room:broadcastPlaySound("./packages/standard_cards/audio/card/crossbow") room:setEmotion(player, "./packages/standard_cards/image/anim/crossbow") - end, + end } -local crossbowSkill = fk.CreateTargetModSkill{ +local crossbowSkill = fk.CreateTargetModSkill { name = "#crossbow_skill", attached_equip = "crossbow", bypass_times = function(self, player, skill, scope) - if player:hasSkill(self.name) and skill.trueName == "slash_skill" - and scope == Player.HistoryPhase then + if player:hasSkill(self.name) and skill.trueName == "slash_skill" and scope == Player.HistoryPhase then return true end - end, + end } crossbowSkill:addRelatedSkill(crossbowAudio) Fk:addSkill(crossbowSkill) -local crossbow = fk.CreateWeapon{ +local crossbow = fk.CreateWeapon { name = "crossbow", suit = Card.Club, number = 1, attack_range = 1, - equip_skill = crossbowSkill, + equip_skill = crossbowSkill } -extension:addCards({ - crossbow, - crossbow:clone(Card.Diamond, 1), -}) +extension:addCards({ crossbow, crossbow:clone(Card.Diamond, 1) }) fk.MarkArmorNullified = "mark__armor_nullified" @@ -821,14 +749,13 @@ local armorInvalidity = fk.CreateInvaliditySkill { } Fk:addSkill(armorInvalidity) -local qingGangSkill = fk.CreateTriggerSkill{ +local qingGangSkill = fk.CreateTriggerSkill { name = "#qinggang_sword_skill", attached_equip = "qinggang_sword", frequency = Skill.Compulsory, events = { fk.TargetSpecified }, can_trigger = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name) and - data.card and data.card.trueName == "slash" + return target == player and player:hasSkill(self.name) and data.card and data.card.trueName == "slash" end, on_use = function(self, event, target, player, data) local room = player.room @@ -836,7 +763,8 @@ local qingGangSkill = fk.CreateTriggerSkill{ data.extra_data = data.extra_data or {} data.extra_data.qinggangNullified = data.extra_data.qinggangNullified or {} - data.extra_data.qinggangNullified[tostring(data.to)] = (data.extra_data.qinggangNullified[tostring(data.to)] or 0) + 1 + data.extra_data.qinggangNullified[tostring(data.to)] = + (data.extra_data.qinggangNullified[tostring(data.to)] or 0) + 1 end, refresh_events = { fk.CardUseFinished }, @@ -853,64 +781,63 @@ local qingGangSkill = fk.CreateTriggerSkill{ end data.qinggangNullified = nil - end, + end } Fk:addSkill(qingGangSkill) -local qingGang = fk.CreateWeapon{ +local qingGang = fk.CreateWeapon { name = "qinggang_sword", suit = Card.Spade, number = 6, attack_range = 2, - equip_skill = qingGangSkill, + equip_skill = qingGangSkill } -extension:addCards({ - qingGang, -}) +extension:addCards({ qingGang }) -local iceSwordSkill = fk.CreateTriggerSkill{ +local iceSwordSkill = fk.CreateTriggerSkill { name = "#ice_sword_skill", attached_equip = "ice_sword", - events = {fk.DamageCaused}, + events = { fk.DamageCaused }, can_trigger = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name) and (not data.chain) and - data.card and data.card.trueName == "slash" and not data.to:isNude() + return + target == player and player:hasSkill(self.name) and (not data.chain) and data.card and data.card.trueName == + "slash" and not data.to:isNude() end, on_use = function(self, event, target, player, data) local room = player.room local to = data.to for i = 1, 2 do - if player.dead or to.dead or to:isNude() then break end + if player.dead or to.dead or to:isNude() then + break + end local card = room:askForCardChosen(player, to, "he", self.name) - room:throwCard({card}, self.name, to, player) + room:throwCard({ card }, self.name, to, player) end return true end } Fk:addSkill(iceSwordSkill) -local iceSword = fk.CreateWeapon{ +local iceSword = fk.CreateWeapon { name = "ice_sword", suit = Card.Spade, number = 2, attack_range = 2, - equip_skill = iceSwordSkill, + equip_skill = iceSwordSkill } -extension:addCards({ - iceSword, -}) +extension:addCards({ iceSword }) -local doubleSwordsSkill = fk.CreateTriggerSkill{ +local doubleSwordsSkill = fk.CreateTriggerSkill { name = "#double_swords_skill", attached_equip = "double_swords", - events = {fk.TargetSpecified}, + events = { fk.TargetSpecified }, can_trigger = function(self, event, target, player, data) - if target == player and player:hasSkill(self.name) and - data.card and data.card.trueName == "slash" then + if target == player and player:hasSkill(self.name) and data.card and data.card.trueName == "slash" then local target = player.room:getPlayerById(data.to) - return target.gender ~= player.gender and target.gender ~= General.Agender and player.gender ~= General.Agender + return target.gender ~= player.gender and target.gender ~= General.Agender and player.gender ~= + General.Agender end end, on_use = function(self, event, target, player, data) @@ -919,66 +846,120 @@ local doubleSwordsSkill = fk.CreateTriggerSkill{ if to:isKongcheng() then player:drawCards(1, self.name) else - local result = room:askForDiscard(to, 1, 1, false, self.name, true, ".", "#double_swords-invoke:"..player.id) + local result = room:askForDiscard(to, 1, 1, false, self.name, true, ".", + "#double_swords-invoke:" .. player.id) if #result == 0 then player:drawCards(1, self.name) end end - end, + end } Fk:addSkill(doubleSwordsSkill) -local doubleSwords = fk.CreateWeapon{ +local doubleSwords = fk.CreateWeapon { name = "double_swords", suit = Card.Spade, number = 2, attack_range = 2, - equip_skill = doubleSwordsSkill, + equip_skill = doubleSwordsSkill } -extension:addCards({ - doubleSwords, -}) +extension:addCards({ doubleSwords }) -local bladeSkill = fk.CreateTriggerSkill{ +local bladeSkill = fk.CreateTriggerSkill { name = "#blade_skill", attached_equip = "blade", - events = {fk.CardEffectCancelledOut}, + events = { fk.CardEffectCancelledOut }, can_trigger = function(self, event, target, player, data) - return player:hasSkill(self.name) and data.from == player.id and data.card.trueName == "slash" and not player.room:getPlayerById(data.to).dead + return player:hasSkill(self.name) and data.from == player.id and data.card.trueName == "slash" and + not player.room:getPlayerById(data.to).dead end, on_cost = function(self, event, target, player, data) - local room = player.room - local use = room:askForUseCard(player, "slash", nil, "#blade_slash:" .. data.to, - true, { must_targets = {data.to}, exclusive_targets = {data.to}, bypass_distances = true, bypass_times = true }) - if use then - use.extraUse = true + local extra_data = { + must_targets = { data.to }, + exclusive_targets = { data.to }, + bypass_distances = true, + bypass_times = true + } + if extra_data then + if extra_data.bypass_distances then + player.room:setPlayerMark(player, MarkEnum.BypassDistancesLimit .. "", 1) + end + if extra_data.bypass_times ~= false then + player.room:setPlayerMark(player, MarkEnum.BypassTimesLimit .. "-tmp", 1) + end + fk.useMustTargets = extra_data.must_targets + end + local command = "AskForUseCard" + player.room:notifyMoveFocus(player, "slash") + local pattern = "slash" + local prompt = "#blade_slash:" .. data.to + + local useData = { + user = player, + cardName = "slash", + pattern = pattern, + extraData = extra_data + } + local use = nil + if self.cost_data == nil then + player.room.logic:trigger(fk.AskForCardUse, player, useData) + if type(useData.result) == "table" then + useData = useData.result + useData.extraUse = extra_data ~= nil + use = useData + end + end + local usedata = { "slash", pattern, prompt, true, extra_data } + if use == nil then + Fk.currentResponsePattern = pattern + local result = player.room:doRequest(player, command, json.encode(usedata)) + Fk.currentResponsePattern = nil + if result ~= "" then + result = player.room:handleUseCardReply(player, result) + result.extraUse = extra_data ~= nil + use = result + end + end + fk.useMustTargets = nil + player.room:setPlayerMark(player, MarkEnum.BypassDistancesLimit .. "-tmp", 0) + player.room:setPlayerMark(player, MarkEnum.BypassTimesLimit .. "-tmp", 0) + if use + then self.cost_data = use return true end end, on_use = function(self, event, target, player, data) - player.room:useCard(self.cost_data) - end, + while true do + local use = self.cost_data + player.room:useCard(use) + if use.breakEvent and self:onCost(event, target, player, data) then + else + break + end + end + self.cost_data = nil + end } Fk:addSkill(bladeSkill) -local blade = fk.CreateWeapon{ +local blade = fk.CreateWeapon { name = "blade", suit = Card.Spade, number = 5, attack_range = 3, - equip_skill = bladeSkill, + equip_skill = bladeSkill } -extension:addCards({ - blade, -}) +extension:addCards({ blade }) -local spearSkill = fk.CreateViewAsSkill{ +local spearSkill = fk.CreateViewAsSkill { name = "spear_skill", attached_equip = "spear", pattern = "slash", card_filter = function(self, to_select, selected) - if #selected == 2 then return false end + if #selected == 2 then + return false + end return Fk:currentRoom():getCardArea(to_select) ~= Player.Equip end, view_as = function(self, cards) @@ -989,37 +970,37 @@ local spearSkill = fk.CreateViewAsSkill{ c.skillName = "spear" c:addSubcards(cards) return c - end, + end } Fk:addSkill(spearSkill) -local spear = fk.CreateWeapon{ +local spear = fk.CreateWeapon { name = "spear", suit = Card.Spade, number = 12, attack_range = 3, - equip_skill = spearSkill, + equip_skill = spearSkill } -extension:addCards({ - spear, -}) +extension:addCards({ spear }) -local axeSkill = fk.CreateTriggerSkill{ +local axeSkill = fk.CreateTriggerSkill { name = "#axe_skill", attached_equip = "axe", - events = {fk.CardEffectCancelledOut}, + events = { fk.CardEffectCancelledOut }, can_trigger = function(self, event, target, player, data) - return player:hasSkill(self.name) and data.from == player.id and data.card.trueName == "slash" and not player.room:getPlayerById(data.to).dead + return player:hasSkill(self.name) and data.from == player.id and data.card.trueName == "slash" and + not player.room:getPlayerById(data.to).dead end, on_cost = function(self, event, target, player, data) local room = player.room local pattern if player:getEquipment(Card.SubtypeWeapon) then - pattern = ".|.|.|.|.|.|^"..tostring(player:getEquipment(Card.SubtypeWeapon)) + pattern = ".|.|.|.|.|.|^" .. tostring(player:getEquipment(Card.SubtypeWeapon)) else pattern = "." end - local cards = room:askForDiscard(player, 2, 2, true, self.name, true, pattern, "#axe-invoke::"..data.to, true) + player.axe_to = data.to + local cards = room:askForDiscard(player, 2, 2, true, self.name, true, pattern, "#axe-invoke::" .. data.to, true) if #cards > 0 then self.cost_data = cards return true @@ -1029,73 +1010,70 @@ local axeSkill = fk.CreateTriggerSkill{ local room = player.room room:throwCard(self.cost_data, "axe", player, player) return true - end, + end } Fk:addSkill(axeSkill) -local axe = fk.CreateWeapon{ +local axe = fk.CreateWeapon { name = "axe", suit = Card.Diamond, number = 5, attack_range = 3, - equip_skill = axeSkill, + equip_skill = axeSkill } -extension:addCards({ - axe, -}) +extension:addCards({ axe }) -local halberdAudio = fk.CreateTriggerSkill{ +local halberdAudio = fk.CreateTriggerSkill { name = "#halberdAudio", - refresh_events = {fk.CardUsing}, + refresh_events = { fk.CardUsing }, can_refresh = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name) and - data.card.trueName == "slash" and #TargetGroup:getRealTargets(data.tos) > 1 + return target == player and player:hasSkill(self.name) and data.card.trueName == "slash" and + #TargetGroup:getRealTargets(data.tos) > 1 end, on_refresh = function(self, event, target, player, data) local room = player.room room:broadcastPlaySound("./packages/standard_cards/audio/card/halberd") room:setEmotion(player, "./packages/standard_cards/image/anim/halberd") - end, + end } -local halberdSkill = fk.CreateTargetModSkill{ +local halberdSkill = fk.CreateTargetModSkill { name = "#halberd_skill", attached_equip = "halberd", extra_target_func = function(self, player, skill, card) - if player:hasSkill(self.name) and skill.trueName == "slash_skill" then - local cards = card:isVirtual() and card.subcards or {card.id} + if player:hasSkill(self.name) and skill.trueName == "slash_skill" and card then + local cards = card:isVirtual() and card.subcards or { card.id } local handcards = player:getCardIds(Player.Hand) - if #cards == #handcards and table.every(cards, function(id) return table.contains(handcards, id) end) then + if #cards == #handcards and table.every(cards, function(id) + return table.contains(handcards, id) + end) then return 2 end end - end, + end } halberdSkill:addRelatedSkill(halberdAudio) Fk:addSkill(halberdSkill) -local halberd = fk.CreateWeapon{ +local halberd = fk.CreateWeapon { name = "halberd", suit = Card.Diamond, number = 12, attack_range = 4, - equip_skill = halberdSkill, + equip_skill = halberdSkill } -extension:addCards({ - halberd, -}) +extension:addCards({ halberd }) -local kylinBowSkill = fk.CreateTriggerSkill{ +local kylinBowSkill = fk.CreateTriggerSkill { name = "#kylin_bow_skill", attached_equip = "kylin_bow", - events = {fk.DamageCaused}, + events = { fk.DamageCaused }, can_trigger = function(self, event, target, player, data) - local ret = target == player and player:hasSkill(self.name) and - data.card and data.card.trueName == "slash" and (not data.chain) + local ret = target == player and player:hasSkill(self.name) and data.card and data.card.trueName == "slash" and + (not data.chain) if ret then ---@type ServerPlayer local to = data.to - return to:getEquipment(Card.SubtypeDefensiveRide) or - to:getEquipment(Card.SubtypeOffensiveRide) + return to:getEquipment(Card.SubtypeDefensiveRide) or to:getEquipment(Card.SubtypeOffensiveRide) end end, on_use = function(self, event, target, player, data) @@ -1108,7 +1086,9 @@ local kylinBowSkill = fk.CreateTriggerSkill{ if to:getEquipment(Card.SubtypeOffensiveRide) then table.insert(ride_tab, "-1") end - if #ride_tab == 0 then return end + if #ride_tab == 0 then + return + end local choice = room:askForChoice(player, ride_tab, self.name) if choice == "+1" then room:throwCard(to:getEquipment(Card.SubtypeDefensiveRide), self.name, to, player) @@ -1118,32 +1098,31 @@ local kylinBowSkill = fk.CreateTriggerSkill{ end } Fk:addSkill(kylinBowSkill) -local kylinBow = fk.CreateWeapon{ +local kylinBow = fk.CreateWeapon { name = "kylin_bow", suit = Card.Heart, number = 5, attack_range = 5, - equip_skill = kylinBowSkill, + equip_skill = kylinBowSkill } -extension:addCards({ - kylinBow, -}) +extension:addCards({ kylinBow }) -local eightDiagramSkill = fk.CreateTriggerSkill{ +local eightDiagramSkill = fk.CreateTriggerSkill { name = "#eight_diagram_skill", attached_equip = "eight_diagram", - events = {fk.AskForCardUse, fk.AskForCardResponse}, + events = { fk.AskForCardUse, fk.AskForCardResponse }, can_trigger = function(self, event, target, player, data) return target == player and player:hasSkill(self.name) and - (data.cardName == "jink" or (data.pattern and Exppattern:Parse(data.pattern):matchExp("jink|0|nosuit|none"))) + (data.cardName == "jink" or + (data.pattern and Exppattern:Parse(data.pattern):matchExp("jink|0|nosuit|none"))) end, on_use = function(self, event, target, player, data) local room = player.room local judgeData = { who = player, reason = self.name, - pattern = ".|.|heart,diamond", + pattern = ".|.|heart,diamond" } room:judge(judgeData) @@ -1151,7 +1130,7 @@ local eightDiagramSkill = fk.CreateTriggerSkill{ if event == fk.AskForCardUse then data.result = { from = player.id, - card = Fk:cloneCard('jink'), + card = Fk:cloneCard('jink') } data.result.card.skillName = "eight_diagram" @@ -1163,49 +1142,44 @@ local eightDiagramSkill = fk.CreateTriggerSkill{ data.result = Fk:cloneCard('jink') data.result.skillName = "eight_diagram" end - return true end end } Fk:addSkill(eightDiagramSkill) -local eightDiagram = fk.CreateArmor{ +local eightDiagram = fk.CreateArmor { name = "eight_diagram", suit = Card.Spade, number = 2, - equip_skill = eightDiagramSkill, + equip_skill = eightDiagramSkill } -extension:addCards({ - eightDiagram, - eightDiagram:clone(Card.Club, 2), -}) +extension:addCards({ eightDiagram, eightDiagram:clone(Card.Club, 2) }) -local niohShieldSkill = fk.CreateTriggerSkill{ +local niohShieldSkill = fk.CreateTriggerSkill { name = "#nioh_shield_skill", attached_equip = "nioh_shield", frequency = Skill.Compulsory, - events = {fk.PreCardEffect}, + events = { fk.PreCardEffect }, can_trigger = function(self, event, target, player, data) - local effect = data ---@type CardEffectEvent - return player.id == effect.to and player:hasSkill(self.name) and - effect.card.trueName == "slash" and effect.card.color == Card.Black + return player.id == data.to and player:hasSkill(self.name) and data.card.trueName == "slash" and + data.card.color == Card.Black end, - on_use = function() return true end, + on_use = function() + return true + end } Fk:addSkill(niohShieldSkill) -local niohShield = fk.CreateArmor{ +local niohShield = fk.CreateArmor { name = "nioh_shield", suit = Card.Club, number = 2, - equip_skill = niohShieldSkill, + equip_skill = niohShieldSkill } -extension:addCards({ - niohShield, -}) +extension:addCards({ niohShield }) -local horseSkill = fk.CreateDistanceSkill{ +local horseSkill = fk.CreateDistanceSkill { name = "horse_skill", global = true, correct_func = function(self, from, to) @@ -1217,71 +1191,59 @@ local horseSkill = fk.CreateDistanceSkill{ ret = ret + 1 end return ret - end, + end } if not Fk.skills["horse_skill"] then Fk:addSkill(horseSkill) end -local diLu = fk.CreateDefensiveRide{ +local diLu = fk.CreateDefensiveRide { name = "dilu", suit = Card.Club, - number = 5, + number = 5 } -extension:addCards({ - diLu, -}) +extension:addCards({ diLu }) -local jueYing = fk.CreateDefensiveRide{ +local jueYing = fk.CreateDefensiveRide { name = "jueying", suit = Card.Spade, - number = 5, + number = 5 } -extension:addCards({ - jueYing, -}) +extension:addCards({ jueYing }) -local zhuaHuangFeiDian = fk.CreateDefensiveRide{ +local zhuaHuangFeiDian = fk.CreateDefensiveRide { name = "zhuahuangfeidian", suit = Card.Heart, - number = 13, + number = 13 } -extension:addCards({ - zhuaHuangFeiDian, -}) +extension:addCards({ zhuaHuangFeiDian }) -local chiTu = fk.CreateOffensiveRide{ +local chiTu = fk.CreateOffensiveRide { name = "chitu", suit = Card.Heart, - number = 5, + number = 5 } -extension:addCards({ - chiTu, -}) +extension:addCards({ chiTu }) -local daYuan = fk.CreateOffensiveRide{ +local daYuan = fk.CreateOffensiveRide { name = "dayuan", suit = Card.Spade, - number = 13, + number = 13 } -extension:addCards({ - daYuan, -}) +extension:addCards({ daYuan }) -local ziXing = fk.CreateOffensiveRide{ +local ziXing = fk.CreateOffensiveRide { name = "zixing", suit = Card.Diamond, - number = 13, + number = 13 } -extension:addCards({ - ziXing, -}) +extension:addCards({ ziXing }) dofile "packages/standard_cards/i18n/init.lua" diff --git a/packages/standard_cards/standard_cards_ai.lua b/packages/standard_cards/standard_cards_ai.lua new file mode 100644 index 00000000..4404f596 --- /dev/null +++ b/packages/standard_cards/standard_cards_ai.lua @@ -0,0 +1,407 @@ +fk.ai_card.slash = { + intention = 100, -- 身份值 + value = 4, -- 卡牌价值 + priority = 2.5 -- 使用优先值 +} +fk.ai_card.peach = { + intention = -150, + value = 10, + priority = 0.5 +} +fk.ai_card.dismantlement = { + intention = function(self, card, from) + if #self.player.player_cards[Player.Judge] < 1 then + return 80 + elseif fk.ai_role[from.id] == "neutral" then + return 30 + end + end, + value = 3.5, + priority = 10.5 +} +fk.ai_card.snatch = { + intention = function(self, card, from) + if #self.player.player_cards[Player.Judge] < 1 then + return 80 + elseif fk.ai_role[from.id] == "neutral" then + return 30 + end + end, + value = 4.5, + priority = 10.4 +} +fk.ai_card.duel = { + intention = 120, + value = 4.5, + priority = 3.5 +} +fk.ai_card.collateral = { + intention = 20, + value = 3, + priority = 4.5 +} +fk.ai_card.ex_nihilo = { + intention = -200, + value = 8, + priority = 10 +} +fk.ai_card.savage_assault = { + intention = 20, + value = 2, + priority = 4 +} +fk.ai_card.archery_attack = { + intention = 30, + value = 2, + priority = 3 +} +fk.ai_card.god_salvation = { + intention = function(self, card, from) + if self.player.hp ~= self.player.maxHp then + return -45 + end + end, + value = 1.5, + priority = 2 +} +fk.ai_card.amazing_grace = { + intention = -30, + value = 2, + priority = 2 +} +fk.ai_card.indulgence = { + intention = 150, + value = -1, + priority = 2 +} + +local function slashEeffect(slash, to) + for _, s in ipairs(to:getAllSkills()) do + if s.name == "#vine_skill" then + if slash.name == "slash" then + return + end + end + if s.name == "#nioh_shield_skill" then + if slash.color == Card.Black then + return + end + end + end + return true +end + +fk.ai_use_play.slash = 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 slashEeffect(card, p) then + self.use_id = card.id + table.insert(self.use_tos, p.id) + end + end +end + +fk.ai_askuse_card["#slash-jink"] = function(self, pattern, prompt, cancelable, extra_data) + local act = self:getActives(pattern) + if tonumber(prompt:split(":")[4]) > #act then + return + end + local cards = + table.map( + self.player:getCardIds("&he"), + function(id) + return Fk:getCardById(id) + end + ) + self:sortValue(cards) + for _, sth in ipairs(act) do + if sth:isInstanceOf(Card) then + self.use_id = sth.id + break + else + local selected = {} + for _, c in ipairs(cards) do + if sth.cardFilter(sth, c.id, selected) then + table.insert(selected, c.id) + end + end + local tc = sth.viewAs(sth, selected) + if tc and tc:matchPattern(pattern) then + self.use_id = + json.encode { + skill = sth.name, + subcards = selected + } + break + end + end + end +end + +fk.ai_askuse_card["#slash-jinks"] = fk.ai_askuse_card["#slash-jink"] + +fk.ai_use_play.snatch = function(self, card) + for _, p in ipairs(self.friends_noself) do + if card.skill:targetFilter(p.id, self.use_tos, {}, card) and #p:getCardIds("j") > 0 then + self.use_id = card.id + 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 #p:getCardIds("he") > 0 then + self.use_id = card.id + table.insert(self.use_tos, p.id) + end + end +end + +fk.ai_nullification.snatch = function(self, card, to, from, positive) + if positive then + if self:isFriend(to) and not self:isFriend(from) and fk.ai_role[from.id] ~= "neutral" 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:isEnemie(to) and self:isEnemie(from) then + if #self.avail_cards > 1 or self:isWeak(to) then + self.use_id = self.avail_cards[1] + end + end + end +end + +fk.ai_use_play.dismantlement = function(self, card) + for _, p in ipairs(self.friends_noself) do + if card.skill:targetFilter(p.id, self.use_tos, {}, card) and #p:getCardIds("j") > 0 then + self.use_id = card.id + 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 #p:getCardIds("he") > 0 then + self.use_id = card.id + table.insert(self.use_tos, p.id) + end + end +end + +fk.ai_nullification.dismantlement = function(self, card, to, from, positive) + if positive then + if self:isFriend(to) and not self:isFriend(from) and fk.ai_role[from.id] ~= "neutral" 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:isEnemie(to) and self:isEnemie(from) then + if #self.avail_cards > 1 or self:isWeak(to) then + self.use_id = self.avail_cards[1] + end + end + end +end + +fk.ai_use_play.indulgence = function(self, card) + self:sort(self.enemies, nil, true) + for _, p in ipairs(self.enemies) do + if card.skill:targetFilter(p.id, self.use_tos, {}, card) then + self.use_id = card.id + table.insert(self.use_tos, p.id) + end + end +end + +fk.ai_nullification.indulgence = 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:isEnemie(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_use_play.collateral = function(self, card) + local max = (card.skill:getMaxTargetNum(self.player, card) - 1) * 2 + self:sort(self.enemies) + for _, p in ipairs(self.enemies) do + if #self.use_tos < max and card.skill:targetFilter(p.id, {}, {}, card) then + for _, pt in ipairs(self.enemies) do + if p ~= pt and p:inMyAttackRange(pt) then + table.insert(self.use_tos, p.id) + table.insert(self.use_tos, pt.id) + self.use_id = card.id + break + end + end + end + end + for _, p in ipairs(self.friends_noself) do + if #self.use_tos < max and card.skill:targetFilter(p.id, {}, {}, card) then + for _, pt in ipairs(self.enemies) do + if p ~= pt and p:inMyAttackRange(pt) then + table.insert(self.use_tos, p.id) + table.insert(self.use_tos, pt.id) + self.use_id = card.id + break + end + end + end + end +end + +fk.ai_nullification.collateral = function(self, card, to, from, positive) + if positive then + if self:isFriend(to) and self:isEnemie(from) 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 + end +end + +fk.ai_nullification.ex_nihilo = function(self, card, to, from, positive) + if positive then + if self:isEnemie(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:isFriend(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_nullification.savage_assault = 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:isEnemie(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_nullification.archery_attack = 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:isEnemie(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_nullification.god_salvation = function(self, card, to, from, positive) + if positive then + if self:isEnemie(to) and to.hp ~= to.maxHp then + if #self.avail_cards > 1 or self:isWeak(to) then + self.use_id = self.avail_cards[1] + end + end + else + if self:isFriend(to) and to.hp ~= to.maxHp 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 + end +end + +fk.ai_use_play.ex_nihilo = function(self, card) + self.use_id = card.id +end + +fk.ai_use_play.lightning = function(self, card) + self.use_id = #self.enemies > #self.friends and card.id +end + +fk.ai_use_play.peach = function(self, card) + if self.command == "PlayCard" then + self.use_id = self.player.hp ~= self.player.maxHp and self.player.hp < #self.player:getCardIds("h") and card.id + else + for _, p in ipairs(self.friends) do + if p.dying then + self.use_id = card.id + self.use_tos = { p.id } + break + end + end + end +end + +fk.ai_use_play.duel = function(self, card) + self:sort(self.enemies) + for _, p in ipairs(self.enemies) do + if card.skill:targetFilter(p.id, self.use_tos, {}, card) then + self.use_id = card.id + table.insert(self.use_tos, p.id) + end + end +end + +fk.ai_skill_invoke["#ice_sword_skill"] = function(self) + local damage = self:eventData("Damage") + return self:isFriend(damage.to) or not self:isWeak(damage.to) and #damage.to:getCardIds("e") > 1 +end + +fk.ai_skill_invoke["#double_swords_skill"] = function(self) + local use = self:eventData("UseCard") + for _, p in ipairs(TargetGroup:getRealTargets(use.tos)) do + if not self:isFriend(p) and self.room:getPlayerById(p).gender ~= self.player.gender then + return true + end + end +end + +fk.ai_dis_card["#double_swords_skill"] = function(self, min_num, num, include_equip, cancelable, pattern, prompt) + local use = self:eventData("UseCard") + return self:isEnemie(use.from) and { self.player:getCardIds("h")[1] } +end + +fk.ai_dis_card["#axe_skill"] = function(self, min_num, num, include_equip, cancelable, pattern, prompt) + local ids = {} + for _, cid in ipairs(self.player:getCardIds("he")) do + if Fk:getCardById(cid):matchPattern(pattern) then + table.insert(ids, cid) + end + if + #ids >= min_num and self:isEnemie(self.player.axe_to) and + (self:isWeak(self.player.axe_to) or #self.player:getCardIds("he") > 3) + then + return ids + end + end +end + +fk.ai_skill_invoke["#kylin_bow_skill"] = function(self) + local damage = self:eventData("Damage") + return not self:isFriend(damage.to) +end + +fk.ai_skill_invoke["#eight_diagram_skill"] = true diff --git a/packages/test/init.lua b/packages/test/init.lua index 95ce37d8..17ab4006 100644 --- a/packages/test/init.lua +++ b/packages/test/init.lua @@ -90,12 +90,6 @@ 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) @@ -130,22 +124,6 @@ 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", From 1f446fa9b8ab859a14bb796c353774a48189e4d0 Mon Sep 17 00:00:00 2001 From: "DESKTOP-C7UTUBQ\\32064" <3206475445@qq.com> Date: Sat, 23 Sep 2023 13:48:09 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lua/server/ai/init.lua | 1 + lua/server/ai/smart_ai.lua | 1083 ++++++++++++++++++++++++++++++++++- lua/server/ai/trust_ai.lua | 1080 +--------------------------------- lua/server/serverplayer.lua | 102 ++-- 4 files changed, 1136 insertions(+), 1130 deletions(-) diff --git a/lua/server/ai/init.lua b/lua/server/ai/init.lua index 8426698b..753fc2d2 100644 --- a/lua/server/ai/init.lua +++ b/lua/server/ai/init.lua @@ -3,3 +3,4 @@ AI = require "server.ai.ai" TrustAI = require "server.ai.trust_ai" RandomAI = require "server.ai.random_ai" +SmartAI = require "server.ai.smart_ai" diff --git a/lua/server/ai/smart_ai.lua b/lua/server/ai/smart_ai.lua index 184d439b..09b1f29b 100644 --- a/lua/server/ai/smart_ai.lua +++ b/lua/server/ai/smart_ai.lua @@ -1,10 +1,1091 @@ -- SPDX-License-Identifier: GPL-3.0-or-later ---@class SmartAI: AI -local SmartAI = AI:subclass("RandomAI") +SmartAI = AI:subclass("SmartAI") + +---@param self SmartAI +---@param skill ActiveSkill|ViewAsSkill|Card +local function usePlaySkill(self, skill) + self.use_id = nil + self.use_tos = {} + Self = self.player + self.special_skill = nil + if skill:isInstanceOf(Card) then + local uc = fk.ai_use_play[skill.name] + if type(uc) == "function" then + uc(self, skill) + end + if self.use_id == nil then + if type(skill.special_skills) == "table" then + for _, sn in ipairs(skill.special_skills) do + uc = fk.ai_use_play[sn] + if type(uc) == "function" then + uc(self, skill) + if self.use_id then + break + end + end + end + end + if skill.type == 3 then + if self.player:getEquipment(skill.sub_type) or #self.player:getCardIds("h") <= self.player.hp then + return "" + end + self.use_id = skill.id + elseif skill.is_damage_card and skill.multiple_targets then + if #self.enemies < #self.friends_noself then + return "" + end + self.use_id = skill.id + end + end + elseif skill:isInstanceOf(ViewAsSkill) then + local selected = {} + local cards = + table.map( + self.player:getCardIds("&he"), + function(id) + return Fk:getCardById(id) + end + ) + self:sortValue(cards) + for _, c in ipairs(cards) do + if skill:cardFilter(c.id, selected) then + table.insert(selected, c.id) + end + end + local tc = skill:viewAs(selected) + if tc then + local uc = fk.ai_use_play[tc.name] + if type(uc) == "function" then + uc(self, tc) + if self.use_id then + self.use_id = selected + end + end + end + else + local uc = fk.ai_use_play[skill.name] + if type(uc) == "function" then + uc(self, skill) + end + end + if self.use_id then + if not skill:isInstanceOf(Card) then + self.use_id = + json.encode { + skill = skill.name, + subcards = self.use_id + } + end + return json.encode { + card = self.use_id, + targets = self.use_tos, + special_skill = self.special_skill + } + end + return "" +end + +fk.ai_use_play = {} + +local trust_cb = {} + +trust_cb.AskForUseActiveSkill = function(self, jsonData) + local data = json.decode(jsonData) + local skill = Fk.skills[data[1]] + local prompt = data[2] + local cancelable = data[3] + self:updatePlayers() + local extra_data = json.decode(data[4]) + for k, v in pairs(extra_data) do + skill[k] = v + end + self.use_id = nil + self.use_tos = {} + local ask = fk.ai_use_skill[data[1]] + if type(ask) == "function" then + ask(self, prompt, cancelable, extra_data) + end + if self.use_id then + return json.encode { + card = self.use_id, + targets = self.use_tos + } + end + return "" +end + +fk.ai_use_skill = {} + +fk.ai_use_skill.choose_players_skill = function(self, prompt, cancelable, data) + local ask = fk.ai_choose_players[data.skillName] + if type(ask) == "function" then + ask(self, data.targets, data.min_num, data.num, cancelable) + end + if #self.use_tos > 0 then + if self.use_id then + self.use_id = + json.encode { + skill = data.skillName, + subcards = self.use_id + } + else + self.use_id = + json.encode { + skill = data.skillName, + subcards = {} + } + end + end +end + +fk.ai_choose_players = {} + +fk.ai_use_skill.discard_skill = function(self, prompt, cancelable, data) + local ask = fk.ai_dis_card[data.skillName] + self:assignValue() + if type(ask) == "function" then + ask = ask(self, data.min_num, data.num, data.include_equip, cancelable, data.pattern, prompt) + end + if type(ask) ~= "table" and not cancelable then + local flag = "h" + if data.include_equip then + flag = "he" + end + ask = {} + local cards = + table.map( + self.player:getCardIds(flag), + function(id) + return Fk:getCardById(id) + end + ) + self:sortValue(cards) + for _, c in ipairs(cards) do + table.insert(ask, c.id) + if #ask >= data.min_num then + break + end + end + end + if type(ask) == "table" and #ask >= data.min_num then + self.use_id = + json.encode { + skill = data.skillName, + subcards = ask + } + end +end + +fk.ai_dis_card = {} + +trust_cb.AskForSkillInvoke = function(self, jsonData) + local data = json.decode(jsonData) + local prompt = data[2] + local extra_data = data[3] + local ask = fk.ai_skill_invoke[data[1]] + self:updatePlayers() + if type(ask) == "function" then + return ask(self, extra_data, prompt) and "1" or "" + elseif type(ask) == "boolean" then + return ask and "1" or "" + elseif Fk.skills[data[1]].frequency == 1 then + return "1" + else + return table.random { "1", "" } + end +end + +fk.ai_skill_invoke = {} + +trust_cb.AskForUseCard = function(self, jsonData) + local data = json.decode(jsonData) + local pattern = data[2] + local prompt = data[3] + local cancelable = data[4] + local extra_data = data[5] + self:updatePlayers() + self.use_id = nil + self.use_tos = {} + local exp = Exppattern:Parse(data[2] or data[1]) + self.avail_cards = + table.filter( + self.player:getCardIds("&he"), + function(id) + return exp:match(Fk:getCardById(id)) and not self.player:prohibitUse(Fk:getCardById(id)) + end + ) + Self = self.player + local ask = fk.ai_askuse_card[prompt:split(":")[1]] + if type(ask) == "function" then + ask(self, pattern, prompt, cancelable, extra_data) + else + local cards = + table.map( + self.player:getCardIds("&he"), + function(id) + return Fk:getCardById(id) + end + ) + self:sortValue(cards) + for _, sth in ipairs(self:getActives(pattern)) do + if sth:isInstanceOf(Card) then + if sth.skill:canUse(self.player, sth) and not self.player:prohibitUse(sth) then + local ret = usePlaySkill(self, sth) + if ret ~= "" then + return ret + end + 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(pattern) then + local uc = fk.ai_use_play[tc.name] + if type(uc) == "function" then + uc(self, tc) + if self.use_id then + self.use_id = + json.encode { + skill = sth.name, + subcards = selected + } + break + end + end + end + end + end + end + ask = fk.ai_askuse_card[data[1]] + if self.use_id == nil and type(ask) == "function" then + ask(self, pattern, prompt, cancelable, extra_data) + end + if self.use_id == true then + self.use_id = self.avail_cards[1] + end + if self.use_id then + return json.encode { + card = self.use_id, + targets = self.use_tos + } + end + return "" +end + +fk.ai_askuse_card = {} +fk.ai_nullification = {} + +fk.ai_askuse_card.nullification = function(self, pattern, prompt, cancelable, extra_data) + local effect = self:eventData("CardEffect") + if effect.to then + fk.askNullificationData = effect + fk.askNullification = 1 + elseif effect.from then + fk.askNullification = fk.askNullification + 1 + end + effect = fk.askNullificationData + local positive = fk.askNullification % 2 == 1 + local ask = fk.ai_nullification[effect.card.name] + if type(ask) == "function" then + ask(self, effect.card, self.room:getPlayerById(effect.to), self.room:getPlayerById(effect.from), positive) + end +end + +fk.ai_askuse_card["#AskForPeaches"] = function(self, pattern, prompt, cancelable, extra_data) + local dying = self:eventData("Dying") + local who = self.room:getPlayerById(dying.who) + if who and self:isFriend(who) then + local cards = + table.map( + self.player:getCardIds("&he"), + function(id) + return Fk:getCardById(id) + end + ) + self:sortValue(cards) + for _, sth in ipairs(self:getActives(pattern)) do + if sth:isInstanceOf(Card) then + self.use_id = sth.id + break + else + local selected = {} + for _, c in ipairs(cards) do + if sth.cardFilter(sth, c.id, selected) then + table.insert(selected, c.id) + end + end + local tc = sth.viewAs(sth, selected) + if tc and tc:matchPattern(pattern) then + self.use_id = + json.encode { + skill = sth.name, + subcards = selected + } + break + end + end + end + end +end + +fk.ai_askuse_card["#AskForPeachesSelf"] = fk.ai_askuse_card["#AskForPeaches"] + +fk.ai_card = {} +fk.cardValue = {} + +function SmartAI:assignValue(assign) + assign = assign or { "slash", "peach", "jink", "nullification" } + for v, p in ipairs(assign) do + local kept = {} + v = fk.ai_card[p] + v = v and v.value or 3 + for _, sth in ipairs(self:getActives(p)) do + if sth:isInstanceOf(Card) then + fk.cardValue[sth.id] = self:getValue(sth, kept) + else + fk.cardValue[sth.name] = self:getValue(sth, kept) + v + end + table.insert(kept, sth) + end + self.keptCv = nil + end +end + +function SmartAI:getValue(card, kept) + local v = fk.ai_card[card.name] + v = v and v.value or 0 + if kept then + if card:isInstanceOf(Card) then + if self.keptCv == nil then + self.keptCv = v + end + return v - #kept * 0.25 + else + return (self.keptCv or v) - #kept * 0.25 + end + elseif card:isInstanceOf(Card) then + return fk.cardValue[card.id] or v + else + return fk.cardValue[card.name] or v + end + return v +end + +function SmartAI:getPriority(card) + local v = card and fk.ai_card[card.name] + v = v and v.priority or 0 + if card:isInstanceOf(Card) then + if card:isInstanceOf(Armor) then + v = v + 7 + elseif card:isInstanceOf(Weapon) then + v = v + 3 + elseif card:isInstanceOf(OffensiveRide) then + v = v + 6 + elseif card:isInstanceOf(DefensiveRide) then + v = v + 4 + end + v = v + (13 - card.number) / 100 + v = v + card.suit / 100 + end + return v +end + +fk.compareFunc = { + hp = function(p) + return p.hp + end, + maxHp = function(p) + return p.maxHp + end, + hand = function(p) + return #p:getHandlyIds(true) + end, + equip = function(p) + return #p:getCardIds("e") + end, + maxcards = function(p) + return p.hp + end, + skill = function(p) + return #p:getAllSkills() + end, + defense = function(p) + return p.hp + #p:getHandlyIds(true) + end +} + +function SmartAI:sort(players, key, inverse) + key = key or "defense" + local func = fk.compareFunc[key] + if func == nil then + func = fk.compareFunc.defense + end + local function compare_func(a, b) + return func(a) < func(b) + end + table.sort(players, compare_func) + if inverse then + players = table.reverse(players) + end +end + +function SmartAI:sortValue(cards, inverse) + local function compare_func(a, b) + return self:getValue(a) < self:getValue(b) + end + table.sort(cards, compare_func) + if inverse then + cards = table.reverse(cards) + end +end + +function SmartAI:sortPriority(cards, inverse) + local function compare_func(a, b) + local va = a and self:getPriority(a) or 0 + local vb = b and self:getPriority(b) or 0 + if va == vb then + va = a and self:getValue(a) or 0 + vb = b and self:getValue(b) or 0 + end + return va > vb + end + table.sort(cards, compare_func) + if inverse then + cards = table.reverse(cards) + end +end + +---@param self SmartAI +trust_cb.AskForResponseCard = function(self, jsonData) + local data = json.decode(jsonData) + local pattern = data[2] + local prompt = data[3] + local cancelable = data[4] + local extra_data = data[5] + self:updatePlayers() + self.use_id = nil + local ask = fk.ai_response_card[prompt:split(":")[1]] + if type(ask) == "function" then + ask(self, pattern, prompt, cancelable, extra_data) + else + ask = fk.ai_response_card[data[1]] + if type(ask) == "function" then + ask(self, pattern, prompt, cancelable, extra_data) + end + local effect = self:eventData("CardEffect") + if effect and effect.card then + self:setUseId(pattern) + end + end + if self.use_id then + return json.encode { + card = self.use_id, + targets = {} + } + end + return "" +end + +fk.ai_response_card = {} + +function SmartAI:getRetrialCardId(cards, exchange) + local judge = self:eventData("Judge") + local isgood = judge.card:matchPattern(judge.pattern) + local canRetrial = {} + self:sortValue(cards) + if exchange then + for _, c in ipairs(cards) do + if c:matchPattern(judge.pattern) == isgood then + table.insert(canRetrial, c) + end + end + else + if isgood then + if self:isFriend(judge.who) then + return + end + elseif self:isEnemie(judge.who) then + return + end + end + for _, c in ipairs(cards) do + if + self:isFriend(judge.who) and c:matchPattern(judge.pattern) or + self:isEnemie(judge.who) and not c:matchPattern(judge.pattern) + then + table.insert(canRetrial, c) + end + end + if #canRetrial > 0 then + return canRetrial[1].id + end +end + +function SmartAI:getActives(pattern) + local cards = + table.map( + self.player:getCardIds("&he"), + function(id) + return Fk:getCardById(id) + end + ) + local exp = Exppattern:Parse(pattern) + cards = + table.filter( + cards, + function(c) + return exp:match(c) + end + ) + table.insertTable( + cards, + table.filter( + self.player:getAllSkills(), + function(s) + return s:isInstanceOf(ViewAsSkill) and s:enabledAtResponse(self.player, pattern) + end + ) + ) + self:sortPriority(cards) + return cards +end + +function SmartAI:setUseId(pattern) + local cards = + table.map( + self.player:getCardIds("&he"), + function(id) + return Fk:getCardById(id) + end + ) + self:sortValue(cards) + for _, sth in ipairs(self:getActives(pattern)) do + if sth:isInstanceOf(Card) then + self.use_id = sth.id + break + 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(pattern) then + self.use_id = + json.encode { + skill = sth.name, + subcards = selected + } + break + end + end + end +end + +function SmartAI:cardsView(pattern) + local actives = + table.filter( + self.player:getAllSkills(), + function(s) + return s:isInstanceOf(ViewAsSkill) and s:enabledAtResponse(self.player, pattern) + end + ) + return actives +end + +---@param self SmartAI +trust_cb.PlayCard = function(self, jsonData) + local cards = + table.map( + self.player:getHandlyIds(true), + function(id) + return Fk:getCardById(id) + end + ) + cards = + table.filter( + cards, + function(c) + return c.skill:canUse(self.player, c) and not self.player:prohibitUse(c) + end + ) + table.insertTable( + cards, + table.filter( + self.player:getAllSkills(), + function(s) + return s:isInstanceOf(ActiveSkill) and s:canUse(self.player) or + s:isInstanceOf(ViewAsSkill) and s:enabledAtPlay(self.player) + end + ) + ) + if #cards < 1 then + return + end + self:updatePlayers() + self:sortPriority(cards) + for _, sth in ipairs(cards) do + local ret = usePlaySkill(self, sth) + if ret ~= "" then + return ret + end + end + return "" +end + +fk.ai_card_chosen = {} + +trust_cb.AskForCardChosen = function(self, jsonData) + local data = json.decode(jsonData) + local to = self.room:getPlayerById(data[1]) + local chosen = fk.ai_card_chosen[data[3]] + if type(chosen) == "function" then + return chosen(self, to, data[2]) + elseif table.contains(self.friends, to) then + if string.find(data[2], "j") then + local jc = to:getCardIds("j") + if #jc > 0 then + return table.random(jc) + end + end + else + if string.find(data[2], "h") then + local hc = to:getCardIds("h") + if #hc == 1 then + return hc[1] + end + end + if string.find(data[2], "e") then + local ec = to:getCardIds("e") + if #ec > 0 then + return table.random(ec) + end + for c, id in ipairs(to:getCardIds("e")) do + --c = Fk:getCardById(id) + return id + end + end + if string.find(data[2], "h") then + local hc = to:getCardIds("h") + if #hc > 0 then + return table.random(hc) + end + end + end + return "" +end + +fk.ai_role = {} +fk.roleValue = {} + +fk.trick_judge = {} + +fk.trick_judge.indulgence = ".|.|heart" +fk.trick_judge.lightning = ".|.|^spade" +fk.trick_judge.supply_shortage = ".|.|club" + +local function table_clone(self) + local t = {} + for _, r in ipairs(self) do + table.insert(t, r) + end + return t +end + +trust_cb.AskForGuanxing = function(self, jsonData) + local data = json.decode(jsonData) + local cards = + table.map( + data.cards, + function(id) + return Fk:getCardById(id) + end + ) + self:sortValue(cards) + local top = {} + if self.room.current.phase < Player.Play then + local jt = + table.map( + self.room.current:getCardIds("j"), + function(id) + return Fk:getCardById(id) + end + ) + if #jt > 0 then + for i, j in ipairs(table.reverse(jt)) do + local tj = fk.trick_judge[j.name] + if tj then + for _, c in ipairs(table_clone(cards)) do + if c:matchPattern(tj) and #top < data.max_top_cards then + table.insert(top, c.id) + table.removeOne(cards, c) + tj = 1 + break + end + end + end + if tj ~= 1 and #cards > 0 and #top < data.max_top_cards then + table.insert(top, cards[1].id) + table.remove(cards, 1) + end + end + end + self:sortValue(cards, true) + for _, c in ipairs(table_clone(cards)) do + if #top < data.max_top_cards and c.skill:canUse(self.player, c) and usePlaySkill(self, c) ~= "" then + table.insert(top, c.id) + table.removeOne(cards, c) + break + end + end + end + for _, c in ipairs(table_clone(cards)) do + if #top < data.min_top_cards then + table.insert(top, c.id) + table.removeOne(cards, c) + break + end + end + return json.encode { + top, + table.map( + cards, + function(c) + return c.id + end + ) + } +end function SmartAI:initialize(player) AI.initialize(self, player) + self.cb_table = trust_cb + self.player = player + self.room = RoomInstance or ClientInstance + + fk.ai_role[player.id] = "neutral" + fk.roleValue[player.id] = { + lord = 0, + loyalist = 0, + rebel = 0, + renegade = 0 + } + self:updatePlayers() +end + +function SmartAI:isRolePredictable() + return self.room.settings.gameMode ~= "aaa_role_mode" +end + +local function aliveRoles(room) + fk.alive_roles = { + lord = 0, + loyalist = 0, + rebel = 0, + renegade = 0 + } + for _, ap in ipairs(room:getAllPlayers(false)) do + fk.alive_roles[ap.role] = 0 + end + for _, ap in ipairs(room:getAlivePlayers(false)) do + fk.alive_roles[ap.role] = fk.alive_roles[ap.role] + 1 + end + return fk.alive_roles +end + +function SmartAI:objectiveLevel(to) + if self.player.id == to.id then + return -2 + elseif #self.room:getAlivePlayers(false) < 3 then + return 5 + end + local ars = aliveRoles(self.room) + if self:isRolePredictable() then + fk.ai_role[self.player.id] = self.role + fk.roleValue[self.player.id][self.role] = 666 + if self.role == "renegade" then + fk.explicit_renegade = true + end + for _, p in ipairs(self.room:getAlivePlayers()) do + if + p.role == self.role or p.role == "lord" and self.role == "loyalist" or + p.role == "loyalist" and self.role == "lord" + then + table.insert(self.friends, p) + if p.id ~= self.player.id then + table.insert(self.friends_noself, p) + end + else + table.insert(self.enemies, p) + end + end + elseif self.role == "renegade" then + if to.role == "lord" then + return -1 + elseif ars.rebel < 1 then + return 4 + elseif fk.ai_role[to.id] == "loyalist" then + return ars.lord + ars.loyalist - ars.rebel + elseif fk.ai_role[to.id] == "rebel" then + local r = ars.rebel - ars.lord + ars.loyalist + if r >= 0 then + return 3 + else + return r + end + end + elseif self.role == "lord" or self.role == "loyalist" then + if fk.ai_role[to.id] == "rebel" then + return 5 + elseif to.role == "lord" then + return -2 + elseif ars.rebel < 1 then + if self.role == "lord" then + return fk.explicit_renegade and fk.ai_role[to.id] == "renegade" and 4 or to.hp > 1 and 2 or 0 + elseif fk.explicit_renegade then + return fk.ai_role[to.id] == "renegade" and 4 or -1 + else + return 3 + end + elseif fk.ai_role[to.id] == "loyalist" then + return -2 + elseif fk.ai_role[to.id] == "renegade" then + local r = ars.lord + ars.loyalist - ars.rebel + if r <= 0 then + return r + else + return 3 + end + end + elseif self.role == "rebel" then + if to.role == "lord" then + return 5 + elseif fk.ai_role[to.id] == "loyalist" then + return 4 + elseif fk.ai_role[to.id] == "rebel" then + return -2 + elseif fk.ai_role[to.id] == "renegade" then + local r = ars.rebel - ars.lord + ars.loyalist + if r > 0 then + return 1 + else + return r + end + end + end + return 0 +end + +function SmartAI:updatePlayers(update) + self.role = self.player.role + local neutrality = {} + self.enemies = {} + self.friends = {} + self.friends_noself = {} + + local aps = self.room:getAlivePlayers() + local function compare_func(a, b) + local v1 = fk.roleValue[a.id].rebel + local v2 = fk.roleValue[b.id].rebel + if v1 == v2 then + v1 = fk.roleValue[a.id].renegade + v2 = fk.roleValue[b.id].renegade + end + return v1 > v2 + end + table.sort(aps, compare_func) + fk.explicit_renegade = false + local ars = aliveRoles(self.room) + local rebel, renegade, loyalist = 0, 0, 0 + for _, ap in ipairs(aps) do + if ap.role == "lord" then + fk.ai_role[ap.id] = "loyalist" + elseif fk.roleValue[ap.id].rebel > 50 and ars.rebel > rebel then + rebel = rebel + 1 + fk.ai_role[ap.id] = "rebel" + elseif fk.roleValue[ap.id].renegade > 50 and ars.renegade > renegade then + renegade = renegade + 1 + fk.ai_role[ap.id] = "renegade" + fk.explicit_renegade = fk.roleValue[ap.id].renegade > 100 + elseif fk.roleValue[ap.id].rebel < -50 and ars.loyalist > loyalist then + loyalist = loyalist + 1 + fk.ai_role[ap.id] = "loyalist" + else + fk.ai_role[ap.id] = "neutral" + end + end + + for n, p in ipairs(self.room:getAlivePlayers(false)) do + n = self:objectiveLevel(p) + if n < 0 then + table.insert(self.friends, p) + if p.id ~= self.player.id then + table.insert(self.friends_noself, p) + end + elseif n > 0 then + table.insert(self.enemies, p) + else + table.insert(neutrality, p) + end + end + self:assignValue() + --[[ + if self.enemies<1 and #neutrality>0 + and#self.toUse<3 and self:getOverflow()>0 + then + function compare_func(a,b) + return sgs.getDefense(a) 0 or fk.roleValue[player.id].rebel > 0 and intention < 0 then + fk.roleValue[player.id].renegade = fk.roleValue[player.id].renegade + + intention * (100 - fk.roleValue[player.id].renegade) / 200 + end + local aps = player.room:getAlivePlayers() + local function compare_func(a, b) + local v1 = fk.roleValue[a.id].rebel + local v2 = fk.roleValue[b.id].rebel + if v1 == v2 then + v1 = fk.roleValue[a.id].renegade + v2 = fk.roleValue[b.id].renegade + end + return v1 > v2 + end + table.sort(aps, compare_func) + fk.explicit_renegade = false + local ars = aliveRoles(player.room) + local rebel, renegade, loyalist = 0, 0, 0 + for _, ap in ipairs(aps) do + if ap.role == "lord" then + fk.ai_role[ap.id] = "loyalist" + elseif fk.roleValue[ap.id].rebel > 50 and ars.rebel > rebel then + rebel = rebel + 1 + fk.ai_role[ap.id] = "rebel" + elseif fk.roleValue[ap.id].renegade > 50 and ars.renegade > renegade then + renegade = renegade + 1 + fk.ai_role[ap.id] = "renegade" + fk.explicit_renegade = fk.roleValue[ap.id].renegade > 100 + elseif fk.roleValue[ap.id].rebel < -50 and ars.loyalist > loyalist then + loyalist = loyalist + 1 + fk.ai_role[ap.id] = "loyalist" + else + fk.ai_role[ap.id] = "neutral" + end + end + fk.qWarning( + player.general .. + " " .. + intention .. + " " .. + fk.ai_role[player.id] .. + " rebelValue:" .. fk.roleValue[player.id].rebel .. " renegadeValue:" .. fk.roleValue[player.id].renegade + ) --]] + end +end + +function SmartAI:filterEvent(event, player, data) + if event == fk.TargetSpecified then + local callback = fk.ai_card[data.card.name] + callback = callback and callback.intention + if type(callback) == "function" then + for _, p in ipairs(TargetGroup:getRealTargets(data.tos)) do + p = self.room:getPlayerById(p) + local intention = callback(p.ai, data.card, self.room:getPlayerById(data.from)) + if type(intention) == "number" then + updateIntention(self.room:getPlayerById(data.from), p, intention) + end + end + elseif type(callback) == "number" then + for _, p in ipairs(TargetGroup:getRealTargets(data.tos)) do + p = self.room:getPlayerById(p) + updateIntention(self.room:getPlayerById(data.from), p, callback) + end + end + elseif event == fk.StartJudge then + fk.trick_judge[data.reason] = data.pattern + elseif event == fk.AfterCardsMove then + end +end + +function SmartAI:isWeak(player, getAP) + player = player or self.player + if type(player) == "number" then + player = self.room:getPlayerById(player) + end + return player.hp < 2 or player.hp <= 2 and #player:getCardIds("&h") <= 2 +end + +function SmartAI:isFriend(pid, tid) + if tid then + local bt = self:isFriend(pid) + return bt ~= nil and bt == self:isFriend(tid) + end + if type(pid) == "number" then + pid = self.room:getPlayerById(pid) + end + local ve = self:objectiveLevel(pid) + if ve < 0 then + return true + elseif ve > 0 then + return false + end +end + +function SmartAI:isEnemie(pid, tid) + if tid then + local bt = self:isFriend(pid) + return bt ~= nil and bt ~= self:isFriend(tid) + end + if type(pid) == "number" then + pid = self.room:getPlayerById(pid) + end + local ve = self:objectiveLevel(pid) + if ve > 0 then + return true + elseif ve < 0 then + return false + end +end + +function SmartAI:eventData(game_event) + local event = self.room.logic:getCurrentEvent():findParent(GameEvent[game_event], true) + return event and event.data[1] +end + +for _, n in ipairs(FileIO.ls("packages")) do + if FileIO.isDir("packages/" .. n) and FileIO.exists("packages/" .. n .. "/" .. n .. "_ai.lua") then + dofile("packages/" .. n .. "/" .. n .. "_ai.lua") + end +end +-- 加载两次拓展是为了能够引用,例如属性杀的使用直接套入普通杀的使用 +for _, n in ipairs(FileIO.ls("packages")) do + if FileIO.isDir("packages/" .. n) and FileIO.exists("packages/" .. n .. "/" .. n .. "_ai.lua") then + dofile("packages/" .. n .. "/" .. n .. "_ai.lua") + end end return SmartAI diff --git a/lua/server/ai/trust_ai.lua b/lua/server/ai/trust_ai.lua index 72ffbb00..153eca26 100644 --- a/lua/server/ai/trust_ai.lua +++ b/lua/server/ai/trust_ai.lua @@ -1,1091 +1,13 @@ -- SPDX-License-Identifier: GPL-3.0-or-later -- Trust AI ---@class TrustAI: AI -TrustAI = AI:subclass("TrustAI") - ----@param self TrustAI ----@param skill ActiveSkill|ViewAsSkill|Card -local function usePlaySkill(self, skill) - self.use_id = nil - self.use_tos = {} - Self = self.player - self.special_skill = nil - if skill:isInstanceOf(Card) then - local uc = fk.ai_use_play[skill.name] - if type(uc) == "function" then - uc(self, skill) - end - if self.use_id == nil then - if type(skill.special_skills) == "table" then - for _, sn in ipairs(skill.special_skills) do - uc = fk.ai_use_play[sn] - if type(uc) == "function" then - uc(self, skill) - if self.use_id then - break - end - end - end - end - if skill.type == 3 then - if self.player:getEquipment(skill.sub_type) or #self.player:getCardIds("h") <= self.player.hp then - return "" - end - self.use_id = skill.id - elseif skill.is_damage_card and skill.multiple_targets then - if #self.enemies < #self.friends_noself then - return "" - end - self.use_id = skill.id - end - end - elseif skill:isInstanceOf(ViewAsSkill) then - local selected = {} - local cards = - table.map( - self.player:getCardIds("&he"), - function(id) - return Fk:getCardById(id) - end - ) - self:sortValue(cards) - for _, c in ipairs(cards) do - if skill:cardFilter(c.id, selected) then - table.insert(selected, c.id) - end - end - local tc = skill:viewAs(selected) - if tc then - local uc = fk.ai_use_play[tc.name] - if type(uc) == "function" then - uc(self, tc) - if self.use_id then - self.use_id = selected - end - end - end - else - local uc = fk.ai_use_play[skill.name] - if type(uc) == "function" then - uc(self, skill) - end - end - if self.use_id then - if not skill:isInstanceOf(Card) then - self.use_id = - json.encode { - skill = skill.name, - subcards = self.use_id - } - end - return json.encode { - card = self.use_id, - targets = self.use_tos, - special_skill = self.special_skill - } - end - return "" -end - -fk.ai_use_play = {} +local TrustAI = AI:subclass("TrustAI") local trust_cb = {} -trust_cb.AskForUseActiveSkill = function(self, jsonData) - local data = json.decode(jsonData) - local skill = Fk.skills[data[1]] - local prompt = data[2] - local cancelable = data[3] - self:updatePlayers() - local extra_data = json.decode(data[4]) - for k, v in pairs(extra_data) do - skill[k] = v - end - self.use_id = nil - self.use_tos = {} - local ask = fk.ai_use_skill[data[1]] - if type(ask) == "function" then - ask(self, prompt, cancelable, extra_data) - end - if self.use_id then - return json.encode { - card = self.use_id, - targets = self.use_tos - } - end - return "" -end - -fk.ai_use_skill = {} - -fk.ai_use_skill.choose_players_skill = function(self, prompt, cancelable, data) - local ask = fk.ai_choose_players[data.skillName] - if type(ask) == "function" then - ask(self, data.targets, data.min_num, data.num, cancelable) - end - if #self.use_tos > 0 then - if self.use_id then - self.use_id = - json.encode { - skill = data.skillName, - subcards = self.use_id - } - else - self.use_id = - json.encode { - skill = data.skillName, - subcards = {} - } - end - end -end - -fk.ai_choose_players = {} - -fk.ai_use_skill.discard_skill = function(self, prompt, cancelable, data) - local ask = fk.ai_dis_card[data.skillName] - self:assignValue() - if type(ask) == "function" then - ask = ask(self, data.min_num, data.num, data.include_equip, cancelable, data.pattern, prompt) - end - if type(ask) ~= "table" and not cancelable then - local flag = "h" - if data.include_equip then - flag = "he" - end - ask = {} - local cards = - table.map( - self.player:getCardIds(flag), - function(id) - return Fk:getCardById(id) - end - ) - self:sortValue(cards) - for _, c in ipairs(cards) do - table.insert(ask, c.id) - if #ask >= data.min_num then - break - end - end - end - if type(ask) == "table" and #ask >= data.min_num then - self.use_id = - json.encode { - skill = data.skillName, - subcards = ask - } - end -end - -fk.ai_dis_card = {} - -trust_cb.AskForSkillInvoke = function(self, jsonData) - local data = json.decode(jsonData) - local prompt = data[2] - local extra_data = data[3] - local ask = fk.ai_skill_invoke[data[1]] - self:updatePlayers() - if type(ask) == "function" then - return ask(self, extra_data, prompt) and "1" or "" - elseif type(ask) == "boolean" then - return ask and "1" or "" - elseif Fk.skills[data[1]].frequency == 1 then - return "1" - else - return table.random { "1", "" } - end -end - -fk.ai_skill_invoke = {} - -trust_cb.AskForUseCard = function(self, jsonData) - local data = json.decode(jsonData) - local pattern = data[2] - local prompt = data[3] - local cancelable = data[4] - local extra_data = data[5] - self:updatePlayers() - self.use_id = nil - self.use_tos = {} - local exp = Exppattern:Parse(data[2] or data[1]) - self.avail_cards = - table.filter( - self.player:getCardIds("&he"), - function(id) - return exp:match(Fk:getCardById(id)) and not self.player:prohibitUse(Fk:getCardById(id)) - end - ) - Self = self.player - local ask = fk.ai_askuse_card[prompt:split(":")[1]] - if type(ask) == "function" then - ask(self, pattern, prompt, cancelable, extra_data) - else - local cards = - table.map( - self.player:getCardIds("&he"), - function(id) - return Fk:getCardById(id) - end - ) - self:sortValue(cards) - for _, sth in ipairs(self:getActives(pattern)) do - if sth:isInstanceOf(Card) then - if sth.skill:canUse(self.player, sth) and not self.player:prohibitUse(sth) then - local ret = usePlaySkill(self, sth) - if ret ~= "" then - return ret - end - 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(pattern) then - local uc = fk.ai_use_play[tc.name] - if type(uc) == "function" then - uc(self, tc) - if self.use_id then - self.use_id = - json.encode { - skill = sth.name, - subcards = selected - } - break - end - end - end - end - end - end - ask = fk.ai_askuse_card[data[1]] - if self.use_id == nil and type(ask) == "function" then - ask(self, pattern, prompt, cancelable, extra_data) - end - if self.use_id == true then - self.use_id = self.avail_cards[1] - end - if self.use_id then - return json.encode { - card = self.use_id, - targets = self.use_tos - } - end - return "" -end - -fk.ai_askuse_card = {} -fk.ai_nullification = {} - -fk.ai_askuse_card.nullification = function(self, pattern, prompt, cancelable, extra_data) - local effect = self:eventData("CardEffect") - if effect.to then - fk.askNullificationData = effect - fk.askNullification = 1 - elseif effect.from then - fk.askNullification = fk.askNullification + 1 - end - effect = fk.askNullificationData - local positive = fk.askNullification % 2 == 1 - local ask = fk.ai_nullification[effect.card.name] - if type(ask) == "function" then - ask(self, effect.card, self.room:getPlayerById(effect.to), self.room:getPlayerById(effect.from), positive) - end -end - -fk.ai_askuse_card["#AskForPeaches"] = function(self, pattern, prompt, cancelable, extra_data) - local dying = self:eventData("Dying") - local who = self.room:getPlayerById(dying.who) - if who and self:isFriend(who) then - local cards = - table.map( - self.player:getCardIds("&he"), - function(id) - return Fk:getCardById(id) - end - ) - self:sortValue(cards) - for _, sth in ipairs(self:getActives(pattern)) do - if sth:isInstanceOf(Card) then - self.use_id = sth.id - break - else - local selected = {} - for _, c in ipairs(cards) do - if sth.cardFilter(sth, c.id, selected) then - table.insert(selected, c.id) - end - end - local tc = sth.viewAs(sth, selected) - if tc and tc:matchPattern(pattern) then - self.use_id = - json.encode { - skill = sth.name, - subcards = selected - } - break - end - end - end - end -end - -fk.ai_askuse_card["#AskForPeachesSelf"] = fk.ai_askuse_card["#AskForPeaches"] - -fk.ai_card = {} -fk.cardValue = {} - -function TrustAI:assignValue(assign) - assign = assign or { "slash", "peach", "jink", "nullification" } - for v, p in ipairs(assign) do - local kept = {} - v = fk.ai_card[p] - v = v and v.value or 3 - for _, sth in ipairs(self:getActives(p)) do - if sth:isInstanceOf(Card) then - fk.cardValue[sth.id] = self:getValue(sth, kept) - else - fk.cardValue[sth.name] = self:getValue(sth, kept) + v - end - table.insert(kept, sth) - end - self.keptCv = nil - end -end - -function TrustAI:getValue(card, kept) - local v = fk.ai_card[card.name] - v = v and v.value or 0 - if kept then - if card:isInstanceOf(Card) then - if self.keptCv == nil then - self.keptCv = v - end - return v - #kept * 0.25 - else - return (self.keptCv or v) - #kept * 0.25 - end - elseif card:isInstanceOf(Card) then - return fk.cardValue[card.id] or v - else - return fk.cardValue[card.name] or v - end - return v -end - -function TrustAI:getPriority(card) - local v = card and fk.ai_card[card.name] - v = v and v.priority or 0 - if card:isInstanceOf(Card) then - if card:isInstanceOf(Armor) then - v = v + 7 - elseif card:isInstanceOf(Weapon) then - v = v + 3 - elseif card:isInstanceOf(OffensiveRide) then - v = v + 6 - elseif card:isInstanceOf(DefensiveRide) then - v = v + 4 - end - v = v + (13 - card.number) / 100 - v = v + card.suit / 100 - end - return v -end - -fk.compareFunc = { - hp = function(p) - return p.hp - end, - maxHp = function(p) - return p.maxHp - end, - hand = function(p) - return #p:getHandlyIds(true) - end, - equip = function(p) - return #p:getCardIds("e") - end, - maxcards = function(p) - return p.hp - end, - skill = function(p) - return #p:getAllSkills() - end, - defense = function(p) - return p.hp + #p:getHandlyIds(true) - end -} - -function TrustAI:sort(players, key, inverse) - key = key or "defense" - local func = fk.compareFunc[key] - if func == nil then - func = fk.compareFunc.defense - end - local function compare_func(a, b) - return func(a) < func(b) - end - table.sort(players, compare_func) - if inverse then - players = table.reverse(players) - end -end - -function TrustAI:sortValue(cards, inverse) - local function compare_func(a, b) - return self:getValue(a) < self:getValue(b) - end - table.sort(cards, compare_func) - if inverse then - cards = table.reverse(cards) - end -end - -function TrustAI:sortPriority(cards, inverse) - local function compare_func(a, b) - local va = a and self:getPriority(a) or 0 - local vb = b and self:getPriority(b) or 0 - if va == vb then - va = a and self:getValue(a) or 0 - vb = b and self:getValue(b) or 0 - end - return va > vb - end - table.sort(cards, compare_func) - if inverse then - cards = table.reverse(cards) - end -end - ----@param self TrustAI -trust_cb.AskForResponseCard = function(self, jsonData) - local data = json.decode(jsonData) - local pattern = data[2] - local prompt = data[3] - local cancelable = data[4] - local extra_data = data[5] - self:updatePlayers() - self.use_id = nil - local ask = fk.ai_response_card[prompt:split(":")[1]] - if type(ask) == "function" then - ask(self, pattern, prompt, cancelable, extra_data) - else - ask = fk.ai_response_card[data[1]] - if type(ask) == "function" then - ask(self, pattern, prompt, cancelable, extra_data) - end - local effect = self:eventData("CardEffect") - if effect and effect.card then - self:setUseId(pattern) - end - end - if self.use_id then - return json.encode { - card = self.use_id, - targets = {} - } - end - return "" -end - -fk.ai_response_card = {} - -function TrustAI:getRetrialCardId(cards, exchange) - local judge = self:eventData("Judge") - local isgood = judge.card:matchPattern(judge.pattern) - local canRetrial = {} - self:sortValue(cards) - if exchange then - for _, c in ipairs(cards) do - if c:matchPattern(judge.pattern) == isgood then - table.insert(canRetrial, c) - end - end - else - if isgood then - if self:isFriend(judge.who) then - return - end - elseif self:isEnemie(judge.who) then - return - end - end - for _, c in ipairs(cards) do - if - self:isFriend(judge.who) and c:matchPattern(judge.pattern) or - self:isEnemie(judge.who) and not c:matchPattern(judge.pattern) - then - table.insert(canRetrial, c) - end - end - if #canRetrial > 0 then - return canRetrial[1].id - end -end - -function TrustAI:getActives(pattern) - local cards = - table.map( - self.player:getCardIds("&he"), - function(id) - return Fk:getCardById(id) - end - ) - local exp = Exppattern:Parse(pattern) - cards = - table.filter( - cards, - function(c) - return exp:match(c) - end - ) - table.insertTable( - cards, - table.filter( - self.player:getAllSkills(), - function(s) - return s:isInstanceOf(ViewAsSkill) and s:enabledAtResponse(self.player, pattern) - end - ) - ) - self:sortPriority(cards) - return cards -end - -function TrustAI:setUseId(pattern) - local cards = - table.map( - self.player:getCardIds("&he"), - function(id) - return Fk:getCardById(id) - end - ) - self:sortValue(cards) - for _, sth in ipairs(self:getActives(pattern)) do - if sth:isInstanceOf(Card) then - self.use_id = sth.id - break - 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(pattern) then - self.use_id = - json.encode { - skill = sth.name, - subcards = selected - } - break - end - end - end -end - -function TrustAI:cardsView(pattern) - local actives = - table.filter( - self.player:getAllSkills(), - function(s) - return s:isInstanceOf(ViewAsSkill) and s:enabledAtResponse(self.player, pattern) - end - ) - return actives -end - ----@param self TrustAI -trust_cb.PlayCard = function(self, jsonData) - local cards = - table.map( - self.player:getHandlyIds(true), - function(id) - return Fk:getCardById(id) - end - ) - cards = - table.filter( - cards, - function(c) - return c.skill:canUse(self.player, c) and not self.player:prohibitUse(c) - end - ) - table.insertTable( - cards, - table.filter( - self.player:getAllSkills(), - function(s) - return s:isInstanceOf(ActiveSkill) and s:canUse(self.player) or - s:isInstanceOf(ViewAsSkill) and s:enabledAtPlay(self.player) - end - ) - ) - if #cards < 1 then - return - end - self:updatePlayers() - self:sortPriority(cards) - for _, sth in ipairs(cards) do - local ret = usePlaySkill(self, sth) - if ret ~= "" then - return ret - end - end - return "" -end - -fk.ai_card_chosen = {} - -trust_cb.AskForCardChosen = function(self, jsonData) - local data = json.decode(jsonData) - local to = self.room:getPlayerById(data[1]) - local chosen = fk.ai_card_chosen[data[3]] - if type(chosen) == "function" then - return chosen(self, to, data[2]) - elseif table.contains(self.friends, to) then - if string.find(data[2], "j") then - local jc = to:getCardIds("j") - if #jc > 0 then - return table.random(jc) - end - end - else - if string.find(data[2], "h") then - local hc = to:getCardIds("h") - if #hc == 1 then - return hc[1] - end - end - if string.find(data[2], "e") then - local ec = to:getCardIds("e") - if #ec > 0 then - return table.random(ec) - end - for c, id in ipairs(to:getCardIds("e")) do - --c = Fk:getCardById(id) - return id - end - end - if string.find(data[2], "h") then - local hc = to:getCardIds("h") - if #hc > 0 then - return table.random(hc) - end - end - end - return "" -end - -fk.ai_role = {} -fk.roleValue = {} - -fk.trick_judge = {} - -fk.trick_judge.indulgence = ".|.|heart" -fk.trick_judge.lightning = ".|.|^spade" -fk.trick_judge.supply_shortage = ".|.|club" - -local function table_clone(self) - local t = {} - for _, r in ipairs(self) do - table.insert(t, r) - end - return t -end - -trust_cb.AskForGuanxing = function(self, jsonData) - local data = json.decode(jsonData) - local cards = - table.map( - data.cards, - function(id) - return Fk:getCardById(id) - end - ) - self:sortValue(cards) - local top = {} - if self.room.current.phase < Player.Play then - local jt = - table.map( - self.room.current:getCardIds("j"), - function(id) - return Fk:getCardById(id) - end - ) - if #jt > 0 then - for i, j in ipairs(table.reverse(jt)) do - local tj = fk.trick_judge[j.name] - if tj then - for _, c in ipairs(table_clone(cards)) do - if c:matchPattern(tj) and #top < data.max_top_cards then - table.insert(top, c.id) - table.removeOne(cards, c) - tj = 1 - break - end - end - end - if tj ~= 1 and #cards > 0 and #top < data.max_top_cards then - table.insert(top, cards[1].id) - table.remove(cards, 1) - end - end - end - self:sortValue(cards, true) - for _, c in ipairs(table_clone(cards)) do - if #top < data.max_top_cards and c.skill:canUse(self.player, c) and usePlaySkill(self, c) ~= "" then - table.insert(top, c.id) - table.removeOne(cards, c) - break - end - end - end - for _, c in ipairs(table_clone(cards)) do - if #top < data.min_top_cards then - table.insert(top, c.id) - table.removeOne(cards, c) - break - end - end - return json.encode { - top, - table.map( - cards, - function(c) - return c.id - end - ) - } -end - function TrustAI:initialize(player) AI.initialize(self, player) self.cb_table = trust_cb - self.player = player - self.room = RoomInstance or ClientInstance - - fk.ai_role[player.id] = "neutral" - fk.roleValue[player.id] = { - lord = 0, - loyalist = 0, - rebel = 0, - renegade = 0 - } - self:updatePlayers() -end - -function TrustAI:isRolePredictable() - return self.room.settings.gameMode ~= "aaa_role_mode" -end - -local function aliveRoles(room) - fk.alive_roles = { - lord = 0, - loyalist = 0, - rebel = 0, - renegade = 0 - } - for _, ap in ipairs(room:getAllPlayers(false)) do - fk.alive_roles[ap.role] = 0 - end - for _, ap in ipairs(room:getAlivePlayers(false)) do - fk.alive_roles[ap.role] = fk.alive_roles[ap.role] + 1 - end - return fk.alive_roles -end - -function TrustAI:objectiveLevel(to) - if self.player.id == to.id then - return -2 - elseif #self.room:getAlivePlayers(false) < 3 then - return 5 - end - local ars = aliveRoles(self.room) - if self:isRolePredictable() then - fk.ai_role[self.player.id] = self.role - fk.roleValue[self.player.id][self.role] = 666 - if self.role == "renegade" then - fk.explicit_renegade = true - end - for _, p in ipairs(self.room:getAlivePlayers()) do - if - p.role == self.role or p.role == "lord" and self.role == "loyalist" or - p.role == "loyalist" and self.role == "lord" - then - table.insert(self.friends, p) - if p.id ~= self.player.id then - table.insert(self.friends_noself, p) - end - else - table.insert(self.enemies, p) - end - end - elseif self.role == "renegade" then - if to.role == "lord" then - return -1 - elseif ars.rebel < 1 then - return 4 - elseif fk.ai_role[to.id] == "loyalist" then - return ars.lord + ars.loyalist - ars.rebel - elseif fk.ai_role[to.id] == "rebel" then - local r = ars.rebel - ars.lord + ars.loyalist - if r >= 0 then - return 3 - else - return r - end - end - elseif self.role == "lord" or self.role == "loyalist" then - if fk.ai_role[to.id] == "rebel" then - return 5 - elseif to.role == "lord" then - return -2 - elseif ars.rebel < 1 then - if self.role == "lord" then - return fk.explicit_renegade and fk.ai_role[to.id] == "renegade" and 4 or to.hp > 1 and 2 or 0 - elseif fk.explicit_renegade then - return fk.ai_role[to.id] == "renegade" and 4 or -1 - else - return 3 - end - elseif fk.ai_role[to.id] == "loyalist" then - return -2 - elseif fk.ai_role[to.id] == "renegade" then - local r = ars.lord + ars.loyalist - ars.rebel - if r <= 0 then - return r - else - return 3 - end - end - elseif self.role == "rebel" then - if to.role == "lord" then - return 5 - elseif fk.ai_role[to.id] == "loyalist" then - return 4 - elseif fk.ai_role[to.id] == "rebel" then - return -2 - elseif fk.ai_role[to.id] == "renegade" then - local r = ars.rebel - ars.lord + ars.loyalist - if r > 0 then - return 1 - else - return r - end - end - end - return 0 -end - -function TrustAI:updatePlayers(update) - self.role = self.player.role - local neutrality = {} - self.enemies = {} - self.friends = {} - self.friends_noself = {} - - local aps = self.room:getAlivePlayers() - local function compare_func(a, b) - local v1 = fk.roleValue[a.id].rebel - local v2 = fk.roleValue[b.id].rebel - if v1 == v2 then - v1 = fk.roleValue[a.id].renegade - v2 = fk.roleValue[b.id].renegade - end - return v1 > v2 - end - table.sort(aps, compare_func) - fk.explicit_renegade = false - local ars = aliveRoles(self.room) - local rebel, renegade, loyalist = 0, 0, 0 - for _, ap in ipairs(aps) do - if ap.role == "lord" then - fk.ai_role[ap.id] = "loyalist" - elseif fk.roleValue[ap.id].rebel > 50 and ars.rebel > rebel then - rebel = rebel + 1 - fk.ai_role[ap.id] = "rebel" - elseif fk.roleValue[ap.id].renegade > 50 and ars.renegade > renegade then - renegade = renegade + 1 - fk.ai_role[ap.id] = "renegade" - fk.explicit_renegade = fk.roleValue[ap.id].renegade > 100 - elseif fk.roleValue[ap.id].rebel < -50 and ars.loyalist > loyalist then - loyalist = loyalist + 1 - fk.ai_role[ap.id] = "loyalist" - else - fk.ai_role[ap.id] = "neutral" - end - end - - for n, p in ipairs(self.room:getAlivePlayers(false)) do - n = self:objectiveLevel(p) - if n < 0 then - table.insert(self.friends, p) - if p.id ~= self.player.id then - table.insert(self.friends_noself, p) - end - elseif n > 0 then - table.insert(self.enemies, p) - else - table.insert(neutrality, p) - end - end - self:assignValue() - --[[ - if self.enemies<1 and #neutrality>0 - and#self.toUse<3 and self:getOverflow()>0 - then - function compare_func(a,b) - return sgs.getDefense(a) 0 or fk.roleValue[player.id].rebel > 0 and intention < 0 then - fk.roleValue[player.id].renegade = fk.roleValue[player.id].renegade + - intention * (100 - fk.roleValue[player.id].renegade) / 200 - end - local aps = player.room:getAlivePlayers() - local function compare_func(a, b) - local v1 = fk.roleValue[a.id].rebel - local v2 = fk.roleValue[b.id].rebel - if v1 == v2 then - v1 = fk.roleValue[a.id].renegade - v2 = fk.roleValue[b.id].renegade - end - return v1 > v2 - end - table.sort(aps, compare_func) - fk.explicit_renegade = false - local ars = aliveRoles(player.room) - local rebel, renegade, loyalist = 0, 0, 0 - for _, ap in ipairs(aps) do - if ap.role == "lord" then - fk.ai_role[ap.id] = "loyalist" - elseif fk.roleValue[ap.id].rebel > 50 and ars.rebel > rebel then - rebel = rebel + 1 - fk.ai_role[ap.id] = "rebel" - elseif fk.roleValue[ap.id].renegade > 50 and ars.renegade > renegade then - renegade = renegade + 1 - fk.ai_role[ap.id] = "renegade" - fk.explicit_renegade = fk.roleValue[ap.id].renegade > 100 - elseif fk.roleValue[ap.id].rebel < -50 and ars.loyalist > loyalist then - loyalist = loyalist + 1 - fk.ai_role[ap.id] = "loyalist" - else - fk.ai_role[ap.id] = "neutral" - end - end - fk.qWarning( - player.general .. - " " .. - intention .. - " " .. - fk.ai_role[player.id] .. - " rebelValue:" .. fk.roleValue[player.id].rebel .. " renegadeValue:" .. fk.roleValue[player.id].renegade - ) --]] - end -end - -function TrustAI:filterEvent(event, player, data) - if event == fk.TargetSpecified then - local callback = fk.ai_card[data.card.name] - callback = callback and callback.intention - if type(callback) == "function" then - for _, p in ipairs(TargetGroup:getRealTargets(data.tos)) do - p = self.room:getPlayerById(p) - local intention = callback(p.ai, data.card, self.room:getPlayerById(data.from)) - if type(intention) == "number" then - updateIntention(self.room:getPlayerById(data.from), p, intention) - end - end - elseif type(callback) == "number" then - for _, p in ipairs(TargetGroup:getRealTargets(data.tos)) do - p = self.room:getPlayerById(p) - updateIntention(self.room:getPlayerById(data.from), p, callback) - end - end - elseif event == fk.StartJudge then - fk.trick_judge[data.reason] = data.pattern - elseif event == fk.AfterCardsMove then - end -end - -function TrustAI:isWeak(player, getAP) - player = player or self.player - if type(player) == "number" then - player = self.room:getPlayerById(player) - end - return player.hp < 2 or player.hp <= 2 and #player:getCardIds("&h") <= 2 -end - -function TrustAI:isFriend(pid, tid) - if tid then - local bt = self:isFriend(pid) - return bt ~= nil and bt == self:isFriend(tid) - end - if type(pid) == "number" then - pid = self.room:getPlayerById(pid) - end - local ve = self:objectiveLevel(pid) - if ve < 0 then - return true - elseif ve > 0 then - return false - end -end - -function TrustAI:isEnemie(pid, tid) - if tid then - local bt = self:isFriend(pid) - return bt ~= nil and bt ~= self:isFriend(tid) - end - if type(pid) == "number" then - pid = self.room:getPlayerById(pid) - end - local ve = self:objectiveLevel(pid) - if ve > 0 then - return true - elseif ve < 0 then - return false - end -end - -function TrustAI:eventData(game_event) - local event = self.room.logic:getCurrentEvent():findParent(GameEvent[game_event], true) - return event and event.data[1] -end - -for _, n in ipairs(FileIO.ls("packages")) do - if FileIO.isDir("packages/" .. n) and FileIO.exists("packages/" .. n .. "/" .. n .. "_ai.lua") then - dofile("packages/" .. n .. "/" .. n .. "_ai.lua") - end -end --- 加载两次拓展是为了能够引用,例如属性杀的使用直接套入普通杀的使用 -for _, n in ipairs(FileIO.ls("packages")) do - if FileIO.isDir("packages/" .. n) and FileIO.exists("packages/" .. n .. "/" .. n .. "_ai.lua") then - dofile("packages/" .. n .. "/" .. n .. "_ai.lua") - end end return TrustAI diff --git a/lua/server/serverplayer.lua b/lua/server/serverplayer.lua index 26dab31b..efb847ef 100644 --- a/lua/server/serverplayer.lua +++ b/lua/server/serverplayer.lua @@ -24,8 +24,8 @@ local ServerPlayer = Player:subclass("ServerPlayer") function ServerPlayer:initialize(_self) Player.initialize(self) - self.serverplayer = _self -- 控制者 - self._splayer = _self -- 真正在玩的玩家 + self.serverplayer = _self -- 控制者 + self._splayer = _self -- 真正在玩的玩家 self._observers = { _self } -- "旁观"中的玩家,然而不包括真正的旁观者 self.id = _self:getId() self.room = nil @@ -45,7 +45,7 @@ function ServerPlayer:initialize(_self) self._prelighted_skills = {} self._timewaste_count = 0 - self.ai = TrustAI:new(self) + self.ai = SmartAI:new(self) end ---@param command string @@ -284,25 +284,25 @@ function ServerPlayer:marshal(player, observe) end for k, v in pairs(self.mark) do - player:doNotify("SetPlayerMark", json.encode{self.id, k, v}) + player:doNotify("SetPlayerMark", json.encode { self.id, k, v }) end for _, s in ipairs(self.player_skills) do - player:doNotify("AddSkill", json.encode{self.id, s.name}) + player:doNotify("AddSkill", json.encode { self.id, s.name }) end for k, v in pairs(self.cardUsedHistory) do if v[1] > 0 then - player:doNotify("AddCardUseHistory", json.encode{k, v[1]}) + player:doNotify("AddCardUseHistory", json.encode { k, v[1] }) end end for k, v in pairs(self.skillUsedHistory) do if v[4] > 0 then - player:doNotify("SetSkillUseHistory", json.encode{self.id, k, v[1], 1}) - player:doNotify("SetSkillUseHistory", json.encode{self.id, k, v[2], 2}) - player:doNotify("SetSkillUseHistory", json.encode{self.id, k, v[3], 3}) - player:doNotify("SetSkillUseHistory", json.encode{self.id, k, v[4], 4}) + player:doNotify("SetSkillUseHistory", json.encode { self.id, k, v[1], 1 }) + player:doNotify("SetSkillUseHistory", json.encode { self.id, k, v[2], 2 }) + player:doNotify("SetSkillUseHistory", json.encode { self.id, k, v[3], 3 }) + player:doNotify("SetSkillUseHistory", json.encode { self.id, k, v[4], 4 }) end end @@ -319,13 +319,13 @@ function ServerPlayer:reconnect() local room = self.room self.serverplayer:setState(fk.Player_Online) - self:doNotify("Setup", json.encode{ + self:doNotify("Setup", json.encode { self.id, self._splayer:getScreenName(), self._splayer:getAvatar(), }) self:doNotify("EnterLobby", "") - self:doNotify("EnterRoom", json.encode{ + self:doNotify("EnterRoom", json.encode { #room.players, room.timeout, room.settings, }) self:doNotify("StartGame", "") @@ -333,7 +333,7 @@ function ServerPlayer:reconnect() -- send player data for _, p in ipairs(room:getOtherPlayers(self, false, true)) do - self:doNotify("AddPlayer", json.encode{ + self:doNotify("AddPlayer", json.encode { p.id, p._splayer:getScreenName(), p._splayer:getAvatar(), @@ -350,13 +350,13 @@ function ServerPlayer:reconnect() for i = -2, -math.huge, -1 do local c = Fk.printed_cards[i] if not c then break end - self:doNotify("PrintCard", json.encode{ c.name, c.suit, c.number }) + self:doNotify("PrintCard", json.encode { c.name, c.suit, c.number }) end -- send card marks for id, marks in pairs(room.card_marks) do for k, v in pairs(marks) do - self:doNotify("SetCardMark", json.encode{ id, k, v }) + self:doNotify("SetCardMark", json.encode { id, k, v }) end end @@ -371,9 +371,9 @@ function ServerPlayer:reconnect() -- send fake skills for _, s in ipairs(self._manually_fake_skills) do - self:doNotify("AddSkill", json.encode{ self.id, s.name, true }) + self:doNotify("AddSkill", json.encode { self.id, s.name, true }) if table.contains(self.prelighted_skills, s) then - self:doNotify("PrelightSkill", json.encode{ s.name, true }) + self:doNotify("PrelightSkill", json.encode { s.name, true }) end end @@ -392,7 +392,7 @@ function ServerPlayer:turnOver() self.faceup = not self.faceup self.room:broadcastProperty(self, "faceup") - self.room:sendLog{ + self.room:sendLog { type = "#TurnOver", from = self.id, arg = self.faceup and "face_up" or "face_down", @@ -408,12 +408,12 @@ function ServerPlayer:showCards(cards) end local room = self.room - room:sendLog{ + room:sendLog { type = "#ShowCard", from = self.id, card = cards, } - room:doBroadcastNotify("ShowCard", json.encode{ + room:doBroadcastNotify("ShowCard", json.encode { from = self.id, cards = cards, }) @@ -478,7 +478,7 @@ function ServerPlayer:gainAnExtraPhase(phase, delay) self.phase = phase room:broadcastProperty(self, "phase") - room:sendLog{ + room:sendLog { type = "#GainAnExtraPhase", from = self.id, arg = phase_name_table[phase], @@ -553,7 +553,7 @@ function ServerPlayer:play(phase_table) if (not skip) or (cancel_skip) then GameEvent(GameEvent.Phase, self, self.phase):exec() else - room:sendLog{ + room:sendLog { type = "#PhaseSkipped", from = self.id, arg = phase_name_table[self.phase], @@ -565,11 +565,11 @@ end ---@param phase Phase function ServerPlayer:skip(phase) if not table.contains({ - Player.Judge, - Player.Draw, - Player.Play, - Player.Discard - }, phase) then + Player.Judge, + Player.Draw, + Player.Play, + Player.Discard + }, phase) then return end self.skipped_phases[phase] = true @@ -597,7 +597,7 @@ function ServerPlayer:gainAnExtraTurn(delay) end end - room:sendLog{ + room:sendLog { type = "#GainAnExtraTurn", from = self.id } @@ -664,7 +664,7 @@ end function ServerPlayer:addVirtualEquip(card) Player.addVirtualEquip(self, card) - self.room:doBroadcastNotify("AddVirtualEquip", json.encode{ + self.room:doBroadcastNotify("AddVirtualEquip", json.encode { player = self.id, name = card.name, subcards = card.subcards, @@ -673,7 +673,7 @@ end function ServerPlayer:removeVirtualEquip(cid) local ret = Player.removeVirtualEquip(self, cid) - self.room:doBroadcastNotify("RemoveVirtualEquip", json.encode{ + self.room:doBroadcastNotify("RemoveVirtualEquip", json.encode { player = self.id, id = cid, }) @@ -682,22 +682,22 @@ end function ServerPlayer:addCardUseHistory(cardName, num) Player.addCardUseHistory(self, cardName, num) - self:doNotify("AddCardUseHistory", json.encode{cardName, num}) + self:doNotify("AddCardUseHistory", json.encode { cardName, num }) end function ServerPlayer:setCardUseHistory(cardName, num, scope) Player.setCardUseHistory(self, cardName, num, scope) - self:doNotify("SetCardUseHistory", json.encode{cardName, num, scope}) + self:doNotify("SetCardUseHistory", json.encode { cardName, num, scope }) end function ServerPlayer:addSkillUseHistory(cardName, num) Player.addSkillUseHistory(self, cardName, num) - self.room:doBroadcastNotify("AddSkillUseHistory", json.encode{self.id, cardName, num}) + self.room:doBroadcastNotify("AddSkillUseHistory", json.encode { self.id, cardName, num }) end function ServerPlayer:setSkillUseHistory(cardName, num, scope) Player.setSkillUseHistory(self, cardName, num, scope) - self.room:doBroadcastNotify("SetSkillUseHistory", json.encode{self.id, cardName, num, scope}) + self.room:doBroadcastNotify("SetSkillUseHistory", json.encode { self.id, cardName, num, scope }) end ---@param chained boolean @@ -710,7 +710,7 @@ function ServerPlayer:setChainState(chained) self.chained = chained room:broadcastProperty(self, "chained") - room:sendLog{ + room:sendLog { type = "#ChainStateChange", from = self.id, arg = self.chained and "chained" or "un-chained" @@ -721,7 +721,7 @@ function ServerPlayer:setChainState(chained) end function ServerPlayer:reset() - self.room:sendLog{ + self.room:sendLog { type = "#ChainStateChange", from = self.id, arg = "reset-general" @@ -770,12 +770,12 @@ function ServerPlayer:addFakeSkill(skill) table.insert(self._fake_skills, skill) for _, s in ipairs(skill.related_skills) do -- if s.main_skill == skill then -- TODO: need more detailed - table.insert(self._fake_skills, s) + table.insert(self._fake_skills, s) -- end end -- TODO - self:doNotify("AddSkill", json.encode{ self.id, skill.name, true }) + self:doNotify("AddSkill", json.encode { self.id, skill.name, true }) end ---@param skill Skill @@ -794,7 +794,7 @@ function ServerPlayer:loseFakeSkill(skill) end -- TODO - self:doNotify("LoseSkill", json.encode{ self.id, skill.name, true }) + self:doNotify("LoseSkill", json.encode { self.id, skill.name, true }) end function ServerPlayer:isFakeSkill(skill) @@ -830,7 +830,7 @@ function ServerPlayer:prelightSkill(skill, isPrelight) end end - self:doNotify("PrelightSkill", json.encode{ skill.name, isPrelight }) + self:doNotify("PrelightSkill", json.encode { skill.name, isPrelight }) end ---@param isDeputy bool @@ -854,7 +854,8 @@ function ServerPlayer:revealGeneral(isDeputy, no_trigger) local ret = true if not ((isDeputy and self.general ~= "anjiang") or (not isDeputy and self.deputyGeneral ~= "anjiang")) then - local other = Fk.generals[self:getMark(isDeputy and "__heg_general" or "__heg_deputy")] or Fk.generals["blank_shibing"] + local other = Fk.generals[self:getMark(isDeputy and "__heg_general" or "__heg_deputy")] or + Fk.generals["blank_shibing"] for _, sname in ipairs(other:getSkillNameList()) do local s = Fk.skills[sname] if s.frequency == Skill.Compulsory and s.relate_to_place ~= (isDeputy and "m" or "d") then @@ -873,9 +874,9 @@ function ServerPlayer:revealGeneral(isDeputy, no_trigger) local kingdom = general.kingdom self.kingdom = kingdom if oldKingdom == "unknown" and #table.filter(room:getOtherPlayers(self, false, true), - function(p) - return p.kingdom == kingdom - end) >= #room.players // 2 then + function(p) + return p.kingdom == kingdom + end) >= #room.players // 2 then self.kingdom = "wild" end room:broadcastProperty(self, "kingdom") @@ -887,7 +888,7 @@ function ServerPlayer:revealGeneral(isDeputy, no_trigger) self.gender = general.gender end - room:sendLog{ + room:sendLog { type = "#RevealGeneral", from = self.id, arg = isDeputy and "deputyGeneral" or "mainGeneral", @@ -905,7 +906,7 @@ function ServerPlayer:revealBySkillName(skill_name) if main then if table.contains(Fk.generals[self:getMark("__heg_general")] - :getSkillNameList(), skill_name) then + :getSkillNameList(), skill_name) then self:revealGeneral(false) return end @@ -913,7 +914,7 @@ function ServerPlayer:revealBySkillName(skill_name) if deputy then if table.contains(Fk.generals[self:getMark("__heg_deputy")] - :getSkillNameList(), skill_name) then + :getSkillNameList(), skill_name) then self:revealGeneral(true) return end @@ -926,7 +927,7 @@ function ServerPlayer:hideGeneral(isDeputy) local mark = isDeputy and "__heg_deputy" or "__heg_general" self:setMark(mark, generalName) - self:doNotify("SetPlayerMark", json.encode{ self.id, mark, generalName}) + self:doNotify("SetPlayerMark", json.encode { self.id, mark, generalName }) if isDeputy then room:setDeputyGeneral(self, "anjiang") @@ -961,6 +962,7 @@ function ServerPlayer:hideGeneral(isDeputy) room.logic:trigger(fk.GeneralHidden, room, generalName) end + -- 神貂蝉 ---@param p ServerPlayer @@ -980,7 +982,7 @@ function ServerPlayer:addBuddy(other) other = self.room:getPlayerById(other) end Player.addBuddy(self, other) - self:doNotify("AddBuddy", json.encode{ other.id, other.player_cards[Player.Hand] }) + self:doNotify("AddBuddy", json.encode { other.id, other.player_cards[Player.Hand] }) end function ServerPlayer:removeBuddy(other) @@ -992,7 +994,7 @@ function ServerPlayer:removeBuddy(other) end function ServerPlayer:getAI() - return self.ai + return self.ai end return ServerPlayer