diff --git a/lua/client/client.lua b/lua/client/client.lua index 856ec252..11c277d0 100644 --- a/lua/client/client.lua +++ b/lua/client/client.lua @@ -281,6 +281,27 @@ fk.client_callback["AskForCardChosen"] = function(jsonData) ClientInstance:notifyUI("AskForCardChosen", json.encode(ui_data)) end +fk.client_callback["AskForCardsChosen"] = function(jsonData) + -- jsonData: [ int target_id, int min, int max, string flag, int reason ] + local data = json.decode(jsonData) + local id, min, max, flag, reason = data[1], data[2], data[3], data[4], data[5] + local target = ClientInstance:getPlayerById(id) + local hand = target.player_cards[Player.Hand] + local equip = target.player_cards[Player.Equip] + local judge = target.player_cards[Player.Judge] + if not string.find(flag, "h") then + hand = {} + end + if not string.find(flag, "e") then + equip = {} + end + if not string.find(flag, "j") then + judge = {} + end + local ui_data = {hand, equip, judge, min, max, reason} + ClientInstance:notifyUI("AskForCardsChosen", json.encode(ui_data)) +end + --- separated moves to many moves(one card per move) ---@param moves CardsMoveStruct[] local function separateMoves(moves) diff --git a/lua/core/card.lua b/lua/core/card.lua index 1f5ab6f4..2cb014cb 100644 --- a/lua/core/card.lua +++ b/lua/core/card.lua @@ -236,7 +236,7 @@ function Card.static:getIdList(c) -- array local ret = {} for _, c2 in ipairs(c) do - table.insertTable(ret, Card:getIdList(c)) + table.insertTable(ret, Card:getIdList(c2)) end return ret end diff --git a/lua/core/util.lua b/lua/core/util.lua index c35dcf35..0d58288f 100644 --- a/lua/core/util.lua +++ b/lua/core/util.lua @@ -142,12 +142,12 @@ function table.random(tab, n) if #tab == 0 then return nil end local tmp = {table.unpack(tab)} local ret = {} - while n > 0 do + while n > 0 and #tmp > 0 do local i = math.random(1, #tmp) table.insert(ret, table.remove(tmp, i)) n = n - 1 end - return #ret == 1 and ret[1] or ret + return n == 1 and ret[1] or ret end ---@param delimiter string diff --git a/lua/server/room.lua b/lua/server/room.lua index 0cdddd23..9865bd05 100644 --- a/lua/server/room.lua +++ b/lua/server/room.lua @@ -842,6 +842,7 @@ end ---@param target ServerPlayer ---@param flag string @ "hej", h for handcard, e for equip, j for judge ---@param reason string +---@return integer function Room:askForCardChosen(chooser, target, flag, reason) local command = "AskForCardChosen" self:notifyMoveFocus(chooser, command) @@ -849,12 +850,6 @@ function Room:askForCardChosen(chooser, target, flag, reason) local result = self:doRequest(chooser, command, json.encode(data)) if result == "" then - result = -1 - else - result = tonumber(result) - end - - if result == -1 then local areas = {} if string.find(flag, "h") then table.insert(areas, Player.Hand) end if string.find(flag, "e") then table.insert(areas, Player.Equip) end @@ -862,11 +857,55 @@ function Room:askForCardChosen(chooser, target, flag, reason) local handcards = target:getCardIds(areas) if #handcards == 0 then return end result = handcards[math.random(1, #handcards)] + 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 +---@param chooser ServerPlayer +---@param target ServerPlayer +---@param min integer +---@param max integer +---@param flag string @ "hej", h for handcard, e for equip, j for judge +---@param reason string +---@return integer[] +function Room:askForCardsChosen(chooser, target, min, max, flag, reason) + if min == 1 and max == 1 then + return { self:askForCardChosen(chooser, target, flag, reason) } + end + + local command = "AskForCardsChosen" + self:notifyMoveFocus(chooser, command) + local data = {target.id, min, max, flag, reason} + local result = self:doRequest(chooser, command, json.encode(data)) + + local ret + if result ~= "" then + ret = json.decode(result) + else + local areas = {} + 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 + local handcards = target:getCardIds(areas) + 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) + table.insertTable(new_ret, table.random(target:getCardIds(Player.Hand), #ret - #new_ret)) + + return new_ret +end + ---@param player ServerPlayer ---@param choices string[] ---@param skill_name string @@ -1056,6 +1095,51 @@ function Room:askForNullification(players, card_name, pattern, prompt, cancelabl return nil end +-- AG(a.k.a. Amazing Grace) functions +-- Popup a box that contains many cards, then ask player to choose one + +---@param player ServerPlayer +---@param id_list integer[] | Card[] +---@param cancelable boolean +---@param reason string +---@return integer +function Room:askForAG(player, id_list, cancelable, reason) + id_list = Card:getIdList(id_list) + if #id_list == 1 and not cancelable then + return id_list[1] + end + + local command = "AskForAG" + self:notifyMoveFocus(player, reason or command) + local data = { id_list, cancelable, reason } + local ret = self:doRequest(player, command, json.encode(data)) + if ret == "" and not cancelable then + ret = table.random(id_list) + end + return tonumber(ret) +end + +---@param player ServerPlayer +---@param id_list integer[] | Card[] +---@param disable_ids integer[] | Card[] +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 }) +end + +---@param player ServerPlayer +---@param id integer +function Room:takeAG(taker, id, notify_list) + self:doBroadcastNotify("TakeAG", json.encode{ taker.id, id }, notify_list) +end + +---@param player ServerPlayer +function Room:closeAG(player) + if player then player:doNotify("CloseAG", "") + else self:doBroadcastNotify("CloseAG", "") end +end + -- Show a qml dialog and return qml's ClientInstance.replyToServer -- Do anything you like through this function diff --git a/packages/test/init.lua b/packages/test/init.lua index 779e9428..abbf1c8d 100644 --- a/packages/test/init.lua +++ b/packages/test/init.lua @@ -60,11 +60,20 @@ local test_active = fk.CreateActiveSkill{ can_use = function(self, player) return true end, + target_filter = function() return true end, on_use = function(self, room, effect) --room:doSuperLightBox("packages/test/qml/Test.qml") local from = room:getPlayerById(effect.from) - local result = room:askForCustomDialog(from, "simayi", "packages/test/qml/TestDialog.qml", "Hello, world. FROM LUA") - print(result) + -- local result = room:askForCustomDialog(from, "simayi", "packages/test/qml/TestDialog.qml", "Hello, world. FROM LUA") + -- print(result) + + -- room:fillAG(from, { 1, 43, 77 }) + -- local id = room:askForAG(from, { 1, 43, 77 }) + -- room:takeAG(from, id) + -- room:delay(2000) + -- room:closeAG(from) + local cards = room:askForCardsChosen(from, room:getPlayerById(effect.tos[1]), 2, 3, "hej", "") + p(cards) end, } local test2 = General(extension, "mouxusheng", "wu", 4, 4, General.Female) diff --git a/qml/Pages/Room.qml b/qml/Pages/Room.qml index 6d233031..dd8f298d 100644 --- a/qml/Pages/Room.qml +++ b/qml/Pages/Room.qml @@ -18,6 +18,7 @@ Item { property bool isStarted: false property alias popupBox: popupBox + property alias manualBox: manualBox property alias bigAnim: bigAnim property alias promptText: prompt.text property alias okCancel: okCancel @@ -388,6 +389,26 @@ Item { } } + // manualBox: same as popupBox, but must be closed manually + Loader { + id: manualBox + z: 999 + onSourceChanged: { + if (item === null) + return; + item.finished.connect(() => source = ""); + item.widthChanged.connect(() => manualBox.moveToCenter()); + item.heightChanged.connect(() => manualBox.moveToCenter()); + moveToCenter(); + } + + function moveToCenter() + { + item.x = Math.round((roomArea.width - item.width) / 2); + item.y = Math.round(roomArea.height * 0.67 - item.height / 2); + } + } + Loader { id: bigAnim anchors.fill: parent diff --git a/qml/Pages/RoomElement/AG.qml b/qml/Pages/RoomElement/AG.qml new file mode 100644 index 00000000..84da8209 --- /dev/null +++ b/qml/Pages/RoomElement/AG.qml @@ -0,0 +1,64 @@ +import QtQuick + +GraphicsBox { + property int spacing: 5 + property string currentPlayerName: "" + property bool interactive: false + + id: root + title.text: Backend.translate("Please choose cards") + width: cards.count * 100 + spacing * (cards.count - 1) + 25 + height: 180 + + ListModel { + id: cards + } + + Row { + x: 20 + y: 35 + spacing: root.spacing + + Repeater { + model: cards + + CardItem { + cid: model.cid + name: model.name + suit: model.suit + number: model.number + autoBack: false + selectable: model.selectable + footnote: model.footnote + footnoteVisible: true + onClicked: { + if (root.interactive && selectable) { + root.interactive = false; + roomScene.state = "notactive"; + ClientInstance.replyToServer("", cid.toString()); + } + } + } + } + } + + function addIds(ids) { + ids.forEach((id) => { + let data = Backend.callLuaFunction("GetCardData", [id]); + data = JSON.parse(data); + data.selectable = true; + data.footnote = ""; + cards.append(data); + }); + } + + function takeAG(g, cid) { + for (let i = 0; i < cards.count; i++) { + let item = cards.get(i); + if (item.cid !== cid) continue; + item.footnote = g; + item.selectable = false; + break; + } + } +} diff --git a/qml/Pages/RoomElement/PlayerCardBox.qml b/qml/Pages/RoomElement/PlayerCardBox.qml index bb97e7b0..c8fa0085 100644 --- a/qml/Pages/RoomElement/PlayerCardBox.qml +++ b/qml/Pages/RoomElement/PlayerCardBox.qml @@ -1,16 +1,21 @@ import QtQuick import QtQuick.Layouts - +import ".." GraphicsBox { - signal cardSelected(int cid) - id: root title.text: Backend.translate("$ChooseCard") - //@to-do: Adjust the UI design in case there are more than 7 cards + // TODO: Adjust the UI design in case there are more than 7 cards width: 70 + Math.min(7, Math.max(1, handcards.count, equips.count, delayedTricks.count)) * 100 height: 50 + (handcards.count > 0 ? 150 : 0) + (equips.count > 0 ? 150 : 0) + (delayedTricks.count > 0 ? 150 : 0) + signal cardSelected(int cid) + signal cardsSelected(var ids) + property bool multiChoose: false + property int min: 0 + property int max: 1 + property var selected_ids: [] + ListModel { id: handcards } @@ -66,7 +71,23 @@ GraphicsBox { autoBack: false known: false selectable: true - onClicked: root.cardSelected(cid); + onClicked: { + if (!root.multiChoose) { + root.cardSelected(cid); + } + } + onSelectedChanged: { + if (selected) { + origY = origY - 20; + root.selected_ids.push(cid); + } else { + origY = origY + 20; + root.selected_ids.splice(root.selected_ids.indexOf(cid), 1); + } + origX = x; + goBack(true); + root.selected_ids = root.selected_ids; + } } } } @@ -107,7 +128,23 @@ GraphicsBox { number: model.number autoBack: false selectable: true - onClicked: root.cardSelected(cid); + onClicked: { + if (!root.multiChoose) { + root.cardSelected(cid); + } + } + onSelectedChanged: { + if (selected) { + origY = origY - 20; + root.selected_ids.push(cid); + } else { + origY = origY + 20; + root.selected_ids.splice(root.selected_ids.indexOf(cid)); + } + origX = x; + goBack(true); + root.selected_ids = root.selected_ids; + } } } } @@ -148,11 +185,34 @@ GraphicsBox { number: model.number autoBack: false selectable: true - onClicked: root.cardSelected(cid); + onClicked: { + if (!root.multiChoose) { + root.cardSelected(cid); + } + } + onSelectedChanged: { + if (selected) { + origY = origY - 20; + root.selected_ids.push(cid); + } else { + origY = origY + 20; + root.selected_ids.splice(root.selected_ids.indexOf(cid)); + } + origX = x; + goBack(true); + root.selected_ids = root.selected_ids; + } } } } } + + MetroButton { + text: Backend.translate("OK") + visible: root.multiChoose + enabled: root.selected_ids.length <= root.max && root.selected_ids.length >= root.min + onClicked: root.cardsSelected(root.selected_ids) + } } onCardSelected: finished(); diff --git a/qml/Pages/RoomLogic.js b/qml/Pages/RoomLogic.js index c94253a2..3a19cdbe 100644 --- a/qml/Pages/RoomLogic.js +++ b/qml/Pages/RoomLogic.js @@ -228,7 +228,7 @@ function setEmotion(id, emotion, isCardId) { } } - let animation = component.createObject(photo, {source: path}); + let animation = component.createObject(photo, {source: (OS === "Win" ? "file:///" : "") + path}); animation.anchors.centerIn = photo; if (isCardId) { animation.started.connect(() => photo.busy = true); @@ -669,6 +669,52 @@ callbacks["AskForCardChosen"] = function(jsonData) { }); } +callbacks["AskForCardsChosen"] = function(jsonData) { + // jsonData: [ int[] handcards, int[] equips, int[] delayedtricks, + // int min, int max, string reason ] + let data = JSON.parse(jsonData); + let handcard_ids = data[0]; + let equip_ids = data[1]; + let delayedTrick_ids = data[2]; + let min = data[3]; + let max = data[4]; + let reason = data[5]; + let handcards = []; + let equips = []; + let delayedTricks = []; + + handcard_ids.forEach(id => { + let card_data = JSON.parse(Backend.callLuaFunction("GetCardData", [id])); + handcards.push(card_data); + }); + + equip_ids.forEach(id => { + let card_data = JSON.parse(Backend.callLuaFunction("GetCardData", [id])); + equips.push(card_data); + }); + + delayedTrick_ids.forEach(id => { + let card_data = JSON.parse(Backend.callLuaFunction("GetCardData", [id])); + delayedTricks.push(card_data); + }); + + roomScene.promptText = Backend.translate("#AskForChooseCard") + .arg(Backend.translate(reason)); + roomScene.state = "replying"; + roomScene.popupBox.source = "RoomElement/PlayerCardBox.qml"; + let box = roomScene.popupBox.item; + box.multiChoose = true; + box.min = min; + box.max = max; + box.addHandcards(handcards); + box.addEquips(equips); + box.addDelayedTricks(delayedTricks); + roomScene.popupBox.moveToCenter(); + box.cardsSelected.connect((ids) => { + replyToServer(JSON.stringify(ids)); + }); +} + callbacks["MoveCards"] = function(jsonData) { // jsonData: merged moves let moves = JSON.parse(jsonData); @@ -910,6 +956,32 @@ callbacks["GameOver"] = function(jsonData) { roomScene.isStarted = false; } +callbacks["FillAG"] = (j) => { + let data = JSON.parse(j); + let ids = data[0]; + roomScene.manualBox.source = "RoomElement/AG.qml"; + roomScene.manualBox.item.addIds(ids); +} + +callbacks["AskForAG"] = (j) => { + roomScene.state = "replying"; + roomScene.manualBox.item.interactive = true; +} + +callbacks["TakeAG"] = (j) => { + if (!roomScene.manualBox.item) return; + let data = JSON.parse(j); + let pid = data[0]; + let cid = data[1]; + let item = getPhotoOrSelf(pid); + let general = Backend.translate(item.general); + + // the item should be AG box + roomScene.manualBox.item.takeAG(general, cid); +} + +callbacks["CloseAG"] = () => roomScene.manualBox.item.close(); + callbacks["CustomDialog"] = (j) => { let data = JSON.parse(j); let path = data.path;