diff --git a/Fk/Pages/Room.qml b/Fk/Pages/Room.qml index 2e88a42b..35f3b13f 100644 --- a/Fk/Pages/Room.qml +++ b/Fk/Pages/Room.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Controls +import QtQuick.Dialogs import QtQuick.Layouts import QtMultimedia import Fk @@ -71,15 +72,15 @@ Item { } // tmp - DelayButton { - id: quitButton - text: "quit" + Button { + id: menuButton anchors.top: parent.top anchors.right: parent.right - delay: Debugging ? 10 : 1000 - onActivated: { - // ClientInstance.clearPlayers(); - ClientInstance.notifyServer("QuitRoom", "[]"); + anchors.rightMargin: 10 + text: Backend.translate("Menu") + z: 2 + onClicked: { + menuContainer.visible || menuContainer.open(); } } @@ -345,6 +346,7 @@ Item { drank: model.drank isOwner: model.isOwner ready: model.ready + surrendered: model.surrendered onSelectedChanged: { Logic.updateSelectedTargets(playerid, selected); @@ -395,6 +397,7 @@ Item { MetroButton { text: Backend.translate("Sort Cards") textFont.pixelSize: 28 + onClicked: Logic.resortHandcards(); } MetroButton { text: Backend.translate("Chat") @@ -790,7 +793,7 @@ Item { MiscStatus { id: miscStatus - anchors.right: quitButton.left + anchors.right: menuButton.left anchors.top: parent.top anchors.rightMargin: 16 anchors.topMargin: 8 @@ -1067,6 +1070,7 @@ Item { drank: 0, isOwner: false, ready: false, + surrendered: false, }); } diff --git a/Fk/Pages/RoomLogic.js b/Fk/Pages/RoomLogic.js index 6490068b..080964aa 100644 --- a/Fk/Pages/RoomLogic.js +++ b/Fk/Pages/RoomLogic.js @@ -204,6 +204,47 @@ function moveCards(moves) { } } +function resortHandcards() { + if (!dashboard.handcardArea.cards.length) { + return; + } + + const subtypeString2Number = { + ["none"]: Card.SubtypeNone, + ["delayed_trick"]: Card.SubtypeDelayedTrick, + ["weapon"]: Card.SubtypeWeapon, + ["armor"]: Card.SubtypeArmor, + ["defensive_horse"]: Card.SubtypeDefensiveRide, + ["offensive_horse"]: Card.SubtypeOffensiveRide, + ["treasure"]: Card.SubtypeTreasure, + } + + dashboard.handcardArea.cards.sort((prev, next) => { + if (prev.type === next.type) { + const prevSubtypeNumber = subtypeString2Number[prev.subtype]; + const nextSubtypeNumber = subtypeString2Number[next.subtype]; + if (prevSubtypeNumber === nextSubtypeNumber) { + const splitedPrevName = prev.name.split('__'); + const prevTrueName = splitedPrevName[splitedPrevName.length - 1]; + + const splitedNextName = next.name.split('__'); + const nextTrueName = splitedNextName[splitedNextName.length - 1]; + if (prevTrueName === nextTrueName) { + return prev.cid - next.cid; + } else { + return prevTrueName > nextTrueName ? -1 : 1; + } + } else { + return prevSubtypeNumber - nextSubtypeNumber; + } + } else { + return prev.type - next.type; + } + }); + + dashboard.handcardArea.updateCardPosition(true); +} + function setEmotion(id, emotion, isCardId) { let path; if (OS === "Win") { @@ -1336,3 +1377,7 @@ callbacks["AskForLuckCard"] = (j) => { roomScene.okButton.enabled = true; roomScene.cancelButton.enabled = true; } + +callbacks["CancelRequest"] = (jsonData) => { + ClientInstance.replyToServer("", "__cancel") +} diff --git a/Fk/RoomElement/CardArea.qml b/Fk/RoomElement/CardArea.qml index 54a7b1ce..7066a40d 100644 --- a/Fk/RoomElement/CardArea.qml +++ b/Fk/RoomElement/CardArea.qml @@ -62,6 +62,9 @@ Item { card = cards[i]; card.origX = i * spacing; card.origY = 0; + card.z = i + 1; + card.initialZ = i + 1; + card.maxZ = cards.length; } } diff --git a/Fk/RoomElement/CardItem.qml b/Fk/RoomElement/CardItem.qml index aff7ddbe..c41d789a 100644 --- a/Fk/RoomElement/CardItem.qml +++ b/Fk/RoomElement/CardItem.qml @@ -26,6 +26,7 @@ Item { property string name: "slash" property string extension: "" property string virt_name: "" + property int type: 0 property string subtype: "" property string color: "" // only use when suit is empty property string footnote: "" // footnote, e.g. "A use card to B" @@ -51,6 +52,8 @@ Item { property bool showDetail: false property int origX: 0 property int origY: 0 + property int initialZ: 0 + property int maxZ: 0 property real origOpacity: 1 // property bool isClicked: false property bool moveAborted: false @@ -259,10 +262,12 @@ Item { if (!draggable) return; if (hovered) { glow.visible = true; - root.z++; + + root.z = root.maxZ ? root.maxZ + 1 : root.z + 1; } else { glow.visible = false; - root.z--; + + root.z = root.initialZ ? root.initialZ : root.z - 1 } } } @@ -317,6 +322,7 @@ Item { suit = data.suit; number = data.number; color = data.color; + type = data.type ? data.type : 0 subtype = data.subtype ? data.subtype : ""; virt_name = data.virt_name ? data.virt_name : ""; mark = data.mark ?? {}; @@ -330,6 +336,7 @@ Item { suit: suit, number: number, color: color, + type: type, subtype: subtype, virt_name: virt_name, mark: mark, diff --git a/Fk/RoomElement/MiscStatus.qml b/Fk/RoomElement/MiscStatus.qml index f056ce95..00116dbb 100644 --- a/Fk/RoomElement/MiscStatus.qml +++ b/Fk/RoomElement/MiscStatus.qml @@ -9,7 +9,8 @@ Item { visible: roundNum || pileNum function getTimeString(time) { - const s = time % 60; + let s = time % 60; + s < 10 && (s = '0' + s); const m = (time - s) / 60; const h = (time - s - m * 60) / 3600; return h ? `${h}:${m}:${s}` : `${m}:${s}`; diff --git a/Fk/RoomElement/Photo.qml b/Fk/RoomElement/Photo.qml index 516d4070..6eb735cb 100644 --- a/Fk/RoomElement/Photo.qml +++ b/Fk/RoomElement/Photo.qml @@ -52,6 +52,7 @@ Item { property bool selected: false property bool playing: false + property bool surrendered: false onPlayingChanged: { if (playing) { animPlaying.start(); @@ -235,7 +236,7 @@ Item { anchors.fill: photoMask source: generalImgItem saturation: 0 - visible: root.dead + visible: root.dead || root.surrendered } Rectangle { @@ -370,8 +371,8 @@ Item { Image { // id: saveme - visible: root.dead || root.dying - source: SkinBank.DEATH_DIR + (root.dead ? root.role : "saveme") + visible: root.dead || root.dying || root.surrendered + source: SkinBank.DEATH_DIR + (root.dead ? root.role : root.surrendered ? "surrender" : "saveme") anchors.centerIn: photoMask } diff --git a/lua/client/client_util.lua b/lua/client/client_util.lua index 033018a2..b6481a73 100644 --- a/lua/client/client_util.lua +++ b/lua/client/client_util.lua @@ -98,6 +98,7 @@ function GetCardData(id) suit = card:getSuitString(), color = card:getColorString(), mark = mark, + type = card.type, subtype = cardSubtypeStrings[card.sub_type] } if card.skillName ~= "" then @@ -625,4 +626,9 @@ function SetObserving(o) ClientInstance.observing = o end +function CheckSurrenderAvailable(playedTime) + local curMode = ClientInstance.room_settings.gameMode + return json.encode(Fk.game_modes[curMode]:surrenderFunc(playedTime)) +end + dofile "lua/client/i18n/init.lua" diff --git a/lua/core/game_mode.lua b/lua/core/game_mode.lua index a613f0ce..5ab380fd 100644 --- a/lua/core/game_mode.lua +++ b/lua/core/game_mode.lua @@ -10,6 +10,7 @@ ---@field public maxPlayer integer @ 最大玩家数 ---@field public rule TriggerSkill @ 规则(通过技能完成,通常用来为特定角色及特定时机提供触发事件) ---@field public logic fun() @ 逻辑(通过function完成,通常用来初始化、分配身份及座次) +---@field public surrenderFunc fun() local GameMode = class("GameMode") --- 构造函数,不可随意调用。 diff --git a/lua/fk_ex.lua b/lua/fk_ex.lua index 978b3bfa..23d5dce2 100644 --- a/lua/fk_ex.lua +++ b/lua/fk_ex.lua @@ -540,5 +540,14 @@ function fk.CreateGameMode(spec) local ret = GameMode:new(spec.name, spec.minPlayer, spec.maxPlayer) ret.rule = spec.rule ret.logic = spec.logic + + if spec.winner_getter then + assert(type(spec.winner_getter) == "function") + ret.getWinner = spec.winner_getter + end + if spec.surrender_func then + assert(type(spec.surrender_func) == "function") + ret.surrenderFunc = spec.surrender_func + end return ret end diff --git a/lua/server/request.lua b/lua/server/request.lua index e25cc686..954ee131 100644 --- a/lua/server/request.lua +++ b/lua/server/request.lua @@ -135,6 +135,23 @@ request_handlers["changeself"] = function(room, id, reqlist) }) end +request_handlers["surrender"] = function(room, id, reqlist) + local logic = room.logic + local curEvent = logic:getCurrentEvent() + if curEvent then + curEvent:addExitFunc( + function() + local player = room:getPlayerById(id) + player.surrendered = true + room:broadcastProperty(player, "surrendered") + room:gameOver(Fk.game_modes[room.settings.gameMode]:getWinner(player)) + end + ) + room.hasSurrendered = true + room:doBroadcastNotify("CancelRequest", "") + end +end + request_handlers["newroom"] = function(s, id) s:registerRoom(id) end diff --git a/lua/server/room.lua b/lua/server/room.lua index 0f0eb7d3..7327501d 100644 --- a/lua/server/room.lua +++ b/lua/server/room.lua @@ -2950,7 +2950,7 @@ function Room:getCardsFromPileByRule(pattern, num, fromPile) if fromPile == "discardPile" then pileToSearch = self.discard_pile elseif fromPile == "allPiles" then - pileToSearch = table.clone(self.draw_pile) + pileToSearch = table.simpleClone(self.draw_pile) table.insertTable(pileToSearch, self.discard_pile) end diff --git a/lua/server/serverplayer.lua b/lua/server/serverplayer.lua index 3bb6321d..80d42d41 100644 --- a/lua/server/serverplayer.lua +++ b/lua/server/serverplayer.lua @@ -92,6 +92,10 @@ local function _waitForReply(player, timeout) player.request_timeout = timeout player.request_start = start if state ~= fk.Player_Online then + if player.room.hasSurrendered then + return "__cancel" + end + if state ~= fk.Player_Robot then player.room:checkNoHuman() player.room:delay(500) @@ -117,6 +121,12 @@ local function _waitForReply(player, timeout) player.serverplayer:setThinking(false) return "" end + + if player.room.hasSurrendered then + player.serverplayer:setThinking(false) + return "" + end + coroutine.yield("__handleRequest", rest) end end diff --git a/packages/standard/game_rule.lua b/packages/standard/game_rule.lua index bca59a8f..4510af08 100644 --- a/packages/standard/game_rule.lua +++ b/packages/standard/game_rule.lua @@ -1,33 +1,5 @@ -- SPDX-License-Identifier: GPL-3.0-or-later ----@param victim ServerPlayer -local function getWinner(victim) - local room = victim.room - local winner = "" - local alive = room.alive_players - - if victim.role == "lord" then - if #alive == 1 and alive[1].role == "renegade" then - winner = "renegade" - else - winner = "rebel" - end - elseif victim.role ~= "loyalist" then - local lord_win = true - for _, p in ipairs(alive) do - if p.role == "rebel" or p.role == "renegade" then - lord_win = false - break - end - end - if lord_win then - winner = "lord+loyalist" - end - end - - return winner -end - ---@param killer ServerPlayer local function rewardAndPunish(killer, victim) if killer.dead then return end @@ -104,7 +76,7 @@ GameRule = fk.CreateTriggerSkill{ end end, [fk.GameOverJudge] = function() - local winner = getWinner(player) + local winner = Fk.game_modes[room.settings.gameMode]:getWinner(player) if winner ~= "" then room:gameOver(winner) return true