diff --git a/image/photo/owner.png b/image/photo/owner.png new file mode 100644 index 00000000..853f2df5 Binary files /dev/null and b/image/photo/owner.png differ diff --git a/image/photo/ready.png b/image/photo/ready.png new file mode 100644 index 00000000..8e847e93 Binary files /dev/null and b/image/photo/ready.png differ diff --git a/lua/client/client.lua b/lua/client/client.lua index 17115412..8e2901be 100644 --- a/lua/client/client.lua +++ b/lua/client/client.lua @@ -1,7 +1,7 @@ Client = class('Client') -- load client classes -ClientPlayer = require "client/clientplayer" +ClientPlayer = require "client.clientplayer" freekill.client_callback = {} diff --git a/lua/core/engine.lua b/lua/core/engine.lua index 761410c3..660742dd 100644 --- a/lua/core/engine.lua +++ b/lua/core/engine.lua @@ -106,8 +106,8 @@ function Engine:getGeneralsRandomly(num, generalPool, except, filter) except = except or {} local availableGenerals = {} - for _, general in ipairs(generalPool) do - if not table.contains(except, general) and not (filter and filter(general)) then + for _, general in pairs(generalPool) do + if not table.contains(except, general.name) and not (filter and filter(general)) then table.insert(availableGenerals, general) end end @@ -117,9 +117,9 @@ function Engine:getGeneralsRandomly(num, generalPool, except, filter) end local result = {} - while num > 0 do + for i = 1, num do local randomGeneral = math.random(1, #availableGenerals) - table.insert(result, randomGeneral) + table.insert(result, availableGenerals[randomGeneral]) table.remove(availableGenerals, randomGeneral) if #availableGenerals == 0 then diff --git a/lua/core/player.lua b/lua/core/player.lua index 65fc13b3..92c591af 100644 --- a/lua/core/player.lua +++ b/lua/core/player.lua @@ -13,7 +13,8 @@ function Player:initialize() self.chained = false self.dying = false self.dead = false - + self.state = "" + self.playerSkills = {} end diff --git a/lua/core/util.lua b/lua/core/util.lua index e9a76565..73ed643a 100644 --- a/lua/core/util.lua +++ b/lua/core/util.lua @@ -58,6 +58,8 @@ FileIO = { isDir = freekill.QmlBackend_isDir } +os.getms = freekill.GetMicroSecond + Stack = class("Stack") function Stack:initialize() self.t = {} diff --git a/lua/freekill.lua b/lua/freekill.lua index ff88cf32..3899c84e 100644 --- a/lua/freekill.lua +++ b/lua/freekill.lua @@ -8,7 +8,7 @@ package.path = package.path .. ";./lua/lib/?.lua" class = require "middleclass" json = require "json" require "sha256" -Util = require "core/util" +Util = require "core.util" math.randomseed(os.time()) DebugMode = true @@ -20,12 +20,12 @@ function pt(t) end -- load core classes -Engine = require "core/engine" -Package = require "core/package" -General = require "core/general" -Card = require "core/card" -Skill = require "core/skill" -Player = require "core/player" +Engine = require "core.engine" +Package = require "core.package" +General = require "core.general" +Card = require "core.card" +Skill = require "core.skill" +Player = require "core.player" -- load packages Fk = Engine:new() diff --git a/lua/server/gamelogic.lua b/lua/server/gamelogic.lua index 7d42098a..6f569a6e 100644 --- a/lua/server/gamelogic.lua +++ b/lua/server/gamelogic.lua @@ -25,30 +25,106 @@ function GameLogic:run() self.room:adjustSeats() self:chooseGenerals() - self:startGame() + self:prepareForStart() + self:action() end function GameLogic:assignRoles() - local n = #self.room.players + local room = self.room + local n = #room.players local roles = self.role_table[n] table.shuffle(roles) for i = 1, n do - local p = self.room.players[i] + local p = room.players[i] p.role = roles[i] if p.role == "lord" then - self.room:broadcastProperty(p, "role") + room:broadcastProperty(p, "role") else - self.room:notifyProperty(p, p, "role") + room:notifyProperty(p, p, "role") end end end function GameLogic:chooseGenerals() + local room = self.room + local function setPlayerGeneral(player, general) + if Fk.generals[general] == nil then return end + player.general = general + self.room:notifyProperty(player, player, "general") + end + local lord = room:getLord() + local lord_general = nil + if lord ~= nil then + local generals = Fk:getGeneralsRandomly(3) + for i = 1, #generals do + generals[i] = generals[i].name + end + lord_general = room:askForGeneral(lord, generals) + setPlayerGeneral(lord, lord_general) + room:broadcastProperty(lord, "general") + end + local nonlord = room:getOtherPlayers(lord) + local generals = Fk:getGeneralsRandomly(#nonlord * 3, Fk.generals, {lord_general}) + table.shuffle(generals) + for _, p in ipairs(nonlord) do + local arg = { + (table.remove(generals, 1)).name, + (table.remove(generals, 1)).name, + (table.remove(generals, 1)).name, + } + p.request_data = json.encode(arg) + p.default_reply = arg[1] + end + + room:doBroadcastRequest("AskForGeneral", nonlord) + for _, p in ipairs(nonlord) do + if p.general == "" and p.reply_ready then + local general = json.decode(p.client_reply)[1] + setPlayerGeneral(p, general) + else + setPlayerGeneral(p, p.default_reply) + end + p.default_reply = "" + end end -function GameLogic:startGame() +function GameLogic:prepareForStart() + local room = self.room + local players = room.players + room.alive_players = players + for i = 1, #players - 1 do + players[i].next = players[i + 1] + end + players[#players].next = players[1] + + for _, p in ipairs(players) do + assert(p.general ~= "") + local general = Fk.generals[p.general] + p.maxHp = general.maxHp + p.hp = general.hp + -- TODO: setup AI here + + if p.role ~= "lord" then + room:broadcastProperty(p, "general") + elseif #players >= 5 then + p.maxHp = p.maxHp + 1 + p.hp = p.hp + 1 + end + room:broadcastProperty(p, "maxHp") + room:broadcastProperty(p, "hp") + + -- TODO: add skills to player + end + + -- TODO: prepare drawPile + -- TODO: init cards in drawPile + + -- TODO: init trigger table for self +end + +function GameLogic:action() end diff --git a/lua/server/room.lua b/lua/server/room.lua index 643f6827..6e6f00e9 100644 --- a/lua/server/room.lua +++ b/lua/server/room.lua @@ -3,13 +3,16 @@ local Room = class("Room") function Room:initialize(_room) self.room = _room self.players = {} -- ServerPlayer[] - self.gameFinished = false + self.alive_players = {} + self.game_finished = false + self.timeout = _room:getTimeout() end -- When this function returns, the Room(C++) thread stopped. function Room:run() for _, p in freekill.qlist(self.room:getPlayers()) do local player = ServerPlayer:new(p) + player.state = p:getStateString() table.insert(self.players, player) self.server.players[player:getId()] = player end @@ -32,8 +35,55 @@ function Room:notifyProperty(p, player, property) }) end -function Room:doBroadcastNotify(command, jsonData) - self.room:doBroadcastNotify(self.room:getPlayers(), command, jsonData) +function Room:doBroadcastNotify(command, jsonData, players) + players = players or self.players + local tolist = freekill.SPlayerList() + for _, p in ipairs(players) do + tolist:append(p.serverplayer) + end + self.room:doBroadcastNotify(tolist, command, jsonData) +end + +function Room:doRequest(player, command, jsonData, wait) + if wait == nil then wait = true end + player:doRequest(command, jsonData, self.timeout) + + if wait then + return player:waitForReply(self.timeout) + end +end + +function Room:doBroadcastRequest(command, players) + players = players or self.players + self:notifyMoveFocus(players, command) + for _, p in ipairs(players) do + self:doRequest(p, command, p.request_data, false) + end + + local remainTime = self.timeout + local currentTime = os.time() + local elapsed = 0 + for _, p in ipairs(players) do + elapsed = os.time() - currentTime + remainTime = remainTime - elapsed + p:waitForReply(remainTime) + end +end + +function Room:notifyMoveFocus(players, command) + if (players.class) then + players = {players} + end + + local ids = {} + for _, p in ipairs(players) do + table.insert(ids, p:getId()) + end + + self:doBroadcastNotify("MoveFocus", json.encode{ + ids, + command + }) end function Room:adjustSeats() @@ -64,8 +114,45 @@ function Room:adjustSeats() self:doBroadcastNotify("ArrangeSeats", json.encode(player_circle)) end +function Room:getLord() + local lord = self.players[1] + if lord.role == "lord" then return lord end + for _, p in ipairs(self.players) do + if p.role == "lord" then return p end + end + + return nil +end + +function Room:getOtherPlayers(expect) + local ret = {table.unpack(self.players)} + table.removeOne(ret, expect) + return ret +end + +function Room:askForGeneral(player, generals) + local command = "AskForGeneral" + self:notifyMoveFocus(player, command) + + if #generals == 1 then return generals[1] end + local defaultChoice = generals[1] + + if (player.state == "online") then + local result = self:doRequest(player, command, json.encode(generals)) + if result == "" then + return defaultChoice + else + -- TODO: result is a JSON array + -- update here when choose multiple generals + return json.decode(result)[1] + end + end + + return defaultChoice +end + function Room:gameOver() - self.gameFinished = true + self.game_finished = true -- dosomething self.room:gameOver() end diff --git a/lua/server/server.lua b/lua/server/server.lua index 180a6660..3c5abbce 100644 --- a/lua/server/server.lua +++ b/lua/server/server.lua @@ -1,9 +1,9 @@ Server = class('Server') -- load server classes -Room = require "server/room" -GameLogic = require "server/gamelogic" -ServerPlayer = require "server/serverplayer" +Room = require "server.room" +GameLogic = require "server.gamelogic" +ServerPlayer = require "server.serverplayer" freekill.server_callback = {} diff --git a/lua/server/serverplayer.lua b/lua/server/serverplayer.lua index 16c6a198..4590f79c 100644 --- a/lua/server/serverplayer.lua +++ b/lua/server/serverplayer.lua @@ -3,6 +3,14 @@ local ServerPlayer = Player:subclass("ServerPlayer") function ServerPlayer:initialize(_self) Player.initialize(self) self.serverplayer = _self + + self.next = nil + + -- Below are for doBroadcastRequest + self.request_data = "" + self.client_reply = "" + self.default_reply = "" + self.reply_ready = false end function ServerPlayer:getId() @@ -14,7 +22,23 @@ function ServerPlayer:doNotify(command, jsonData) end function ServerPlayer:doRequest(command, jsonData, timeout) + timeout = timeout or self.room.timeout + self.client_reply = "" + self.reply_ready = false self.serverplayer:doRequest(command, jsonData, timeout) end +function ServerPlayer:waitForReply(timeout) + local result = "" + if timeout == nil then + result = self.serverplayer:waitForReply() + else + result = self.serverplayer:waitForReply(timeout) + end + self.request_data = "" + self.client_reply = result + if result ~= "" then self.reply_ready = true end + return result +end + return ServerPlayer diff --git a/packages/standard/init.lua b/packages/standard/init.lua index 4624fe24..bfeb7ecb 100644 --- a/packages/standard/init.lua +++ b/packages/standard/init.lua @@ -1,5 +1,5 @@ local extension = Package:new("standard") -extension.metadata = require "standard/metadata" +extension.metadata = require "standard.metadata" Fk:loadTranslationTable{ ["wei"] = "魏", diff --git a/qml/Config.qml b/qml/Config.qml index fffc69bf..17d08bad 100644 --- a/qml/Config.qml +++ b/qml/Config.qml @@ -9,4 +9,5 @@ QtObject { // Client data property int roomCapacity: 0 + property int roomTimeout: 0 } diff --git a/qml/Logic.js b/qml/Logic.js index 959b5054..238e143e 100644 --- a/qml/Logic.js +++ b/qml/Logic.js @@ -29,7 +29,10 @@ callbacks["EnterLobby"] = function(jsonData) { } callbacks["EnterRoom"] = function(jsonData) { - config.roomCapacity = JSON.parse(jsonData)[0]; + // jsonData: int capacity, int timeout + let data = JSON.parse(jsonData); + config.roomCapacity = data[0]; + config.roomTimeout = data[1]; mainStack.push(room); mainWindow.busy = false; } diff --git a/qml/Pages/MetroButton.qml b/qml/Pages/MetroButton.qml new file mode 100644 index 00000000..78d6cc97 --- /dev/null +++ b/qml/Pages/MetroButton.qml @@ -0,0 +1,69 @@ +import QtQuick 2.15 + +Item { + property bool enabled: true + property alias text: title.text + property alias textColor: title.color + property alias textFont: title.font + property alias backgroundColor: bg.color + property alias border: bg.border + property alias iconSource: icon.source + property int padding: 5 + + signal clicked + + id: button + width: icon.width + title.implicitWidth + padding * 2 + height: Math.max(icon.height, title.implicitHeight) + padding * 2 + + Rectangle { + id: bg + anchors.fill: parent + color: "black" + border.width: 2 + border.color: "white" + opacity: 0.8 + } + + states: [ + State { + name: "hovered"; when: mouse.containsMouse + PropertyChanges { target: bg; color: "white" } + PropertyChanges { target: title; color: "black" } + }, + State { + name: "disabled"; when: !enabled + PropertyChanges { target: button; opacity: 0.2 } + } + ] + + MouseArea { + id: mouse + anchors.fill: parent + hoverEnabled: parent.enabled + onReleased: if (parent.enabled) parent.clicked() + } + + Row { + x: padding + y: padding + anchors.centerIn: parent + spacing: 5 + + Image { + id: icon + anchors.verticalCenter: parent.verticalCenter + fillMode: Image.PreserveAspectFit + } + + Text { + id: title + font.pixelSize: 18 + // font.family: "WenQuanYi Micro Hei" + anchors.verticalCenter: parent.verticalCenter + text: "" + color: "white" + } + } +} + diff --git a/qml/Pages/Room.qml b/qml/Pages/Room.qml index 1e7156e6..3bc290eb 100644 --- a/qml/Pages/Room.qml +++ b/qml/Pages/Room.qml @@ -13,11 +13,9 @@ Item { property bool isOwner: false property bool isStarted: false + property alias popupBox: popupBox + // tmp - Text { - anchors.centerIn: parent - text: "You are in room." - } Button { text: "quit" anchors.bottom: parent.bottom @@ -28,7 +26,7 @@ Item { } Button { text: "start game" - visible: isOwner && !isStarted + visible: dashboardModel.isOwner && !isStarted anchors.centerIn: parent } @@ -73,20 +71,20 @@ Item { id: photos model: photoModel Photo { - general: _general - screenName: _screenName - role: _role - kingdom: _kingdom - netstate: _netstate - maxHp: _maxHp - hp: _hp - seatNumber: _seatNumber - isDead: _isDead - dying: _dying - faceturned: _faceturned - chained: _chained - drank: _drank - isOwner: _isOwner + general: model.general + screenName: model.screenName + role: model.role + kingdom: model.kingdom + netstate: model.netstate + maxHp: model.maxHp + hp: model.hp + seatNumber: model.seatNumber + isDead: model.isDead + dying: model.dying + faceturned: model.faceturned + chained: model.chained + drank: model.drank + isOwner: model.isOwner } } @@ -129,6 +127,30 @@ Item { self.isOwner: dashboardModel.isOwner } + Loader { + id: popupBox + onSourceChanged: { + if (item === null) + return; + item.finished.connect(function(){ + source = ""; + }); + item.widthChanged.connect(function(){ + popupBox.moveToCenter(); + }); + item.heightChanged.connect(function(){ + popupBox.moveToCenter(); + }); + moveToCenter(); + } + + function moveToCenter() + { + item.x = Math.round((roomArea.width - item.width) / 2); + item.y = Math.round(roomArea.height * 0.67 - item.height / 2); + } + } + Component.onCompleted: { toast.show("Sucesessfully entered room."); @@ -157,20 +179,20 @@ Item { photoModel.append({ id: -1, index: i - 1, // For animating seat swap - _general: "", - _screenName: "", - _role: "unknown", - _kingdom: "qun", - _netstate: "online", - _maxHp: 0, - _hp: 0, - _seatNumber: i + 1, - _isDead: false, - _dying: false, - _faceturned: false, - _chained: false, - _drank: false, - _isOwner: false + general: "", + screenName: "", + role: "unknown", + kingdom: "qun", + netstate: "online", + maxHp: 0, + hp: 0, + seatNumber: i + 1, + isDead: false, + dying: false, + faceturned: false, + chained: false, + drank: false, + isOwner: false }); } diff --git a/qml/Pages/RoomElement/CardArea.qml b/qml/Pages/RoomElement/CardArea.qml index 3ccf334d..c2ce9ae7 100644 --- a/qml/Pages/RoomElement/CardArea.qml +++ b/qml/Pages/RoomElement/CardArea.qml @@ -20,9 +20,9 @@ Item { function remove(outputs) { - var result = []; - for (var i = 0; i < cards.length; i++) { - for (var j = 0; j < outputs.length; j++) { + let result = []; + for (let i = 0; i < cards.length; i++) { + for (let j = 0; j < outputs.length; j++) { if (outputs[j] === cards[i].cid) { result.push(cards[i]); cards.splice(i, 1); @@ -37,9 +37,9 @@ Item { function updateCardPosition(animated) { - var i, card; + let i, card; - var overflow = false; + let overflow = false; for (i = 0; i < cards.length; i++) { card = cards[i]; card.origX = i * card.width; @@ -52,8 +52,8 @@ Item { if (overflow) { // TODO: Adjust cards in multiple lines if there are too many cards - var xLimit = root.width - card.width; - var spacing = xLimit / (cards.length - 1); + let xLimit = root.width - card.width; + let spacing = xLimit / (cards.length - 1); for (i = 0; i < cards.length; i++) { card = cards[i]; card.origX = i * spacing; @@ -61,7 +61,7 @@ Item { } } - var parentPos = roomScene.mapFromItem(root, 0, 0); + let parentPos = roomScene.mapFromItem(root, 0, 0); for (i = 0; i < cards.length; i++) { card = cards[i]; card.origX += parentPos.x; diff --git a/qml/Pages/RoomElement/CardItem.qml b/qml/Pages/RoomElement/CardItem.qml index 9e867518..0e4a2951 100644 --- a/qml/Pages/RoomElement/CardItem.qml +++ b/qml/Pages/RoomElement/CardItem.qml @@ -26,6 +26,7 @@ Item { property bool footnoteVisible: true 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 function getColor() { @@ -38,9 +39,12 @@ Item { property int cid: 0 property bool selectable: true property bool selected: false + property bool draggable: false + property bool autoBack: true property int origX: 0 property int origY: 0 - property real origOpacity: 0 + property real origOpacity: 1 + property bool isClicked: false property bool moveAborted: false property alias goBackAnim: goBackAnimation property int goBackDuration: 500 @@ -71,6 +75,7 @@ Item { source: known ? (name != "" ? SkinBank.CARD_DIR + name : "") : (SkinBank.CARD_DIR + "card-back") anchors.fill: parent + fillMode: Image.PreserveAspectCrop } Image { @@ -97,7 +102,7 @@ Item { Image { id: colorItem visible: known && suit == "" - source: visible ? SkinBank.CARD_SUIT_DIR + "/" + color : "" + source: (visible && color != "") ? SkinBank.CARD_SUIT_DIR + "/" + color : "" x: 1 } @@ -126,6 +131,41 @@ Item { opacity: 0.7 } + MouseArea { + anchors.fill: parent + drag.target: draggable ? parent : undefined + drag.axis: Drag.XAndYAxis + hoverEnabled: true + + onReleased: { + root.isClicked = mouse.isClick; + parent.released(); + if (autoBack) + goBackAnimation.start(); + } + + onEntered: { + parent.entered(); + if (draggable) { + glow.visible = true; + root.z++; + } + } + + onExited: { + parent.exited(); + if (draggable) { + glow.visible = false; + root.z--; + } + } + + onClicked: { + selected = selectable ? !selected : false; + parent.clicked(); + } + } + ParallelAnimation { id: goBackAnimation @@ -180,7 +220,7 @@ Item { function toData() { - var data = { + let data = { cid: cid, name: name, suit: suit, diff --git a/qml/Pages/RoomElement/ChooseGeneralBox.qml b/qml/Pages/RoomElement/ChooseGeneralBox.qml new file mode 100644 index 00000000..dd906ad9 --- /dev/null +++ b/qml/Pages/RoomElement/ChooseGeneralBox.qml @@ -0,0 +1,177 @@ +import QtQuick 2.15 +import ".." +import "../skin-bank.js" as SkinBank + +GraphicsBox { + property alias generalList: generalList + // property var generalList: [] + property int choiceNum: 1 + property var choices: [] + property var selectedItem: [] + property bool loaded: false + + ListModel { + id: generalList + } + + id: root + title.text: qsTr("Please choose ") + choiceNum + qsTr(" general(s)") + width: generalArea.width + body.anchors.leftMargin + body.anchors.rightMargin + height: body.implicitHeight + body.anchors.topMargin + body.anchors.bottomMargin + + Column { + id: body + anchors.fill: parent + anchors.margins: 40 + anchors.bottomMargin: 20 + + Item { + id: generalArea + width: (generalList.count >= 5 ? Math.ceil(generalList.count / 2) : Math.max(3, generalList.count)) * 97 + height: generalList.count >= 5 ? 290 : 150 + z: 1 + + Repeater { + id: generalMagnetList + model: generalList.count + + Item { + width: 93 + height: 130 + x: (index % Math.ceil(generalList.count / (generalList.count >= 5 ? 2 : 1))) * 98 + (generalList.count >= 5 && index > generalList.count / 2 && generalList.count % 2 == 1 ? 50 : 0) + y: generalList.count < 5 ? 0 : (index < generalList.count / 2 ? 0 : 135) + } + } + } + + Item { + id: splitLine + width: parent.width - 80 + height: 6 + anchors.horizontalCenter: parent.horizontalCenter + clip: true + } + + Item { + width: parent.width + height: 165 + + Row { + id: resultArea + anchors.centerIn: parent + spacing: 10 + + Repeater { + id: resultList + model: choiceNum + + Rectangle { + color: "#1D1E19" + radius: 3 + width: 93 + height: 130 + } + } + } + } + + Item { + id: buttonArea + width: parent.width + height: 40 + + MetroButton { + id: fightButton + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + text: qsTr("Fight") + width: 120 + height: 35 + enabled: false + + onClicked: close(); + } + } + } + + Repeater { + id: generalCardList + model: generalList + + GeneralCardItem { + name: model.name + selectable: true + draggable: true + + onClicked: { + let toSelect = true; + for (let i = 0; i < selectedItem.length; i++) { + if (selectedItem[i] === this) { + toSelect = false; + selectedItem.splice(i, 1); + } + } + if (toSelect && selectedItem.length < choiceNum) + selectedItem.push(this); + updatePosition(); + } + + onReleased: { + if (!isClicked) + arrangeCards(); + } + } + } + + function arrangeCards() + { + let item, i; + + selectedItem = []; + for (i = 0; i < generalList.count; i++) { + item = generalCardList.itemAt(i); + if (item.y > splitLine.y) + selectedItem.push(item); + } + + selectedItem.sort((a, b) => a.x - b.x); + + if (selectedItem.length > choiceNum) + selectedItem.splice(choiceNum, selectedItem.length - choiceNum); + + updatePosition(); + } + + function updatePosition() + { + choices = []; + let item, magnet, pos, i; + for (i = 0; i < selectedItem.length && i < resultList.count; i++) { + item = selectedItem[i]; + choices.push(item.name); + magnet = resultList.itemAt(i); + pos = root.mapFromItem(resultArea, magnet.x, magnet.y); + if (item.origX !== pos.x || item.origY !== item.y) { + item.origX = pos.x; + item.origY = pos.y; + item.goBack(true); + } + } + + fightButton.enabled = (choices.length == choiceNum); + + for (i = 0; i < generalCardList.count; i++) { + item = generalCardList.itemAt(i); + if (selectedItem.indexOf(item) != -1) + continue; + + magnet = generalMagnetList.itemAt(i); + pos = root.mapFromItem(generalMagnetList.parent, magnet.x, magnet.y); + if (item.origX !== pos.x || item.origY !== item.y) { + item.origX = pos.x; + item.origY = pos.y; + item.goBack(true); + } + } + } +} diff --git a/qml/Pages/RoomElement/GeneralCardItem.qml b/qml/Pages/RoomElement/GeneralCardItem.qml new file mode 100644 index 00000000..b5aeb10f --- /dev/null +++ b/qml/Pages/RoomElement/GeneralCardItem.qml @@ -0,0 +1,24 @@ +import QtQuick 2.15 +import "../skin-bank.js" as SkinBank + +/* Layout of general card: + * +--------+ + *kindom|wu 9999| <- hp + *name -|s | + * |q img | + * | | + * | | + * +--------+ + * Inherit from CardItem to use common signal + */ + +CardItem { + property string kingdom: "qun" + name: "caocao" + // description: Sanguosha.getGeneralDescription(name) + suit: "" + number: 0 + footnote: "" + card.source: SkinBank.GENERAL_DIR + name + glow.color: "white" //Engine.kingdomColor[kingdom] +} diff --git a/qml/Pages/RoomElement/GraphicsBox.qml b/qml/Pages/RoomElement/GraphicsBox.qml new file mode 100644 index 00000000..afb479b3 --- /dev/null +++ b/qml/Pages/RoomElement/GraphicsBox.qml @@ -0,0 +1,53 @@ +import QtQuick 2.15 +import QtGraphicalEffects 1.0 + +Item { + property alias title: titleItem + signal accepted() //Read result + signal finished() //Close the box + + id: root + + Rectangle { + id: background + anchors.fill: parent + color: "#B0000000" + radius: 5 + border.color: "#A6967A" + border.width: 1 + } + + DropShadow { + source: background + anchors.fill: background + color: "#B0000000" + radius: 5 + samples: 12 + spread: 0.2 + horizontalOffset: 5 + verticalOffset: 4 + transparentBorder: true + } + + Text { + id: titleItem + color: "#E4D5A0" + font.pixelSize: 18 + horizontalAlignment: Text.AlignHCenter + anchors.top: parent.top + anchors.topMargin: 4 + anchors.horizontalCenter: parent.horizontalCenter + } + + MouseArea { + anchors.fill: parent + drag.target: parent + drag.axis: Drag.XAndYAxis + } + + function close() + { + accepted(); + finished(); + } +} diff --git a/qml/Pages/RoomElement/HandcardArea.qml b/qml/Pages/RoomElement/HandcardArea.qml index 09c60c88..c9744f93 100644 --- a/qml/Pages/RoomElement/HandcardArea.qml +++ b/qml/Pages/RoomElement/HandcardArea.qml @@ -19,7 +19,7 @@ Item { { cardArea.add(inputs); if (inputs instanceof Array) { - for (var i = 0; i < inputs.length; i++) + for (let i = 0; i < inputs.length; i++) filterInputCard(inputs[i]); } else { filterInputCard(inputs); @@ -36,9 +36,9 @@ Item { function remove(outputs) { - var result = cardArea.remove(outputs); - var card; - for (var i = 0; i < result.length; i++) { + let result = cardArea.remove(outputs); + let card; + for (let i = 0; i < result.length; i++) { card = result[i]; card.draggable = false; card.selectable = false; @@ -49,7 +49,7 @@ Item { function enableCards(cardIds) { - var card, i; + let card, i; for (i = 0; i < cards.length; i++) { card = cards[i]; card.selectable = cardIds.contains(card.cid); @@ -64,7 +64,7 @@ Item { { cardArea.updateCardPosition(false); - var i, card; + let i, card; for (i = 0; i < cards.length; i++) { card = cards[i]; if (card.selected) @@ -81,8 +81,8 @@ Item { { area.updateCardPosition(true); - for (var i = 0; i < cards.length; i++) { - var card = cards[i]; + for (let i = 0; i < cards.length; i++) { + let card = cards[i]; if (card.selected) { if (!selectedCards.contains(card)) selectCard(card); @@ -101,7 +101,7 @@ Item { function unselectCard(card) { - for (var i = 0; i < selectedCards.length; i++) { + for (let i = 0; i < selectedCards.length; i++) { if (selectedCards[i] === card) { selectedCards.splice(i, 1); cardSelected(card.cid, false); @@ -112,7 +112,7 @@ Item { function unselectAll(exceptId) { let card = undefined; - for (var i = 0; i < selectedCards.length; i++) { + for (let i = 0; i < selectedCards.length; i++) { if (selectedCards[i].cid !== exceptId) { selectedCards[i].selected = false; } else { diff --git a/qml/Pages/RoomElement/InvisibleCardArea.qml b/qml/Pages/RoomElement/InvisibleCardArea.qml index bb2c17b8..0b329b49 100644 --- a/qml/Pages/RoomElement/InvisibleCardArea.qml +++ b/qml/Pages/RoomElement/InvisibleCardArea.qml @@ -10,9 +10,9 @@ Item { function add(inputs) { - var card; + let card; if (inputs instanceof Array) { - for (var i = 0; i < inputs.length; i++) { + for (let i = 0; i < inputs.length; i++) { card = inputs[i]; pendingInput.push(card); cards.push(card.toData()); @@ -38,7 +38,7 @@ Item { if (!checkExisting) return true; - for (var i = 0; i < cards.length; i++) + for (let i = 0; i < cards.length; i++) { if (cards[i].cid === cid) return true; @@ -48,13 +48,13 @@ Item { function remove(outputs) { - var component = Qt.createComponent("CardItem.qml"); + let component = Qt.createComponent("CardItem.qml"); if (component.status !== Component.Ready) return []; - var parentPos = roomScene.mapFromItem(root, 0, 0); - var card; - var items = []; + let parentPos = roomScene.mapFromItem(root, 0, 0); + let card; + let items = []; for (let i = 0; i < outputs.length; i++) { if (_contains(outputs[i])) { let state = JSON.parse(Sanguosha.getCard4Qml(outputs[i])) @@ -82,10 +82,10 @@ Item { function updateCardPosition(animated) { - var i, card; + let i, card; if (animated) { - var parentPos = roomScene.mapFromItem(root, 0, 0); + let parentPos = roomScene.mapFromItem(root, 0, 0); for (i = 0; i < pendingInput.length; i++) { card = pendingInput[i]; card.origX = parentPos.x - card.width / 2 + ((i - pendingInput.length / 2) * 15); diff --git a/qml/Pages/RoomElement/Photo.qml b/qml/Pages/RoomElement/Photo.qml index 58aa8700..9744a8de 100644 --- a/qml/Pages/RoomElement/Photo.qml +++ b/qml/Pages/RoomElement/Photo.qml @@ -1,5 +1,6 @@ import QtQuick 2.15 import QtGraphicalEffects 1.15 +import QtQuick.Controls 2.15 import "PhotoElement" import "../skin-bank.js" as SkinBank @@ -24,6 +25,9 @@ Item { property bool drank: false property bool isOwner: false + property alias progressBar: progressBar + property alias progressTip: progressTip.text + Behavior on x { NumberAnimation { duration: 600; easing.type: Easing.InOutQuad } } @@ -95,6 +99,15 @@ Item { visible: root.isDead } + Image { + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.bottomMargin: 8 + anchors.rightMargin: 4 + source: SkinBank.PHOTO_DIR + (isOwner ? "owner" : "ready") + visible: screenName != "" && !roomScene.isStarted + } + Image { id: turnedOver visible: root.faceturned @@ -155,6 +168,7 @@ Item { GlowText { id: seatNum + visible: !progressBar.visible anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: -32 @@ -191,4 +205,42 @@ Item { function tremble() { trembleAnimation.start() } + + ProgressBar { + id: progressBar + width: parent.width + height: 4 + anchors.bottom: parent.bottom + anchors.bottomMargin: -4 + from: 0.0 + to: 100.0 + + visible: false + NumberAnimation on value { + running: progressBar.visible + from: 100.0 + to: 0.0 + duration: config.roomTimeout * 1000 + + onFinished: { + progressBar.visible = false; + root.progressTip = ""; + } + } + } + + Image { + anchors.top: progressBar.bottom + anchors.topMargin: 1 + source: SkinBank.PHOTO_DIR + "control/tip" + visible: progressTip.text != "" + Text { + id: progressTip + font.family: "FZLiBian-S02" + font.pixelSize: 18 + x: 18 + color: "white" + text: "" + } + } } diff --git a/qml/Pages/RoomElement/TablePile.qml b/qml/Pages/RoomElement/TablePile.qml index 684ba4c1..6c301b15 100644 --- a/qml/Pages/RoomElement/TablePile.qml +++ b/qml/Pages/RoomElement/TablePile.qml @@ -22,7 +22,7 @@ Item { running: true triggeredOnStart: true onTriggered: { - var i, card; + let i, card; if (toVanish) { for (i = 0; i < discardedCards.length; i++) { card = discardedCards[i]; @@ -60,13 +60,13 @@ Item { function remove(outputs) { - var i, j; + let i, j; - var result = area.remove(outputs); - var vanished = []; + let result = area.remove(outputs); + let vanished = []; if (result.length < outputs.length) { for (i = 0; i < outputs.length; i++) { - var exists = false; + let exists = false; for (j = 0; j < result.length; j++) { if (result[j].cid === outputs[i]) { exists = true; @@ -96,9 +96,9 @@ Item { if (cards.length <= 0) return; - var i, card; + let i, card; - var overflow = false; + let overflow = false; for (i = 0; i < cards.length; i++) { card = cards[i]; card.homeX = i * card.width; @@ -111,8 +111,8 @@ Item { if (overflow) { //@to-do: Adjust cards in multiple lines if there are too many cards - var xLimit = root.width - card.width; - var spacing = xLimit / (cards.length - 1); + let xLimit = root.width - card.width; + let spacing = xLimit / (cards.length - 1); for (i = 0; i < cards.length; i++) { card = cards[i]; card.homeX = i * spacing; @@ -120,8 +120,8 @@ Item { } } - var offsetX = Math.max(0, (root.width - cards.length * card.width) / 2); - var parentPos = roomScene.mapFromItem(root, 0, 0); + let offsetX = Math.max(0, (root.width - cards.length * card.width) / 2); + let parentPos = roomScene.mapFromItem(root, 0, 0); for (i = 0; i < cards.length; i++) { card = cards[i]; card.homeX += parentPos.x + offsetX; diff --git a/qml/Pages/RoomLogic.js b/qml/Pages/RoomLogic.js index 8c41ba6d..587f08cd 100644 --- a/qml/Pages/RoomLogic.js +++ b/qml/Pages/RoomLogic.js @@ -7,14 +7,14 @@ function arrangePhotos() { * +---------------+ */ - var photoWidth = 175; - var roomAreaPadding = 10; - var verticalPadding = Math.max(10, roomArea.width * 0.01); - var horizontalSpacing = Math.max(30, roomArea.height * 0.1); - var verticalSpacing = (roomArea.width - photoWidth * 7 - verticalPadding * 2) / 6; + const photoWidth = 175; + const roomAreaPadding = 10; + let verticalPadding = Math.max(10, roomArea.width * 0.01); + let horizontalSpacing = Math.max(30, roomArea.height * 0.1); + let verticalSpacing = (roomArea.width - photoWidth * 7 - verticalPadding * 2) / 6; // Position 1-7 - var regions = [ + const regions = [ { x: verticalPadding + (photoWidth + verticalSpacing) * 6, y: roomAreaPadding + horizontalSpacing * 2 }, { x: verticalPadding + (photoWidth + verticalSpacing) * 5, y: roomAreaPadding + horizontalSpacing }, { x: verticalPadding + (photoWidth + verticalSpacing) * 4, y: roomAreaPadding }, @@ -24,7 +24,7 @@ function arrangePhotos() { { x: verticalPadding, y: roomAreaPadding + horizontalSpacing * 2 }, ]; - var regularSeatIndex = [ + const regularSeatIndex = [ [4], [3, 5], [1, 4, 7], @@ -33,9 +33,9 @@ function arrangePhotos() { [1, 2, 3, 5, 6, 7], [1, 2, 3, 4, 5, 6, 7], ]; - var seatIndex = regularSeatIndex[playerNum - 2]; + let seatIndex = regularSeatIndex[playerNum - 2]; - var item, region, i; + let item, region, i; for (i = 0; i < playerNum - 1; i++) { item = photos.itemAt(i); @@ -58,9 +58,8 @@ callbacks["AddPlayer"] = function(jsonData) { let name = data[1]; let avatar = data[2]; item.id = uid; - item._screenName = name; - item._general = avatar; - photos.itemAt(i).tremble(); + item.screenName = name; + item.general = avatar; return; } } @@ -73,19 +72,31 @@ callbacks["RemovePlayer"] = function(jsonData) { let item = photoModel.get(i); if (item.id === uid) { item.id = -1; - item._screenName = ""; - item._general = ""; + item.screenName = ""; + item.general = ""; return; } } } -/* callbacks["RoomOwner"] = function(jsonData) { // jsonData: int uid of the owner - toast.show(J) + let uid = JSON.parse(jsonData)[0]; + + if (dashboardModel.id === uid) { + dashboardModel.isOwner = true; + roomScene.dashboardModelChanged(); + return; + } + + for (let i = 0; i < photoModel.count; i++) { + let item = photoModel.get(i); + if (item.id === uid) { + item.isOwner = true; + return; + } + } } -*/ callbacks["PropertyUpdate"] = function(jsonData) { // jsonData: int id, string property_name, value @@ -103,7 +114,7 @@ callbacks["PropertyUpdate"] = function(jsonData) { for (let i = 0; i < photoModel.count; i++) { let item = photoModel.get(i); if (item.id === uid) { - item["_" + property_name] = value; + item[property_name] = value; return; } } @@ -112,10 +123,11 @@ callbacks["PropertyUpdate"] = function(jsonData) { callbacks["ArrangeSeats"] = function(jsonData) { // jsonData: seat order let order = JSON.parse(jsonData); + roomScene.isStarted = true; for (let i = 0; i < photoModel.count; i++) { let item = photoModel.get(i); - item._seatNumber = order.indexOf(item.id) + 1; + item.seatNumber = order.indexOf(item.id) + 1; } dashboardModel.seatNumber = order.indexOf(Self.id) + 1; @@ -134,3 +146,45 @@ callbacks["ArrangeSeats"] = function(jsonData) { arrangePhotos(); } + +function cancelAllFocus() { + let item; + for (let i = 0; i < playerNum - 1; i++) { + item = photos.itemAt(i); + item.progressBar.visible = false; + item.progressTip = ""; + } +} + +callbacks["MoveFocus"] = function(jsonData) { + // jsonData: int[] focuses, string command + cancelAllFocus(); + let data = JSON.parse(jsonData); + let focuses = data[0]; + let command = data[1]; + + let item, model; + for (let i = 0; i < playerNum - 1; i++) { + model = photoModel.get(i); + if (focuses.indexOf(model.id) != -1) { + item = photos.itemAt(i); + item.progressBar.visible = true; + item.progressTip = command + " thinking..."; + } + } +} + +callbacks["AskForGeneral"] = function(jsonData) { + // jsonData: string[] Generals + // TODO: choose multiple generals + let data = JSON.parse(jsonData); + roomScene.popupBox.source = "RoomElement/ChooseGeneralBox.qml"; + let box = roomScene.popupBox.item; + box.choiceNum = 1; + box.accepted.connect(() => { + ClientInstance.replyToServer("AskForGeneral", JSON.stringify([box.choices[0]])); + }); + for (let i = 0; i < data.length; i++) + box.generalList.append({ "name": data[i] }); + box.updatePosition(); +} diff --git a/src/main.cpp b/src/main.cpp index 818b5e5c..db24b097 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,32 +3,39 @@ int main(int argc, char *argv[]) { - QGuiApplication app(argc, argv); - QGuiApplication::setApplicationName("FreeKill"); - QGuiApplication::setApplicationVersion("Alpha 0.0.1"); + QCoreApplication *app; + QCoreApplication::setApplicationName("FreeKill"); + QCoreApplication::setApplicationVersion("Alpha 0.0.1"); QCommandLineParser parser; parser.setApplicationDescription("FreeKill server"); parser.addHelpOption(); parser.addVersionOption(); parser.addOption({{"s", "server"}, "start server at ", "port"}); - parser.process(app); + QStringList cliOptions; + for (int i = 0; i < argc; i++) + cliOptions << argv[i]; + + parser.parse(cliOptions); bool startServer = parser.isSet("server"); ushort serverPort = 9527; if (startServer) { + app = new QCoreApplication(argc, argv); bool ok = false; if (parser.value("server").toInt(&ok) && ok) serverPort = parser.value("server").toInt(); Server *server = new Server; if (!server->listen(QHostAddress::Any, serverPort)) { fprintf(stderr, "cannot listen on port %d!\n", serverPort); - app.exit(1); + app->exit(1); } - return app.exec(); + return app->exec(); } + app = new QGuiApplication(argc, argv); + QQmlApplicationEngine *engine = new QQmlApplicationEngine; QmlBackend backend; @@ -44,7 +51,7 @@ int main(int argc, char *argv[]) engine->rootContext()->setContextProperty("Debugging", debugging); engine->load("qml/main.qml"); - int ret = app.exec(); + int ret = app->exec(); // delete the engine first // to avoid "TypeError: Cannot read property 'xxx' of null" diff --git a/src/network/router.cpp b/src/network/router.cpp index c0aa2697..7788b1b6 100644 --- a/src/network/router.cpp +++ b/src/network/router.cpp @@ -60,7 +60,7 @@ void Router::request(int type, const QString& command, replyMutex.lock(); expectedReplyId = requestId; - replyTimeout = 0; + replyTimeout = timeout; requestStartTime = QDateTime::currentDateTime(); m_reply = QString(); replyMutex.unlock(); @@ -123,7 +123,7 @@ QString Router::waitForReply() QString Router::waitForReply(int timeout) { - replyReadySemaphore.tryAcquire(1, timeout); + replyReadySemaphore.tryAcquire(1, timeout * 1000); return m_reply; } @@ -188,7 +188,6 @@ void Router::handlePacket(const QByteArray& rawPacket) m_reply = jsonData; // TODO: callback? - qDebug() << rawPacket << Qt::endl; replyReadySemaphore.release(); if (extraReplyReadySemaphore) { diff --git a/src/server/room.cpp b/src/server/room.cpp index 61eb4a4c..9b8f4d06 100644 --- a/src/server/room.cpp +++ b/src/server/room.cpp @@ -8,7 +8,9 @@ Room::Room(Server* server) server->nextRoomId++; this->server = server; setParent(server); + owner = nullptr; gameStarted = false; + timeout = 15; if (!isLobby()) { connect(this, &Room::playerAdded, server->lobby(), &Room::removePlayer); connect(this, &Room::playerRemoved, server->lobby(), &Room::addPlayer); @@ -76,7 +78,7 @@ void Room::setOwner(ServerPlayer *owner) this->owner = owner; QJsonArray jsonData; jsonData << owner->getId(); - owner->doNotify("RoomOwner", QJsonDocument(jsonData).toJson()); + doBroadcastNotify(players, "RoomOwner", QJsonDocument(jsonData).toJson()); } void Room::addPlayer(ServerPlayer *player) @@ -101,6 +103,7 @@ void Room::addPlayer(ServerPlayer *player) // Second, let the player enter room and add other players jsonData = QJsonArray(); jsonData << this->capacity; + jsonData << this->timeout; player->doNotify("EnterRoom", QJsonDocument(jsonData).toJson()); foreach (ServerPlayer *p, getOtherPlayers(player)) { @@ -111,6 +114,12 @@ void Room::addPlayer(ServerPlayer *player) player->doNotify("AddPlayer", QJsonDocument(jsonData).toJson()); } + if (this->owner != nullptr) { + jsonData = QJsonArray(); + jsonData << this->owner->getId(); + player->doNotify("RoomOwner", QJsonDocument(jsonData).toJson()); + } + if (isFull()) start(); } @@ -150,11 +159,17 @@ QList Room::getOtherPlayers(ServerPlayer* expect) const ServerPlayer *Room::findPlayer(int id) const { - foreach (ServerPlayer *p, players) { - if (p->getId() == id) - return p; - } - return nullptr; + return server->findPlayer(id); +} + +int Room::getTimeout() const +{ + return timeout; +} + +void Room::setTimeout(int timeout) +{ + this->timeout = timeout; } bool Room::isStarted() const diff --git a/src/server/room.h b/src/server/room.h index 379fb268..26ae735d 100644 --- a/src/server/room.h +++ b/src/server/room.h @@ -31,6 +31,9 @@ public: QList getOtherPlayers(ServerPlayer *expect) const; ServerPlayer *findPlayer(int id) const; + int getTimeout() const; + void setTimeout(int timeout); + bool isStarted() const; // ====================================} @@ -64,6 +67,8 @@ private: ServerPlayer *owner; // who created this room? QList players; bool gameStarted; + + int timeout; }; #endif // _ROOM_H diff --git a/src/server/serverplayer.cpp b/src/server/serverplayer.cpp index b6a85947..d124764f 100644 --- a/src/server/serverplayer.cpp +++ b/src/server/serverplayer.cpp @@ -8,7 +8,7 @@ ServerPlayer::ServerPlayer(Room *room) { socket = nullptr; router = new Router(this, socket, Router::TYPE_SERVER); - + setState(Player::Online); this->room = room; server = room->getServer(); } @@ -68,6 +68,16 @@ void ServerPlayer::doRequest(const QString& command, const QString& jsonData, in router->request(type, command, jsonData, timeout); } +QString ServerPlayer::waitForReply() +{ + return router->waitForReply(); +} + +QString ServerPlayer::waitForReply(int timeout) +{ + return router->waitForReply(timeout); +} + void ServerPlayer::doNotify(const QString& command, const QString& jsonData) { int type = Router::TYPE_NOTIFICATION | Router::SRC_SERVER | Router::DEST_CLIENT; diff --git a/src/server/serverplayer.h b/src/server/serverplayer.h index b0bbdebc..dba5c913 100644 --- a/src/server/serverplayer.h +++ b/src/server/serverplayer.h @@ -24,6 +24,8 @@ public: void doRequest(const QString &command, const QString &jsonData, int timeout = -1); + QString waitForReply(int timeout); + QString waitForReply(); void doNotify(const QString &command, const QString &jsonData); void prepareForRequest(const QString &command, diff --git a/src/swig/player.i b/src/swig/player.i index 6ef1c59f..f9b6841e 100644 --- a/src/swig/player.i +++ b/src/swig/player.i @@ -46,7 +46,9 @@ public: void speak(const QString &message); void doRequest(const QString &command, - const QString &json_data, int timeout = -1); + const QString &json_data, int timeout); + QString waitForReply(); + QString waitForReply(int timeout); void doNotify(const QString &command, const QString &json_data); void prepareForRequest(const QString &command, const QString &data); diff --git a/src/swig/qt.i b/src/swig/qt.i index 1d32f4ef..d8968898 100644 --- a/src/swig/qt.i +++ b/src/swig/qt.i @@ -34,3 +34,15 @@ public: %template(PlayerList) QList; %template(IntList) QList; %template(BoolList) QList; + +%native(GetMicroSecond) int GetMicroSecond(lua_State *L); +%{ +#include +static int GetMicroSecond(lua_State *L) { + struct timeval tv; + gettimeofday(&tv, nullptr); + long microsecond = tv.tv_sec * 1000000 + tv.tv_usec; + lua_pushnumber(L, microsecond); + return 1; +} +%} diff --git a/src/swig/server.i b/src/swig/server.i index a028442b..e62e6438 100644 --- a/src/swig/server.i +++ b/src/swig/server.i @@ -71,6 +71,8 @@ public: QList getPlayers() const; ServerPlayer *findPlayer(int id) const; + int getTimeout() const; + bool isStarted() const; // ====================================}