From dedde9464309b4595aa919c0476c6998cd59d75a Mon Sep 17 00:00:00 2001 From: notify Date: Sat, 30 Apr 2022 15:27:56 +0800 Subject: [PATCH] Use card (#19) * the process of using card (uncompleted) * code style: tab is 2 spaces(not \t or 4 space) * update lua54.dll to MinGW version(no cygwin1.dll required) * basic ui logic * ActiveSkill * modidy ActiveSkill defaults * todo: defaultEquipSkill * client * send use card to server * playing phase, equip Co-authored-by: Ho-spair --- CMakeLists.txt | 8 +- .../{double_sword.png => double_swords.png} | Bin .../{renwang_shield.png => nioh_shield.png} | Bin lib/win/lua54.dll | Bin 230180 -> 283828 bytes lua/client/client.lua | 180 +-- lua/client/client_util.lua | 165 ++- lua/client/clientplayer.lua | 6 +- lua/core/card.lua | 66 +- lua/core/card_type/basic.lua | 9 +- lua/core/card_type/equip.lua | 53 +- lua/core/card_type/skill.lua | 9 - lua/core/card_type/trick.lua | 20 +- lua/core/debug.lua | 20 +- lua/core/engine.lua | 224 +-- lua/core/general.lua | 26 +- lua/core/package.lua | 48 +- lua/core/player.lua | 236 ++-- lua/core/skill.lua | 8 +- lua/core/skill_type/active_skill.lua | 57 + lua/core/skill_type/trigger.lua | 22 +- lua/core/util.lua | 311 +++-- lua/fk_ex.lua | 286 ++-- lua/freekill.lua | 3 +- lua/server/event.lua | 22 +- lua/server/gamelogic.lua | 428 +++--- lua/server/lobby.lua | 82 +- lua/server/room.lua | 1225 ++++++++++------- lua/server/serverplayer.lua | 246 ++-- lua/server/system_enum.lua | 5 + packages/standard/game_rule.lua | 235 ++-- packages/standard/init.lua | 60 +- packages/standard/metadata.lua | 22 +- packages/standard_cards/init.lua | 487 +++---- packages/standard_cards/metadata.lua | 22 +- qml/Config.qml | 14 +- qml/GlobalPopups/CreateRoom.qml | 106 +- qml/GlobalPopups/EditProfile.qml | 178 +-- qml/GlobalPopups/Test.qml | 2 +- qml/Logic.js | 66 +- qml/Pages/CardsOverview.qml | 72 +- qml/Pages/GeneralsOverview.qml | 74 +- qml/Pages/Init.qml | 86 +- qml/Pages/Lobby.qml | 276 ++-- qml/Pages/Logic.js | 16 +- qml/Pages/MetroButton.qml | 116 +- qml/Pages/Room.qml | 628 +++++---- qml/Pages/RoomElement/CardArea.qml | 124 +- qml/Pages/RoomElement/CardItem.qml | 440 +++--- qml/Pages/RoomElement/ChoiceBox.qml | 44 +- qml/Pages/RoomElement/ChooseGeneralBox.qml | 302 ++-- qml/Pages/RoomElement/Dashboard.qml | 179 ++- qml/Pages/RoomElement/GeneralCardItem.qml | 16 +- qml/Pages/RoomElement/GlowText.qml | 44 +- qml/Pages/RoomElement/GraphicsBox.qml | 84 +- qml/Pages/RoomElement/HandcardArea.qml | 226 +-- qml/Pages/RoomElement/IndicatorLine.qml | 176 +-- qml/Pages/RoomElement/InvisibleCardArea.qml | 198 +-- qml/Pages/RoomElement/Photo.qml | 628 +++++---- .../PhotoElement/DelayedTrickArea.qml | 98 +- .../RoomElement/PhotoElement/EquipArea.qml | 174 +-- .../RoomElement/PhotoElement/EquipItem.qml | 244 ++-- qml/Pages/RoomElement/PhotoElement/HpBar.qml | 122 +- .../RoomElement/PhotoElement/Magatama.qml | 96 +- .../RoomElement/PhotoElement/RoleComboBox.qml | 66 +- qml/Pages/RoomElement/PixmapAnimation.qml | 140 +- qml/Pages/RoomElement/SkillArea.qml | 2 +- qml/Pages/RoomElement/TablePile.qml | 242 ++-- qml/Pages/RoomLogic.js | 638 +++++---- qml/Toast.qml | 94 +- qml/ToastManager.qml | 46 +- qml/main.qml | 198 +-- qml/util.js | 22 +- server/init.sql | 14 +- src/CMakeLists.txt | 68 +- src/client/client.cpp | 62 +- src/client/client.h | 32 +- src/client/clientplayer.cpp | 4 +- src/client/clientplayer.h | 28 +- src/core/player.cpp | 80 +- src/core/player.h | 62 +- src/core/util.cpp | 162 +-- src/main.cpp | 86 +- src/network/client_socket.cpp | 96 +- src/network/client_socket.h | 38 +- src/network/router.cpp | 242 ++-- src/network/router.h | 102 +- src/network/server_socket.cpp | 20 +- src/network/server_socket.h | 12 +- src/resources.qrc | 5 - src/server/room.cpp | 338 ++--- src/server/room.h | 116 +- src/server/server.cpp | 354 ++--- src/server/server.h | 54 +- src/server/serverplayer.cpp | 112 +- src/server/serverplayer.h | 48 +- src/swig/client.i | 52 +- src/swig/naturalvar.i | 26 +- src/swig/player.i | 56 +- src/swig/qt.i | 52 +- src/swig/server.i | 154 +-- src/ui/qmlbackend.cpp | 189 +-- src/ui/qmlbackend.h | 50 +- 102 files changed, 7268 insertions(+), 6314 deletions(-) rename image/card/equipIcon/{double_sword.png => double_swords.png} (100%) rename image/card/equipIcon/{renwang_shield.png => nioh_shield.png} (100%) mode change 100644 => 100755 lib/win/lua54.dll delete mode 100644 lua/core/card_type/skill.lua create mode 100644 lua/core/skill_type/active_skill.lua delete mode 100644 src/resources.qrc diff --git a/CMakeLists.txt b/CMakeLists.txt index a856a565..9fc0e38e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,10 +3,10 @@ cmake_minimum_required(VERSION 3.16) project(FreeKill VERSION 0.0.1) find_package(Qt5 REQUIRED COMPONENTS - Gui - Qml - Network - Multimedia + Gui + Qml + Network + Multimedia ) find_package(Lua) diff --git a/image/card/equipIcon/double_sword.png b/image/card/equipIcon/double_swords.png similarity index 100% rename from image/card/equipIcon/double_sword.png rename to image/card/equipIcon/double_swords.png diff --git a/image/card/equipIcon/renwang_shield.png b/image/card/equipIcon/nioh_shield.png similarity index 100% rename from image/card/equipIcon/renwang_shield.png rename to image/card/equipIcon/nioh_shield.png diff --git a/lib/win/lua54.dll b/lib/win/lua54.dll old mode 100644 new mode 100755 index 77ff13fd9e05518c3e62ed53264c0c1c14ce4688..fe6e1bafea9038b6cd9a36493ea930c2753db72f GIT binary patch delta 133254 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

x75EtZ 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�&a$ts}zz#6^xV!J;pI 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 diff --git a/lua/client/client.lua b/lua/client/client.lua index 19f6dfe5..8d0dfea4 100644 --- a/lua/client/client.lua +++ b/lua/client/client.lua @@ -5,140 +5,140 @@ Client = class('Client') -- load client classes ClientPlayer = require "client.clientplayer" -dofile "lua/client/client_util.lua" fk.client_callback = {} function Client:initialize() - self.client = fk.ClientInstance - self.notifyUI = function(self, command, jsonData) - fk.Backend:emitNotifyUI(command, jsonData) - end - self.client.callback = function(_self, command, jsonData) - local cb = fk.client_callback[command] - if (type(cb) == "function") then - cb(jsonData) - else - self:notifyUI(command, jsonData); - end + self.client = fk.ClientInstance + self.notifyUI = function(self, command, jsonData) + fk.Backend:emitNotifyUI(command, jsonData) + end + self.client.callback = function(_self, command, jsonData) + local cb = fk.client_callback[command] + if (type(cb) == "function") then + cb(jsonData) + else + self:notifyUI(command, jsonData); end + end - self.players = {} -- ClientPlayer[] + self.players = {} -- ClientPlayer[] end ---@param id integer ---@return ClientPlayer function Client:findPlayer(id) - for _, p in ipairs(self.players) do - if p.player:getId() == id then return p end - end - return nil + for _, p in ipairs(self.players) do + if p.player:getId() == id then return p end + end + return nil end fk.client_callback["Setup"] = function(jsonData) - -- jsonData: [ int id, string screenName, string avatar ] - local data = json.decode(jsonData) - local id, name, avatar = data[1], data[2], data[3] - local self = fk.Self - self:setId(id) - self:setScreenName(name) - self:setAvatar(avatar) - Self = ClientPlayer:new(fk.Self) - table.insert(ClientInstance.players, Self) + -- jsonData: [ int id, string screenName, string avatar ] + local data = json.decode(jsonData) + local id, name, avatar = data[1], data[2], data[3] + local self = fk.Self + self:setId(id) + self:setScreenName(name) + self:setAvatar(avatar) + Self = ClientPlayer:new(fk.Self) + table.insert(ClientInstance.players, Self) end fk.client_callback["AddPlayer"] = function(jsonData) - -- jsonData: [ int id, string screenName, string avatar ] - -- when other player enter the room, we create clientplayer(C and lua) for them - local data = json.decode(jsonData) - local id, name, avatar = data[1], data[2], data[3] - local player = fk.ClientInstance:addPlayer(id, name, avatar) - table.insert(ClientInstance.players, ClientPlayer:new(player)) - ClientInstance:notifyUI("AddPlayer", jsonData) + -- jsonData: [ int id, string screenName, string avatar ] + -- when other player enter the room, we create clientplayer(C and lua) for them + local data = json.decode(jsonData) + local id, name, avatar = data[1], data[2], data[3] + local player = fk.ClientInstance:addPlayer(id, name, avatar) + table.insert(ClientInstance.players, ClientPlayer:new(player)) + ClientInstance:notifyUI("AddPlayer", jsonData) end fk.client_callback["RemovePlayer"] = function(jsonData) - -- jsonData: [ int id ] - local data = json.decode(jsonData) - local id = data[1] - for _, p in ipairs(ClientInstance.players) do - if p.player:getId() == id then - table.removeOne(ClientInstance.players, p) - break - end + -- jsonData: [ int id ] + local data = json.decode(jsonData) + local id = data[1] + for _, p in ipairs(ClientInstance.players) do + if p.player:getId() == id then + table.removeOne(ClientInstance.players, p) + break end - fk.ClientInstance:removePlayer(id) - ClientInstance:notifyUI("RemovePlayer", jsonData) + end + fk.ClientInstance:removePlayer(id) + ClientInstance:notifyUI("RemovePlayer", jsonData) end fk.client_callback["ArrangeSeats"] = function(jsonData) - local data = json.decode(jsonData) - local n = #ClientInstance.players - local players = {} + local data = json.decode(jsonData) + local n = #ClientInstance.players + local players = {} - for i = 1, n do - table.insert(players, ClientInstance:findPlayer(data[i])) - end - ClientInstance.players = players + for i = 1, n do + table.insert(players, ClientInstance:findPlayer(data[i])) + end + ClientInstance.players = players - ClientInstance:notifyUI("ArrangeSeats", jsonData) + ClientInstance:notifyUI("ArrangeSeats", jsonData) end fk.client_callback["PropertyUpdate"] = function(jsonData) - -- jsonData: [ int id, string property_name, value ] - local data = json.decode(jsonData) - local id, name, value = data[1], data[2], data[3] - ClientInstance:findPlayer(id)[name] = value - ClientInstance:notifyUI("PropertyUpdate", jsonData) + -- jsonData: [ int id, string property_name, value ] + local data = json.decode(jsonData) + local id, name, value = data[1], data[2], data[3] + ClientInstance:findPlayer(id)[name] = value + ClientInstance:notifyUI("PropertyUpdate", jsonData) end --- separated moves to many moves(one card per move) ---@param moves CardsMoveStruct[] local function separateMoves(moves) - local ret = {} ---@type CardsMoveInfo[] - for _, move in ipairs(moves) do - for _, info in ipairs(move.moveInfo) do - table.insert(ret, { - ids = {info.cardId}, - from = move.from, - to = move.to, - toArea = move.toArea, - fromArea = info.fromArea, - }) - end + local ret = {} ---@type CardsMoveInfo[] + for _, move in ipairs(moves) do + for _, info in ipairs(move.moveInfo) do + table.insert(ret, { + ids = {info.cardId}, + from = move.from, + to = move.to, + toArea = move.toArea, + fromArea = info.fromArea, + }) end - return ret + end + return ret end --- merge separated moves (one fromArea per move) local function mergeMoves(moves) - local ret = {} - local temp = {} - for _, move in ipairs(moves) do - if temp[move.fromArea] == nil then - temp[move.fromArea] = { - ids = {}, - from = move.from, - to = move.to, - fromArea = move.fromArea, - toArea = move.toArea - } - end - table.insert(temp[move.fromArea].ids, move.ids[1]) + local ret = {} + local temp = {} + for _, move in ipairs(moves) do + if temp[move.fromArea] == nil then + temp[move.fromArea] = { + ids = {}, + from = move.from, + to = move.to, + fromArea = move.fromArea, + toArea = move.toArea + } end - for _, v in pairs(temp) do - table.insert(ret, v) - end - return ret + table.insert(temp[move.fromArea].ids, move.ids[1]) + end + for _, v in pairs(temp) do + table.insert(ret, v) + end + return ret end fk.client_callback["MoveCards"] = function(jsonData) - -- jsonData: CardsMoveStruct[] - local raw_moves = json.decode(jsonData) - local separated = separateMoves(raw_moves) - local merged = mergeMoves(separated) - ClientInstance:notifyUI("MoveCards", json.encode(merged)) + -- jsonData: CardsMoveStruct[] + local raw_moves = json.decode(jsonData) + local separated = separateMoves(raw_moves) + local merged = mergeMoves(separated) + ClientInstance:notifyUI("MoveCards", json.encode(merged)) end -- Create ClientInstance (used by Lua) ClientInstance = Client:new() +dofile "lua/client/client_util.lua" diff --git a/lua/client/client_util.lua b/lua/client/client_util.lua index 182332d9..6e607adb 100644 --- a/lua/client/client_util.lua +++ b/lua/client/client_util.lua @@ -1,64 +1,145 @@ +-- All functions in this file are used by Qml + function Translate(src) - return Fk.translations[src] + return Fk.translations[src] end function GetGeneralData(name) - local general = Fk.generals[name] - if general == nil then general = Fk.generals["diaochan"] end - return json.encode { - kingdom = general.kingdom, - hp = general.hp, - maxHp = general.maxHp - } + local general = Fk.generals[name] + if general == nil then general = Fk.generals["diaochan"] end + return json.encode { + kingdom = general.kingdom, + hp = general.hp, + maxHp = general.maxHp + } 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", +} + function GetCardData(id) - local card = Fk.cards[id] - if card == nil then return json.encode{ - cid = id, - known = false - } end - return json.encode{ - cid = id, - name = card.name, - number = card.number, - suit = card:getSuitString(), - color = card.color, - } + local card = Fk.cards[id] + if card == nil then return json.encode{ + cid = id, + known = false + } end + local ret = { + cid = id, + name = card.name, + number = card.number, + suit = card:getSuitString(), + color = card.color, + subtype = cardSubtypeStrings[card.sub_type] + } + return json.encode(ret) 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) - end + local ret = {} + for _, name in ipairs(Fk.package_names) do + if Fk.packages[name].type == Package.GeneralPack then + table.insert(ret, name) end - return json.encode(ret) + end + return json.encode(ret) end function GetGenerals(pack_name) - local ret = {} - for _, g in ipairs(Fk.packages[pack_name].generals) do - table.insert(ret, g.name) - end - return json.encode(ret) + local ret = {} + for _, g in ipairs(Fk.packages[pack_name].generals) do + table.insert(ret, g.name) + end + return json.encode(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) - end + local ret = {} + for _, name in ipairs(Fk.package_names) do + if Fk.packages[name].type == Package.CardPack then + table.insert(ret, name) end - return json.encode(ret) + end + 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 + +---@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 + error() + end + + local ret = c.skill:canUse(ClientInstance:findPlayer(player)) + return json.encode(ret) +end + +---@param card string | integer +---@param to_select integer @ id of the target +---@param selected integer[] @ ids of selected targets +---@param selected_cards integer[] @ ids of selected cards +function CanUseCardToTarget(card, to_select, selected) + 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:targetFilter(to_select, selected, selected_cards) + return json.encode(ret) +end + +---@param card string | integer +---@param to_select integer @ id of a card not selected +---@param selected integer[] @ ids of selected cards +---@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 ret = c.skill:cardFilter(to_select, selected_cards, selected_targets) + return json.encode(ret) +end + +---@param card string | integer +---@param selected integer[] @ ids of selected cards +---@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 + error() + end + + local ret = c.skill:feasible(selected_cards, selected_targets) + return json.encode(ret) end diff --git a/lua/client/clientplayer.lua b/lua/client/clientplayer.lua index e2c575d6..b611ae43 100644 --- a/lua/client/clientplayer.lua +++ b/lua/client/clientplayer.lua @@ -5,9 +5,9 @@ local ClientPlayer = Player:subclass("ClientPlayer") function ClientPlayer:initialize(cp) - self.player = cp - self.handcardNum = 0 - self.known_cards = {} + self.player = cp + self.handcardNum = 0 + self.known_cards = {} end return ClientPlayer diff --git a/lua/core/card.lua b/lua/core/card.lua index c288c61b..bc1261ee 100644 --- a/lua/core/card.lua +++ b/lua/core/card.lua @@ -3,6 +3,7 @@ ---@field name string ---@field suit Suit ---@field number integer +---@field trueName string ---@field color Color ---@field id integer ---@field type CardType @@ -26,10 +27,9 @@ Card.NoColor = 3 ---@alias CardType integer -Card.TypeSkill = 1 -Card.TypeBasic = 2 -Card.TypeTrick = 3 -Card.TypeEquip = 4 +Card.TypeBasic = 1 +Card.TypeTrick = 2 +Card.TypeEquip = 3 ---@alias CardSubtype integer @@ -54,39 +54,41 @@ Card.DiscardPile = 7 Card.Void = 8 function Card:initialize(name, suit, number, color) - self.name = name - self.suit = suit or Card.NoSuit - self.number = number or 0 + self.name = name + self.suit = suit or Card.NoSuit + self.number = number or 0 + self.trueName = name - if suit == Card.Spade or suit == Card.Club then - self.color = Card.Black - elseif suit == Card.Heart or suit == Card.Diamond then - self.color = Card.Red - elseif color ~= nil then - self.color = color - else - self.color = Card.NoColor - end + if suit == Card.Spade or suit == Card.Club then + self.color = Card.Black + elseif suit == Card.Heart or suit == Card.Diamond then + self.color = Card.Red + elseif color ~= nil then + self.color = color + else + self.color = Card.NoColor + end - self.package = nil - self.id = 0 - self.type = 0 - self.sub_type = Card.SubTypeNone + self.package = nil + self.id = 0 + self.type = 0 + self.sub_type = Card.SubTypeNone + self.skill = nil end function Card:getSuitString() - local suit = self.suit - if suit == Card.Spade then - return "spade" - elseif suit == Card.Heart then - return "heart" - elseif suit == Card.Club then - return "club" - elseif suit == Card.Diamond then - return "diamond" - else - return "unknown" - end + local suit = self.suit + if suit == Card.Spade then + return "spade" + elseif suit == Card.Heart then + return "heart" + elseif suit == Card.Club then + return "club" + elseif suit == Card.Diamond then + return "diamond" + else + return "unknown" + end end return Card diff --git a/lua/core/card_type/basic.lua b/lua/core/card_type/basic.lua index 06f0ca48..330811cc 100644 --- a/lua/core/card_type/basic.lua +++ b/lua/core/card_type/basic.lua @@ -2,16 +2,17 @@ local BasicCard = Card:subclass("BasicCard") function BasicCard:initialize(name, suit, number) - Card.initialize(self, name, suit, number) - self.type = Card.TypeBasic + Card.initialize(self, name, suit, number) + self.type = Card.TypeBasic end ---@param suit Suit ---@param number integer ---@return BasicCard function BasicCard:clone(suit, number) - local newCard = BasicCard:new(self.name, suit, number) - return newCard + local newCard = BasicCard:new(self.name, suit, number) + newCard.skill = self.skill + return newCard end return BasicCard diff --git a/lua/core/card_type/equip.lua b/lua/core/card_type/equip.lua index 9ac303b2..2b072065 100644 --- a/lua/core/card_type/equip.lua +++ b/lua/core/card_type/equip.lua @@ -1,90 +1,97 @@ ---@class EquipCard : Card +---@field equipSkill Skill local EquipCard = Card:subclass("EquipCard") function EquipCard:initialize(name, suit, number) - Card.initialize(self, name, suit, number) - self.type = Card.TypeEquip + Card.initialize(self, name, suit, number) + self.type = Card.TypeEquip + self.equipSkill = nil end ---@class Weapon : EquipCard local Weapon = EquipCard:subclass("Weapon") function Weapon:initialize(name, suit, number, attackRange) - EquipCard.initialize(self, name, suit, number) - self.sub_type = Card.SubtypeWeapon - self.attack_range = attackRange or 1 + EquipCard.initialize(self, name, suit, number) + self.sub_type = Card.SubtypeWeapon + self.attack_range = attackRange or 1 end ---@param suit Suit ---@param number integer ---@return Weapon function Weapon:clone(suit, number) - local newCard = Weapon:new(self.name, suit, number, self.attack_range) - return newCard + local newCard = Weapon:new(self.name, suit, number, self.attack_range) + newCard.skill = self.skill + return newCard end ---@class Armor : EquipCard local Armor = EquipCard:subclass("armor") function Armor:initialize(name, suit, number) - EquipCard.initialize(self, name, suit, number) - self.sub_type = Card.SubtypeArmor + EquipCard.initialize(self, name, suit, number) + self.sub_type = Card.SubtypeArmor end ---@param suit Suit ---@param number integer ---@return Armor function Armor:clone(suit, number) - local newCard = Armor:new(self.name, suit, number) - return newCard + local newCard = Armor:new(self.name, suit, number) + newCard.skill = self.skill + return newCard end ---@class DefensiveRide : EquipCard local DefensiveRide = EquipCard:subclass("DefensiveRide") function DefensiveRide:initialize(name, suit, number) - EquipCard.initialize(self, name, suit, number) - self.sub_type = Card.SubtypeDefensiveRide + EquipCard.initialize(self, name, suit, number) + self.sub_type = Card.SubtypeDefensiveRide end ---@param suit Suit ---@param number integer ---@return DefensiveRide function DefensiveRide:clone(suit, number) - local newCard = DefensiveRide:new(self.name, suit, number) - return newCard + local newCard = DefensiveRide:new(self.name, suit, number) + newCard.skill = self.skill + return newCard end ---@class OffensiveRide : EquipCard local OffensiveRide = EquipCard:subclass("OffensiveRide") function OffensiveRide:initialize(name, suit, number) - EquipCard.initialize(self, name, suit, number) - self.sub_type = Card.SubtypeOffensiveRide + EquipCard.initialize(self, name, suit, number) + self.sub_type = Card.SubtypeOffensiveRide end ---@param suit Suit ---@param number integer ---@return OffensiveRide function OffensiveRide:clone(suit, number) - local newCard = OffensiveRide:new(self.name, suit, number) - return newCard + local newCard = OffensiveRide:new(self.name, suit, number) + newCard.skill = self.skill + return newCard end ---@class Treasure : EquipCard local Treasure = EquipCard:subclass("Treasure") function Treasure:initialize(name, suit, number) - EquipCard.initialize(self, name, suit, number) - self.sub_type = Card.SubtypeTreasure + EquipCard.initialize(self, name, suit, number) + self.sub_type = Card.SubtypeTreasure end ---@param suit Suit ---@param number integer ---@return Treasure function Treasure:clone(suit, number) - local newCard = Treasure:new(self.name, suit, number) - return newCard + local newCard = Treasure:new(self.name, suit, number) + newCard.skill = self.skill + return newCard end return { EquipCard, Weapon, Armor, DefensiveRide, OffensiveRide, Treasure } diff --git a/lua/core/card_type/skill.lua b/lua/core/card_type/skill.lua deleted file mode 100644 index a3064d13..00000000 --- a/lua/core/card_type/skill.lua +++ /dev/null @@ -1,9 +0,0 @@ ----@class SkillCard : Card -local SkillCard = Card:subclass("SkillCard") - -function SkillCard:initialize(name) - Card.initialize(self, name, Card.NoSuit, 0) - self.type = Card.TypeSkill -end - -return SkillCard diff --git a/lua/core/card_type/trick.lua b/lua/core/card_type/trick.lua index 13106541..2afc0f72 100644 --- a/lua/core/card_type/trick.lua +++ b/lua/core/card_type/trick.lua @@ -2,32 +2,36 @@ local TrickCard = Card:subclass("TrickCard") function TrickCard:initialize(name, suit, number) - Card.initialize(self, name, suit, number) - self.type = Card.TypeTrick + Card.initialize(self, name, suit, number) + self.type = Card.TypeTrick end ---@param suit Suit ---@param number integer ---@return TrickCard function TrickCard:clone(suit, number) - local newCard = TrickCard:new(self.name, suit, number) - return newCard + local newCard = TrickCard:new(self.name, suit, number) + + newCard.skill = self.skill + + return newCard end ---@class DelayedTrickCard : TrickCard local DelayedTrickCard = TrickCard:subclass("DelayedTrickCard") function DelayedTrickCard:initialize(name, suit, number) - TrickCard.initialize(self, name, suit, number) - self.sub_type = Card.SubtypeDelayedTrick + TrickCard.initialize(self, name, suit, number) + self.sub_type = Card.SubtypeDelayedTrick end ---@param suit Suit ---@param number integer ---@return DelayedTrickCard function DelayedTrickCard:clone(suit, number) - local newCard = DelayedTrickCard:new(self.name, suit, number) - return newCard + local newCard = DelayedTrickCard:new(self.name, suit, number) + newCard.skill = self.skill + return newCard end return { TrickCard, DelayedTrickCard } diff --git a/lua/core/debug.lua b/lua/core/debug.lua index 638dee0a..63d4de33 100644 --- a/lua/core/debug.lua +++ b/lua/core/debug.lua @@ -3,16 +3,16 @@ inspect = require "inspect" DebugMode = true function PrintWhenMethodCall() - local info = debug.getinfo(2) - local name = info.name - local line = info.currentline - local namewhat = info.namewhat - local shortsrc = info.short_src - if (namewhat == "method") and - (shortsrc ~= "[C]") and - (not string.find(shortsrc, "/lib")) then - print(shortsrc .. ":" .. line .. ": " .. name) - end + local info = debug.getinfo(2) + local name = info.name + local line = info.currentline + local namewhat = info.namewhat + local shortsrc = info.short_src + if (namewhat == "method") and + (shortsrc ~= "[C]") and + (not string.find(shortsrc, "/lib")) then + print(shortsrc .. ":" .. line .. ": " .. name) + end end --debug.sethook(PrintWhenMethodCall, "c") diff --git a/lua/core/engine.lua b/lua/core/engine.lua index 6e0fe1c3..497c1165 100644 --- a/lua/core/engine.lua +++ b/lua/core/engine.lua @@ -11,126 +11,126 @@ local Engine = class("Engine") function Engine:initialize() - -- Engine should be singleton - if Fk ~= nil then - error("Engine has been initialized") - return - end + -- Engine should be singleton + if Fk ~= nil then + error("Engine has been initialized") + return + end - Fk = self + Fk = self - self.packages = {} -- name --> Package - self.package_names = {} - self.skills = {} -- name --> Skill - self.related_skills = {} -- skillName --> relatedSkill[] - self.global_trigger = {} - self.generals = {} -- name --> General - self.lords = {} -- lordName[] - self.cards = {} -- Card[] - self.translations = {} -- srcText --> translated + self.packages = {} -- name --> Package + self.package_names = {} + self.skills = {} -- name --> Skill + self.related_skills = {} -- skillName --> relatedSkill[] + self.global_trigger = {} + self.generals = {} -- name --> General + self.lords = {} -- lordName[] + self.cards = {} -- Card[] + self.translations = {} -- srcText --> translated - self:loadPackages() + self:loadPackages() end ---@param pack Package function Engine:loadPackage(pack) - assert(pack:isInstanceOf(Package)) - if self.packages[pack.name] ~= nil then - error(string.format("Duplicate package %s detected", pack.name)) - end - self.packages[pack.name] = pack - table.insert(self.package_names, pack.name) + assert(pack:isInstanceOf(Package)) + if self.packages[pack.name] ~= nil then + error(string.format("Duplicate package %s detected", pack.name)) + end + self.packages[pack.name] = pack + table.insert(self.package_names, pack.name) - -- add cards, generals and skills to Engine - if pack.type == Package.CardPack then - self:addCards(pack.cards) - elseif pack.type == Package.GeneralPack then - self:addGenerals(pack.generals) - end - self:addSkills(pack:getSkills()) + -- add cards, generals and skills to Engine + if pack.type == Package.CardPack then + self:addCards(pack.cards) + elseif pack.type == Package.GeneralPack then + self:addGenerals(pack.generals) + end + self:addSkills(pack:getSkills()) end function Engine:loadPackages() - local directories = FileIO.ls("packages") + local directories = FileIO.ls("packages") - -- load standard & standard_cards first - self:loadPackage(require("packages.standard")) - self:loadPackage(require("packages.standard_cards")) - table.removeOne(directories, "standard") - table.removeOne(directories, "standard_cards") + -- load standard & standard_cards first + self:loadPackage(require("packages.standard")) + self:loadPackage(require("packages.standard_cards")) + table.removeOne(directories, "standard") + table.removeOne(directories, "standard_cards") - for _, dir in ipairs(directories) do - if FileIO.isDir("packages/" .. dir) then - local pack = require(string.format("packages.%s", dir)) - -- Note that instance of Package is a table too - -- so dont use type(pack) == "table" here - if pack[1] ~= nil then - for _, p in ipairs(pack) do - self:loadPackage(p) - end - else - self:loadPackage(pack) - end + for _, dir in ipairs(directories) do + if FileIO.isDir("packages/" .. dir) then + local pack = require(string.format("packages.%s", dir)) + -- Note that instance of Package is a table too + -- so dont use type(pack) == "table" here + if pack[1] ~= nil then + for _, p in ipairs(pack) do + self:loadPackage(p) end + else + self:loadPackage(pack) + end end + end end ---@param t table function Engine:loadTranslationTable(t) - assert(type(t) == "table") - for k, v in pairs(t) do - self.translations[k] = v - end + assert(type(t) == "table") + for k, v in pairs(t) do + self.translations[k] = v + end end ---@param skill Skill function Engine:addSkill(skill) - assert(skill.class:isSubclassOf(Skill)) - if self.skills[skill.name] ~= nil then - error(string.format("Duplicate skill %s detected", skill.name)) - end - self.skills[skill.name] = skill + assert(skill.class:isSubclassOf(Skill)) + if self.skills[skill.name] ~= nil then + error(string.format("Duplicate skill %s detected", skill.name)) + end + self.skills[skill.name] = skill end ---@param skills Skill[] function Engine:addSkills(skills) - assert(type(skills) == "table") - for _, skill in ipairs(skills) do - self:addSkill(skill) - end + assert(type(skills) == "table") + for _, skill in ipairs(skills) do + self:addSkill(skill) + end end ---@param general General function Engine:addGeneral(general) - assert(general:isInstanceOf(General)) - if self.generals[general.name] ~= nil then - error(string.format("Duplicate general %s detected", general.name)) - end - self.generals[general.name] = general + assert(general:isInstanceOf(General)) + if self.generals[general.name] ~= nil then + error(string.format("Duplicate general %s detected", general.name)) + end + self.generals[general.name] = general end ---@param generals General[] function Engine:addGenerals(generals) - assert(type(generals) == "table") - for _, general in ipairs(generals) do - self:addGeneral(general) - end + assert(type(generals) == "table") + for _, general in ipairs(generals) do + self:addGeneral(general) + end end local cardId = 1 ---@param card Card function Engine:addCard(card) - assert(card.class:isSubclassOf(Card)) - card.id = cardId - cardId = cardId + 1 - table.insert(self.cards, card) + assert(card.class:isSubclassOf(Card)) + card.id = cardId + cardId = cardId + 1 + table.insert(self.cards, card) end ---@param cards Card[] function Engine:addCards(cards) - for _, card in ipairs(cards) do - self:addCard(card) - end + for _, card in ipairs(cards) do + self:addCard(card) + end end ---@param num integer @@ -139,68 +139,68 @@ end ---@param filter function ---@return General[] function Engine:getGeneralsRandomly(num, generalPool, except, filter) - if filter then - assert(type(filter) == "function") - end + if filter then + assert(type(filter) == "function") + end - generalPool = generalPool or self.generals - except = except or {} + generalPool = generalPool or self.generals + except = except or {} - local availableGenerals = {} - 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 + local availableGenerals = {} + 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 + + if #availableGenerals == 0 then + return {} + end + + local result = {} + for i = 1, num do + local randomGeneral = math.random(1, #availableGenerals) + table.insert(result, availableGenerals[randomGeneral]) + table.remove(availableGenerals, randomGeneral) if #availableGenerals == 0 then - return {} + break end + end - local result = {} - for i = 1, num do - local randomGeneral = math.random(1, #availableGenerals) - table.insert(result, availableGenerals[randomGeneral]) - table.remove(availableGenerals, randomGeneral) - - if #availableGenerals == 0 then - break - end - end - - return result + return result end ---@param except General[] ---@return General[] function Engine:getAllGenerals(except) - local result = {} - for _, general in ipairs(self.generals) do - if not (except and table.contains(except, general)) then - table.insert(result, general) - end + local result = {} + for _, general in ipairs(self.generals) do + if not (except and table.contains(except, general)) then + table.insert(result, general) end + end - return result + return result end ---@param except integer[] ---@return integer[] function Engine:getAllCardIds(except) - local result = {} - for _, card in ipairs(self.cards) do - if not (except and table.contains(except, card.id)) then - table.insert(result, card.id) - end + local result = {} + for _, card in ipairs(self.cards) do + if not (except and table.contains(except, card.id)) then + table.insert(result, card.id) end + end - return result + return result end ---@param id integer ---@return Card function Engine:getCardById(id) - return self.cards[id] + return self.cards[id] end return Engine diff --git a/lua/core/general.lua b/lua/core/general.lua index de52e89a..dd06d163 100644 --- a/lua/core/general.lua +++ b/lua/core/general.lua @@ -15,24 +15,24 @@ General.Male = 1 General.Female = 2 function General:initialize(package, name, kingdom, hp, maxHp, gender) - self.package = package - self.name = name - self.kingdom = kingdom - self.hp = hp - self.maxHp = maxHp or hp - self.gender = gender or General.Male + self.package = package + self.name = name + self.kingdom = kingdom + self.hp = hp + self.maxHp = maxHp or hp + self.gender = gender or General.Male - self.skills = {} -- skills first added to this general - self.other_skills = {} -- skill belongs other general, e.g. "mashu" of pangde + self.skills = {} -- skills first added to this general + self.other_skills = {} -- skill belongs other general, e.g. "mashu" of pangde end ---@param skill Skill function General:addSkill(skill) - if (type(skill) == "string") then - table.insert(self.other_skills, skill) - elseif (skill.class and skill.class:isSubclassOf(Skill)) then - table.insert(self.skills, skill) - end + if (type(skill) == "string") then + table.insert(self.other_skills, skill) + elseif (skill.class and skill.class:isSubclassOf(Skill)) then + table.insert(self.skills, skill) + end end return General diff --git a/lua/core/package.lua b/lua/core/package.lua index 1e64b2c1..b5180a0a 100644 --- a/lua/core/package.lua +++ b/lua/core/package.lua @@ -14,48 +14,48 @@ Package.CardPack = 2 Package.SpecialPack = 3 function Package:initialize(name, _type) - assert(type(name) == "string") - assert(type(_type) == "nil" or type(_type) == "number") - self.name = name - self.type = _type or Package.GeneralPack + assert(type(name) == "string") + assert(type(_type) == "nil" or type(_type) == "number") + self.name = name + self.type = _type or Package.GeneralPack - self.generals = {} - self.extra_skills = {} -- skill not belongs to any generals, like "jixi" - self.related_skills = {} - self.cards = {} + self.generals = {} + self.extra_skills = {} -- skill not belongs to any generals, like "jixi" + self.related_skills = {} + self.cards = {} end ---@return Skill[] function Package:getSkills() - local ret = {table.unpack(self.related_skills)} - if self.type == Package.GeneralPack then - for _, g in ipairs(self.generals) do - for _, s in ipairs(g.skills) do - table.insert(ret, s) - end - end + local ret = {table.unpack(self.related_skills)} + if self.type == Package.GeneralPack then + for _, g in ipairs(self.generals) do + for _, s in ipairs(g.skills) do + table.insert(ret, s) + end end - return ret + end + return ret end ---@param general General function Package:addGeneral(general) - assert(general.class and general:isInstanceOf(General)) - table.insert(self.generals, general) + assert(general.class and general:isInstanceOf(General)) + table.insert(self.generals, general) end ---@param card Card function Package:addCard(card) - assert(card.class and card:isInstanceOf(Card)) - card.package = self - table.insert(self.cards, card) + assert(card.class and card:isInstanceOf(Card)) + card.package = self + table.insert(self.cards, card) end ---@param cards Card[] function Package:addCards(cards) - for _, card in ipairs(cards) do - self:addCard(card) - end + for _, card in ipairs(cards) do + self:addCard(card) + end end return Package diff --git a/lua/core/player.lua b/lua/core/player.lua index 6e35c55d..11b2a5d6 100644 --- a/lua/core/player.lua +++ b/lua/core/player.lua @@ -19,6 +19,7 @@ ---@field mark table ---@field player_cards table ---@field special_cards table +---@field cardUsedHistory table local Player = class("Player") ---@alias Phase integer @@ -41,184 +42,211 @@ Player.Judge = 3 Player.Special = 4 function Player:initialize() - self.id = 114514 - self.hp = 0 - self.maxHp = 0 - self.kingdom = "qun" - self.role = "" - self.general = "" - self.seat = 0 - self.phase = Player.PhaseNone - self.faceup = true - self.chained = false - self.dying = false - self.dead = false - self.state = "" + self.id = 114514 + self.hp = 0 + self.maxHp = 0 + self.kingdom = "qun" + self.role = "" + self.general = "" + self.seat = 0 + self.phase = Player.PhaseNone + self.faceup = true + self.chained = false + self.dying = false + self.dead = false + self.state = "" - self.player_skills = {} - self.flag = {} - self.tag = {} - self.mark = {} - self.player_cards = { - [Player.Hand] = {}, - [Player.Equip] = {}, - [Player.Judge] = {}, - } - self.special_cards = {} + self.player_skills = {} + self.flag = {} + self.tag = {} + self.mark = {} + self.player_cards = { + [Player.Hand] = {}, + [Player.Equip] = {}, + [Player.Judge] = {}, + } + self.special_cards = {} + + self.cardUsedHistory = {} end ---@param general General ---@param setHp boolean ---@param addSkills boolean function Player:setGeneral(general, setHp, addSkills) - self.general = general - if setHp then - self.maxHp = general.maxHp - self.hp = general.hp - end + self.general = general + if setHp then + self.maxHp = general.maxHp + self.hp = general.hp + end - if addSkills then - table.insertTable(self.player_skills, general.skills) - end + if addSkills then + table.insertTable(self.player_skills, general.skills) + end end ---@param flag string function Player:hasFlag(flag) - return table.contains(self.flag, flag) + return table.contains(self.flag, flag) end ---@param flag string function Player:setFlag(flag) - if flag == "." then - self:clearFlags() - return - end - if flag:sub(1, 1) == "-" then - flag = flag:sub(2, #flag) - table.removeOne(self.flag, flag) - return - end - if not self:hasFlag(flag) then - table.insert(self.flag, flag) - end + if flag == "." then + self:clearFlags() + return + end + if flag:sub(1, 1) == "-" then + flag = flag:sub(2, #flag) + table.removeOne(self.flag, flag) + return + end + if not self:hasFlag(flag) then + table.insert(self.flag, flag) + end end function Player:clearFlags() - self.flag = {} + self.flag = {} end function Player:addMark(mark, count) - count = count or 1 - local num = self.mark[mark] - num = num or 0 - self:setMark(mark, math.max(num + count, 0)) + count = count or 1 + local num = self.mark[mark] + num = num or 0 + self:setMark(mark, math.max(num + count, 0)) end function Player:removeMark(mark, count) - count = count or 1 - local num = self.mark[mark] - num = num or 0 - self:setMark(mark, math.max(num - count, 0)) + count = count or 1 + local num = self.mark[mark] + num = num or 0 + self:setMark(mark, math.max(num - count, 0)) end function Player:setMark(mark, count) - if self.mark[mark] ~= count then - self.mark[mark] = count - end + if self.mark[mark] ~= count then + self.mark[mark] = count + end end function Player:getMark(mark) - return (self.mark[mark] or 0) + return (self.mark[mark] or 0) end function Player:getMarkNames() - local ret = {} - for k, _ in pairs(self.mark) do - table.insert(ret, k) - end - return ret + local ret = {} + for k, _ in pairs(self.mark) do + table.insert(ret, k) + end + return ret end ---@param playerArea PlayerCardArea ---@param cardIds integer[] ---@param specialName string function Player:addCards(playerArea, cardIds, specialName) - assert(table.contains({ Player.Hand, Player.Equip, Player.Judge, Player.Special }, playerArea)) - assert(playerArea ~= Player.Special or type(specialName) == "string") + assert(table.contains({ Player.Hand, Player.Equip, Player.Judge, Player.Special }, playerArea)) + assert(playerArea ~= Player.Special or type(specialName) == "string") - if playerArea == Player.Special then - self.special_cards[specialName] = self.special_cards[specialName] or {} - table.insertTable(self.special_cards[specialName], cardIds) - else - table.insertTable(self.player_cards[playerArea], cardIds) - end + if playerArea == Player.Special then + self.special_cards[specialName] = self.special_cards[specialName] or {} + table.insertTable(self.special_cards[specialName], cardIds) + else + table.insertTable(self.player_cards[playerArea], cardIds) + end end ---@param playerArea PlayerCardArea ---@param cardIds integer[] ---@param specialName string function Player:removeCards(playerArea, cardIds, specialName) - assert(table.contains({ Player.Hand, Player.Equip, Player.Judge, Player.Special }, playerArea)) - assert(playerArea ~= Player.Special or type(specialName) == "string") + assert(table.contains({ Player.Hand, Player.Equip, Player.Judge, Player.Special }, playerArea)) + assert(playerArea ~= Player.Special or type(specialName) == "string") - local fromAreaIds = playerArea == Player.Special and self.special_cards[specialName] or self.player_cards[playerArea] - if fromAreaIds then - for _, id in ipairs(cardIds) do - if #fromAreaIds == 0 then - break - end + local fromAreaIds = playerArea == Player.Special and self.special_cards[specialName] or self.player_cards[playerArea] + if fromAreaIds then + for _, id in ipairs(cardIds) do + if #fromAreaIds == 0 then + break + end - table.removeOne(fromAreaIds, id) - end + table.removeOne(fromAreaIds, id) end + end end ---@param playerAreas PlayerCardArea ---@param specialName string ---@return integer[] function Player:getCardIds(playerAreas, specialName) - local rightAreas = { Player.Hand, Player.Equip, Player.Judge } - playerAreas = playerAreas or rightAreas - assert(type(playerAreas) == "number" or type(playerAreas) == "table") - local areas = type(playerAreas) == "table" and playerAreas or { playerAreas } + local rightAreas = { Player.Hand, Player.Equip, Player.Judge } + playerAreas = playerAreas or rightAreas + assert(type(playerAreas) == "number" or type(playerAreas) == "table") + 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") - local currentCardIds = area == Player.Special and self.special_cards[specialName] or self.player_cards[area] - table.insertTable(cardIds, currentCardIds) + 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") + local currentCardIds = area == Player.Special and self.special_cards[specialName] or self.player_cards[area] + table.insertTable(cardIds, currentCardIds) + end + + return cardIds +end + +---@param cardSubtype CardSubtype +---@return integer|null +function Player:getEquipment(cardSubtype) + for _, cardId in ipairs(self.player_cards[Player.Equip]) do + if Fk:getCardById(cardId).sub_type == cardSubtype then + return cardId end + end - return cardIds + return nil end function Player:getMaxCards() - local baseValue = math.max(self.hp, 0) + local baseValue = math.max(self.hp, 0) - return baseValue + return baseValue end ---@param subtype CardSubtype ---@return integer|null function Player:getEquipBySubtype(subtype) - local equipId = nil - for _, id in ipairs(self.player_cards[Player.Equip]) do - if Fk.getCardById(id).sub_type == subtype then - equipId = id - break - end + local equipId = nil + for _, id in ipairs(self.player_cards[Player.Equip]) do + if Fk:getCardById(id).sub_type == subtype then + equipId = id + break end + end - return equipId + return equipId end function Player:getAttackRange() - local weapon = Fk.getCardById(self:getEquipBySubtype(Card.SubtypeWeapon)) - local baseAttackRange = math.max(weapon and weapon.attack_range or 1, 0) + local weapon = Fk:getCardById(self:getEquipBySubtype(Card.SubtypeWeapon)) + local baseAttackRange = math.max(weapon and weapon.attack_range or 1, 0) - return math.max(baseAttackRange, 0) + return math.max(baseAttackRange, 0) +end + +function Player:addCardUseHistory(cardName, num) + assert(type(num) == "number" and num ~= 0) + + self.cardUsedHistory[cardName] = self.cardUsedHistory[cardName] or 0 + self.cardUsedHistory[cardName] = self.cardUsedHistory[cardName] + num +end + +function Player:resetCardUseHistory(cardName) + if self.cardUsedHistory[cardName] then + self.cardUsedHistory[cardName] = 0 + end end return Player diff --git a/lua/core/skill.lua b/lua/core/skill.lua index 337308d8..8a051b73 100644 --- a/lua/core/skill.lua +++ b/lua/core/skill.lua @@ -13,10 +13,10 @@ Skill.Limited = 4 Skill.Wake = 5 function Skill:initialize(name, frequency) - -- TODO: visible, lord, etc - self.name = name - self.frequency = frequency - self.visible = true + -- TODO: visible, lord, etc + self.name = name + self.frequency = frequency + self.visible = true end return Skill diff --git a/lua/core/skill_type/active_skill.lua b/lua/core/skill_type/active_skill.lua new file mode 100644 index 00000000..158ffe45 --- /dev/null +++ b/lua/core/skill_type/active_skill.lua @@ -0,0 +1,57 @@ +--- ActiveSkill is a skill type like SkillCard+ViewAsSkill in QSanguosha +--- +---@class ActiveSkill : Skill +local ActiveSkill = Skill:subclass("ActiveSkill") + +function ActiveSkill:initialize(name) + Skill.initialize(self, name, Skill.NotFrequent) +end + +--------- +-- Note: these functions are used both client and ai +------- { + +--- Determine whether the skill can be used in playing phase +---@param player Player +function ActiveSkill:canUse(player) + return true +end + +--- Determine whether a card can be selected by this skill +--- only used in skill of players +---@param to_select integer @ id of a card not selected +---@param selected integer[] @ ids of selected cards +---@param selected_targets integer[] @ ids of selected players +function ActiveSkill:cardFilter(to_select, selected, selected_targets) + return true +end + +--- Determine whether a target can be selected by this skill +--- only used in skill of players +---@param to_select integer @ id of the target +---@param selected integer[] @ ids of selected targets +---@param selected_cards integer[] @ ids of selected cards +function ActiveSkill:targetFilter(to_select, selected, selected_cards) + return false +end + +--- Determine if selected cards and targets are valid for this skill +--- If returns true, the OK button should be enabled +--- only used in skill of players +---@param selected integer[] @ ids of selected cards +---@param selected_targets integer[] @ ids of selected players +function ActiveSkill:feasible(selected, selected_targets) + return true +end + +------- } + +---@param room Room +---@param cardUseEvent CardUseStruct +function ActiveSkill:onUse(room, cardUseEvent) end + +---@param room Room +---@param cardEffectEvent CardEffectEvent +function ActiveSkill:onEffect(room, cardEffectEvent) end + +return ActiveSkill diff --git a/lua/core/skill_type/trigger.lua b/lua/core/skill_type/trigger.lua index 55924ab8..48a4a339 100644 --- a/lua/core/skill_type/trigger.lua +++ b/lua/core/skill_type/trigger.lua @@ -6,12 +6,12 @@ local TriggerSkill = Skill:subclass("TriggerSkill") function TriggerSkill:initialize(name, frequency) - Skill.initialize(self, name, frequency) + Skill.initialize(self, name, frequency) - self.global = false - self.events = {} - self.refresh_events = {} - self.priority_table = {} -- GameEvent --> priority + self.global = false + self.events = {} + self.refresh_events = {} + self.priority_table = {} -- GameEvent --> priority end -- Default functions @@ -37,8 +37,8 @@ function TriggerSkill:refresh(event, target, player, data) end ---@param data any @ useful data of the event ---@return boolean function TriggerSkill:triggerable(event, target, player, data) - return target and (target == player) - and (self.global or (target:isAlive() and target:hasSkill(self))) + return target and (target == player) + and (self.global or (target:isAlive() and target:hasSkill(self))) end ---Trigger this skill @@ -48,10 +48,10 @@ end ---@param data any @ useful data of the event ---@return boolean @ returns true if trigger is broken function TriggerSkill:trigger(event, target, player, data) - if player.room:askForSkillInvoke(player, self.name) then - return self:use(event, target, player, data) - end - return false + if player.room:askForSkillInvoke(player, self.name) then + return self:use(event, target, player, data) + end + return false end ---Use this skill diff --git a/lua/core/util.lua b/lua/core/util.lua index 13b0cbdc..434cc8b8 100644 --- a/lua/core/util.lua +++ b/lua/core/util.lua @@ -1,52 +1,52 @@ -- the iterator of QList object local qlist_iterator = function(list, n) - if n < list:length() - 1 then - return n + 1, list:at(n + 1) -- the next element of list - end + if n < list:length() - 1 then + return n + 1, list:at(n + 1) -- the next element of list + end end function fk.qlist(list) - return qlist_iterator, list, -1 + return qlist_iterator, list, -1 end function table:contains(element) - if #self == 0 or type(self[1]) ~= type(element) then return false end - for _, e in ipairs(self) do - if e == element then return true end - end + if #self == 0 or type(self[1]) ~= type(element) then return false end + for _, e in ipairs(self) do + if e == element then return true end + end end function table:shuffle() - for i = #self, 2, -1 do - local j = math.random(i) - self[i], self[j] = self[j], self[i] - end + for i = #self, 2, -1 do + local j = math.random(i) + self[i], self[j] = self[j], self[i] + end end function table:insertTable(list) - for _, e in ipairs(list) do - table.insert(self, e) - end + for _, e in ipairs(list) do + table.insert(self, e) + end end function table:indexOf(value, from) - from = from or 1 - for i = from, #self do - if self[i] == value then return i end - end - return -1 + from = from or 1 + for i = from, #self do + if self[i] == value then return i end + end + return -1 end function table:removeOne(element) - if #self == 0 or type(self[1]) ~= type(element) then return false end + if #self == 0 or type(self[1]) ~= type(element) then return false end - for i = 1, #self do - if self[i] == element then - table.remove(self, i) - return true - end - end - return false + for i = 1, #self do + if self[i] == element then + table.remove(self, i) + return true + end + end + return false end -- Note: only clone key and value, no metatable @@ -55,57 +55,57 @@ end ---@param self T ---@return T function table.clone(self) - local ret = {} - for k, v in pairs(self) do - if type(v) == "table" then - ret[k] = table.clone(v) - else - ret[k] = v - end - end - return ret + local ret = {} + for k, v in pairs(self) do + if type(v) == "table" then + ret[k] = table.clone(v) + else + ret[k] = v + end + end + return ret end ---@class Sql Sql = { - ---@param filename string - open = function(filename) - return fk.OpenDatabase(filename) - end, + ---@param filename string + open = function(filename) + return fk.OpenDatabase(filename) + end, - ---@param db fk.SQLite3 - close = function(db) - fk.CloseDatabase(db) - end, + ---@param db fk.SQLite3 + close = function(db) + fk.CloseDatabase(db) + end, - --- Execute an SQL statement. - ---@param db fk.SQLite3 - ---@param sql string - exec = function(db, sql) - fk.ExecSQL(db, sql) - end, + --- Execute an SQL statement. + ---@param db fk.SQLite3 + ---@param sql string + exec = function(db, sql) + fk.ExecSQL(db, sql) + end, - --- Execute a `SELECT` SQL statement. - ---@param db fk.SQLite3 - ---@param sql string - ---@return table @ { [columnName] --> result : string[] } - exec_select = function(db, sql) - return json.decode(fk.SelectFromDb(db, sql)) - end, + --- Execute a `SELECT` SQL statement. + ---@param db fk.SQLite3 + ---@param sql string + ---@return table @ { [columnName] --> result : string[] } + exec_select = function(db, sql) + return json.decode(fk.SelectFromDb(db, sql)) + end, } FileIO = { - pwd = fk.QmlBackend_pwd, - ls = function(filename) - if filename == nil then - return fk.QmlBackend_ls(".") - else - return fk.QmlBackend_ls(filename) - end - end, - cd = fk.QmlBackend_cd, - exists = fk.QmlBackend_exists, - isDir = fk.QmlBackend_isDir + pwd = fk.QmlBackend_pwd, + ls = function(filename) + if filename == nil then + return fk.QmlBackend_ls(".") + else + return fk.QmlBackend_ls(filename) + end + end, + cd = fk.QmlBackend_cd, + exists = fk.QmlBackend_exists, + isDir = fk.QmlBackend_isDir } os.getms = fk.GetMicroSecond @@ -113,23 +113,23 @@ os.getms = fk.GetMicroSecond ---@class Stack : Object Stack = class("Stack") function Stack:initialize() - self.t = {} - self.p = 0 + self.t = {} + self.p = 0 end function Stack:push(e) - self.p = self.p + 1 - self.t[self.p] = e + self.p = self.p + 1 + self.t[self.p] = e end function Stack:isEmpty() - return self.p == 0 + return self.p == 0 end function Stack:pop() - if self.p == 0 then return nil end - self.p = self.p - 1 - return self.t[self.p + 1] + if self.p == 0 then return nil end + self.p = self.p - 1 + return self.t[self.p + 1] end @@ -139,15 +139,156 @@ end ---@param table string ---@param enum string[] function CreateEnum(table, enum) - local enum_format = "%s.%s = %d" - for i, v in ipairs(enum) do - print(string.format(enum_format, table, v, i)) - end + local enum_format = "%s.%s = %d" + for i, v in ipairs(enum) do + print(string.format(enum_format, table, v, i)) + end end function switch(param, case_table) - local case = case_table[param] - if case then return case() end - local def = case_table["default"] - return def and def() or nil + local case = case_table[param] + if case then return case() end + local def = case_table["default"] + return def and def() or nil end + +---@class TargetGroup : Object +local TargetGroup = class("TargetGroup") + +function TargetGroup.static:getRealTargets(targetGroup) + if not targetGroup then + return {} + end + + local realTargets = {} + for _, targets in ipairs(targetGroup) do + table.insert(realTargets, targets[1]) + end + + return realTargets +end + +function TargetGroup.static:includeRealTargets(targetGroup, playerId) + if not targetGroup then + return false + end + + for _, targets in ipairs(targetGroup) do + if targets[1] == playerId then + return true + end + end + + return false +end + +function TargetGroup.static:removeTarget(targetGroup, playerId) + if not targetGroup then + return + end + + for index, targets in ipairs(targetGroup) do + if (targets[1] == playerId) then + table.remove(targetGroup, index) + return + end + end +end + +function TargetGroup.static:pushTargets(targetGroup, playerIds) + if not targetGroup then + return + end + + if type(playerIds) == "table" then + table.insert(targetGroup, playerIds) + elseif type(playerIds) == "number" then + table.insert(targetGroup, { playerIds }) + end +end + +---@class AimGroup : Object +local AimGroup = class("AimGroup") + +AimGroup.Undone = 1 +AimGroup.Done = 2 +AimGroup.Cancelled = 3 + +function AimGroup.static:initAimGroup(playerIds) + return { [AimGroup.Undone] = playerIds, [AimGroup.Done] = {}, [AimGroup.Cancelled] = {} } +end + +function AimGroup.static:getAllTargets(aimGroup) + local targets = {} + table.insertTable(targets, aimGroup[AimGroup.Undone]) + table.insertTable(targets, aimGroup[AimGroup.Done]) + return targets +end + +function AimGroup.static:getUndoneOrDoneTargets(aimGroup, done) + return done and aimGroup[AimGroup.Done] or aimGroup[AimGroup.Undone] +end + +function AimGroup.static:setTargetDone(aimGroup, playerId) + local index = table.indexOf(aimGroup[AimGroup.Undone], playerId) + if index ~= -1 then + table.remove(aimGroup[AimGroup.Undone], index) + table.insert(aimGroup[AimGroup.Done], playerId) + end +end + +function AimGroup.static:addTargets(room, aimEvent, playerIds) + local playerId = type(playerIds) == "table" and playerIds[1] or playerIds + table.insert(aimEvent.tos[AimGroup.Undone], playerId) + room:sortPlayersByAction(aimEvent.tos[AimGroup.Undone]) + if aimEvent.targetGroup then + TargetGroup:pushTargets(aimEvent.targetGroup, playerIds) + end +end + +function AimGroup.static:cancelTarget(aimEvent, playerId) + local cancelled = false + for status = AimGroup.Undone, AimGroup.Done do + local indexList = {} + for index, pId in ipairs(aimEvent.tos[status]) do + if pId == playerId then + table.insert(indexList, index) + end + end + + if #indexList > 0 then + cancelled = true + for i = 1, #indexList do + table.remove(aimEvent.tos[status], indexList[i]) + end + end + end + + if cancelled then + table.insert(aimEvent.tos[AimGroup.Cancelled], playerId) + if aimEvent.targetGroup then + TargetGroup:removeTarget(aimEvent.targetGroup, playerId) + end + end +end + +function AimGroup.static:removeDeadTargets(room, aimEvent) + for index = AimGroup.Undone, AimGroup.Done do + aimEvent.tos[index] = room:deadPlayerFilter(aimEvent.tos[index]) + end + + if aimEvent.targetGroup then + local targets = TargetGroup:getRealTargets(aimEvent.targetGroup) + for _, target in ipairs(targets) do + if not room:getPlayerById(target):isAlive() then + TargetGroup:removeTarget(aimEvent.targetGroup, target) + end + end + end +end + +function AimGroup.static:getCancelledTargets(aimGroup) + return aimGroup[AimGroup.Cancelled] +end + +return { TargetGroup, AimGroup } diff --git a/lua/fk_ex.lua b/lua/fk_ex.lua index bce0141d..d76855a7 100644 --- a/lua/fk_ex.lua +++ b/lua/fk_ex.lua @@ -1,18 +1,16 @@ -- load types for extension -SkillCard = require "core.card_type.skill" +dofile "lua/server/event.lua" +dofile "lua/server/system_enum.lua" +TriggerSkill = require "core.skill_type.trigger" +ActiveSkill = require "core.skill_type.active_skill" + BasicCard = require "core.card_type.basic" local Trick = require "core.card_type.trick" TrickCard, DelayedTrickCard = table.unpack(Trick) local Equip = require "core.card_type.equip" _, Weapon, Armor, DefensiveRide, OffensiveRide, Treasure = table.unpack(Equip) -dofile "lua/server/event.lua" -dofile "lua/server/system_enum.lua" -TriggerSkill = require "core.skill_type.trigger" - ----@class CardSpec: Card - ---@class SkillSpec: Skill ---@alias TrigFunc fun(self: TriggerSkill, event: Event, target: ServerPlayer, player: ServerPlayer):boolean @@ -26,166 +24,208 @@ TriggerSkill = require "core.skill_type.trigger" ---@field on_refresh TrigFunc ---@field can_refresh TrigFunc +---@param spec TriggerSkillSpec +---@return TriggerSkill +function fk.CreateTriggerSkill(spec) + assert(type(spec.name) == "string") + --assert(type(spec.on_trigger) == "function") + if spec.frequency then assert(type(spec.frequency) == "number") end + + local frequency = spec.frequency or Skill.NotFrequent + local skill = TriggerSkill:new(spec.name, frequency) + + if type(spec.events) == "number" then + table.insert(skill.events, spec.events) + elseif type(spec.events) == "table" then + table.insertTable(skill.events, spec.events) + end + + if type(spec.refresh_events) == "number" then + table.insert(skill.refresh_events, spec.refresh_events) + elseif type(spec.refresh_events) == "table" then + table.insertTable(skill.refresh_events, spec.refresh_events) + end + + if type(spec.global) == "boolean" then skill.global = spec.global end + + if spec.on_trigger then skill.trigger = spec.on_trigger end + + if spec.can_trigger then + skill.triggerable = spec.can_trigger + end + + if spec.can_refresh then + skill.canRefresh = spec.can_refresh + end + + if spec.on_refresh then + skill.refresh = spec.on_refresh + end + + if not spec.priority then + if frequency == Skill.Wake then + spec.priority = 3 + elseif frequency == Skill.Compulsory then + spec.priority = 2 + else + spec.priority = 1 + end + end + if type(spec.priority) == "number" then + for _, event in ipairs(skill.events) do + skill.priority_table[event] = spec.priority + end + elseif type(spec.priority) == "table" then + for event, priority in pairs(spec.priority) do + skill.priority_table[event] = priority + end + end + return skill +end + +---@class ActiveSkillSpec: SkillSpec +---@field can_use fun(self: ActiveSkill, player: Player): boolean +---@field card_filter fun(self: ActiveSkill, to_select: integer, selected: integer[], selected_targets: integer[]): boolean +---@field target_filter fun(self: ActiveSkill, to_select: integer, selected: integer[], selected_cards: integer[]): boolean +---@field feasible fun(self: ActiveSkill, selected: integer[], selected_targets: integer[]): boolean +---@field on_use fun(self: ActiveSkill, room: Room, cardUseEvent: CardUseStruct): boolean +---@field on_effect fun(self: ActiveSkill, room: Room, cardEffectEvent: CardEffectEvent): boolean + +---@param spec ActiveSkillSpec +---@return ActiveSkill +function fk.CreateActiveSkill(spec) + assert(type(spec.name) == "string") + local skill = ActiveSkill:new(spec.name) + if spec.can_use then skill.canUse = spec.can_use end + if spec.card_filter then skill.cardFilter = spec.card_filter end + if spec.target_filter then skill.targetFilter = spec.target_filter end + if spec.feasible then skill.feasible = spec.feasible end + if spec.on_use then skill.onUse = spec.on_use end + if spec.on_effect then skill.onEffect = spec.on_effect end + return skill +end + +---@class CardSpec: Card +---@field skill Skill + +local defaultCardSkill = fk.CreateActiveSkill{ + name = "default_card_skill", + on_use = function(self, room, use) + if not use.tos or #TargetGroup:getRealTargets(use.tos) == 0 then + use.tos = { { use.from } } + end + end +} + ---@param spec CardSpec ---@return BasicCard function fk.CreateBasicCard(spec) - assert(type(spec.name) == "string" or type(spec.class_name) == "string") - if not spec.name then spec.name = spec.class_name - elseif not spec.class_name then spec.class_name = spec.name end - if spec.suit then assert(type(spec.suit) == "number") end - if spec.number then assert(type(spec.number) == "number") end + assert(type(spec.name) == "string" or type(spec.class_name) == "string") + if not spec.name then spec.name = spec.class_name + elseif not spec.class_name then spec.class_name = spec.name end + if spec.suit then assert(type(spec.suit) == "number") end + if spec.number then assert(type(spec.number) == "number") end - local card = BasicCard:new(spec.name, spec.suit, spec.number) - return card + local card = BasicCard:new(spec.name, spec.suit, spec.number) + card.skill = spec.skill or defaultCardSkill + return card end ---@param spec CardSpec ---@return TrickCard function fk.CreateTrickCard(spec) - assert(type(spec.name) == "string" or type(spec.class_name) == "string") - if not spec.name then spec.name = spec.class_name - elseif not spec.class_name then spec.class_name = spec.name end - if spec.suit then assert(type(spec.suit) == "number") end - if spec.number then assert(type(spec.number) == "number") end + assert(type(spec.name) == "string" or type(spec.class_name) == "string") + if not spec.name then spec.name = spec.class_name + elseif not spec.class_name then spec.class_name = spec.name end + if spec.suit then assert(type(spec.suit) == "number") end + if spec.number then assert(type(spec.number) == "number") end - local card = TrickCard:new(spec.name, spec.suit, spec.number) - return card + local card = TrickCard:new(spec.name, spec.suit, spec.number) + card.skill = spec.skill or defaultCardSkill + return card end ---@param spec CardSpec ---@return DelayedTrickCard function fk.CreateDelayedTrickCard(spec) - assert(type(spec.name) == "string" or type(spec.class_name) == "string") - if not spec.name then spec.name = spec.class_name - elseif not spec.class_name then spec.class_name = spec.name end - if spec.suit then assert(type(spec.suit) == "number") end - if spec.number then assert(type(spec.number) == "number") end + assert(type(spec.name) == "string" or type(spec.class_name) == "string") + if not spec.name then spec.name = spec.class_name + elseif not spec.class_name then spec.class_name = spec.name end + if spec.suit then assert(type(spec.suit) == "number") end + if spec.number then assert(type(spec.number) == "number") end - local card = DelayedTrickCard:new(spec.name, spec.suit, spec.number) - return card + local card = DelayedTrickCard:new(spec.name, spec.suit, spec.number) + card.skill = spec.skill or defaultCardSkill + return card end ---@param spec CardSpec ---@return Weapon function fk.CreateWeapon(spec) - assert(type(spec.name) == "string" or type(spec.class_name) == "string") - if not spec.name then spec.name = spec.class_name - elseif not spec.class_name then spec.class_name = spec.name end - if spec.suit then assert(type(spec.suit) == "number") end - if spec.number then assert(type(spec.number) == "number") end - if spec.attack_range then assert(type(spec.attack_range) == "number" and spec.attack_range >= 0) end + assert(type(spec.name) == "string" or type(spec.class_name) == "string") + if not spec.name then spec.name = spec.class_name + elseif not spec.class_name then spec.class_name = spec.name end + if spec.suit then assert(type(spec.suit) == "number") end + if spec.number then assert(type(spec.number) == "number") end + if spec.attack_range then assert(type(spec.attack_range) == "number" and spec.attack_range >= 0) end - local card = Weapon:new(spec.name, spec.suit, spec.number, spec.attack_range) - return card + local card = Weapon:new(spec.name, spec.suit, spec.number, spec.attack_range) + card.skill = spec.skill or defaultCardSkill + return card end ---@param spec CardSpec ---@return Armor function fk.CreateArmor(spec) - assert(type(spec.name) == "string" or type(spec.class_name) == "string") - if not spec.name then spec.name = spec.class_name - elseif not spec.class_name then spec.class_name = spec.name end - if spec.suit then assert(type(spec.suit) == "number") end - if spec.number then assert(type(spec.number) == "number") end + assert(type(spec.name) == "string" or type(spec.class_name) == "string") + if not spec.name then spec.name = spec.class_name + elseif not spec.class_name then spec.class_name = spec.name end + if spec.suit then assert(type(spec.suit) == "number") end + if spec.number then assert(type(spec.number) == "number") end - local card = Armor:new(spec.name, spec.suit, spec.number) - return card + local card = Armor:new(spec.name, spec.suit, spec.number) + card.skill = spec.skill or defaultCardSkill + return card end ---@param spec CardSpec ---@return DefensiveRide function fk.CreateDefensiveRide(spec) - assert(type(spec.name) == "string" or type(spec.class_name) == "string") - if not spec.name then spec.name = spec.class_name - elseif not spec.class_name then spec.class_name = spec.name end - if spec.suit then assert(type(spec.suit) == "number") end - if spec.number then assert(type(spec.number) == "number") end + assert(type(spec.name) == "string" or type(spec.class_name) == "string") + if not spec.name then spec.name = spec.class_name + elseif not spec.class_name then spec.class_name = spec.name end + if spec.suit then assert(type(spec.suit) == "number") end + if spec.number then assert(type(spec.number) == "number") end - local card = DefensiveRide:new(spec.name, spec.suit, spec.number) - return card + local card = DefensiveRide:new(spec.name, spec.suit, spec.number) + card.skill = spec.skill or defaultCardSkill + return card end ---@param spec CardSpec ---@return OffensiveRide function fk.CreateOffensiveRide(spec) - assert(type(spec.name) == "string" or type(spec.class_name) == "string") - if not spec.name then spec.name = spec.class_name - elseif not spec.class_name then spec.class_name = spec.name end - if spec.suit then assert(type(spec.suit) == "number") end - if spec.number then assert(type(spec.number) == "number") end + assert(type(spec.name) == "string" or type(spec.class_name) == "string") + if not spec.name then spec.name = spec.class_name + elseif not spec.class_name then spec.class_name = spec.name end + if spec.suit then assert(type(spec.suit) == "number") end + if spec.number then assert(type(spec.number) == "number") end - local card = OffensiveRide:new(spec.name, spec.suit, spec.number) - return card + local card = OffensiveRide:new(spec.name, spec.suit, spec.number) + card.skill = spec.skill or defaultCardSkill + return card end ---@param spec CardSpec ---@return Treasure function fk.CreateTreasure(spec) - assert(type(spec.name) == "string" or type(spec.class_name) == "string") - if not spec.name then spec.name = spec.class_name - elseif not spec.class_name then spec.class_name = spec.name end - if spec.suit then assert(type(spec.suit) == "number") end - if spec.number then assert(type(spec.number) == "number") end + assert(type(spec.name) == "string" or type(spec.class_name) == "string") + if not spec.name then spec.name = spec.class_name + elseif not spec.class_name then spec.class_name = spec.name end + if spec.suit then assert(type(spec.suit) == "number") end + if spec.number then assert(type(spec.number) == "number") end - local card = Treasure:new(spec.name, spec.suit, spec.number) - return card -end - ----@param spec TriggerSkillSpec ----@return TriggerSkill -function fk.CreateTriggerSkill(spec) - assert(type(spec.name) == "string") - --assert(type(spec.on_trigger) == "function") - if spec.frequency then assert(type(spec.frequency) == "number") end - - local frequency = spec.frequency or Skill.NotFrequent - local skill = TriggerSkill:new(spec.name, frequency) - - if type(spec.events) == "number" then - table.insert(skill.events, spec.events) - elseif type(spec.events) == "table" then - table.insertTable(skill.events, spec.events) - end - - if type(spec.refresh_events) == "number" then - table.insert(skill.refresh_events, spec.refresh_events) - elseif type(spec.refresh_events) == "table" then - table.insertTable(skill.refresh_events, spec.refresh_events) - end - - if type(spec.global) == "boolean" then skill.global = spec.global end - - if spec.on_trigger then skill.trigger = spec.on_trigger end - - if spec.can_trigger then - skill.triggerable = spec.can_trigger - end - - if spec.can_refresh then - skill.canRefresh = spec.can_refresh - end - - if spec.on_refresh then - skill.refresh = spec.on_refresh - end - - if not spec.priority then - if frequency == Skill.Wake then - spec.priority = 3 - elseif frequency == Skill.Compulsory then - spec.priority = 2 - else - spec.priority = 1 - end - end - if type(spec.priority) == "number" then - for _, event in ipairs(skill.events) do - skill.priority_table[event] = spec.priority - end - elseif type(spec.priority) == "table" then - for event, priority in pairs(spec.priority) do - skill.priority_table[event] = priority - end - end - return skill + local card = Treasure:new(spec.name, spec.suit, spec.number) + card.skill = spec.skill or defaultCardSkill + return card end diff --git a/lua/freekill.lua b/lua/freekill.lua index 02704748..6fe2d094 100644 --- a/lua/freekill.lua +++ b/lua/freekill.lua @@ -10,7 +10,8 @@ class = require "middleclass" json = require "json" dofile "lua/lib/sha256.lua" -dofile "lua/core/util.lua" +local GroupUtils = require "core.util" +TargetGroup, AimGroup = table.unpack(GroupUtils) dofile "lua/core/debug.lua" math.randomseed(os.time()) diff --git a/lua/server/event.lua b/lua/server/event.lua index 3ed1abb5..7e7afc88 100644 --- a/lua/server/event.lua +++ b/lua/server/event.lua @@ -50,4 +50,24 @@ fk.EnterDying = 38 fk.Dying = 39 fk.AfterDying = 40 -fk.NumOfEvents = 41 +fk.PreCardUse = 41 +fk.AfterCardUseDeclared = 42 +fk.AfterCardTargetDeclared = 43 +fk.BeforeCardUseEffect = 44 +fk.CardUsing = 45 +fk.TargetSpecifying = 46 +fk.TargetConfirming = 47 +fk.TargetSpecified = 48 +fk.TargetConfirmed = 49 +fk.CardUseFinished = 50 + +fk.PreCardRespond = 51 +fk.CardResponding = 52 +fk.CardRespondFinished = 53 + +fk.PreCardEffect = 54 +fk.BeforeCardEffect = 55 +fk.CardEffecting = 56 +fk.CardEffectFinished = 57 + +fk.NumOfEvents = 58 diff --git a/lua/server/gamelogic.lua b/lua/server/gamelogic.lua index a5c86d1d..41adc7d6 100644 --- a/lua/server/gamelogic.lua +++ b/lua/server/gamelogic.lua @@ -8,274 +8,274 @@ local GameLogic = class("GameLogic") function GameLogic:initialize(room) - self.room = room - self.skill_table = {} -- TriggerEvent --> TriggerSkill[] - self.refresh_skill_table = {} - self.skills = {} -- skillName[] - self.event_stack = Stack:new() + self.room = room + self.skill_table = {} -- TriggerEvent --> TriggerSkill[] + self.refresh_skill_table = {} + self.skills = {} -- skillName[] + self.event_stack = Stack:new() - 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 - table.shuffle(self.room.players) - self:assignRoles() - self.room:adjustSeats() + -- default logic + table.shuffle(self.room.players) + self:assignRoles() + self.room:adjustSeats() - self:chooseGenerals() - self:prepareForStart() - self:action() + self:chooseGenerals() + self:prepareForStart() + self:action() 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 - room:broadcastProperty(p, "role") - else - room:notifyProperty(p, p, "role") - end + for i = 1, n do + local p = room.players[i] + p.role = roles[i] + if p.role == "lord" then + room:broadcastProperty(p, "role") + else + 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 - room.current = lord - 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") + 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 + room.current = lord + 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 + 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 = "" + 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:prepareForStart() - 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] + 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] + + 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 - players[#players].next = players[1] + room:broadcastProperty(p, "maxHp") + room:broadcastProperty(p, "hp") - 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 + -- TODO: add skills to player + end - 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: prepare drawPile + -- TODO: init cards in drawPile + local allCardIds = Fk:getAllCardIds() + table.shuffle(allCardIds) + room.draw_pile = allCardIds + for _, id in ipairs(room.draw_pile) do + self.room:setCardArea(id, Card.DrawPile) + end - -- TODO: add skills to player - end - - -- TODO: prepare drawPile - -- TODO: init cards in drawPile - local allCardIds = Fk:getAllCardIds() - table.shuffle(allCardIds) - room.draw_pile = allCardIds - for _, id in ipairs(room.draw_pile) do - self.room:setCardArea(id, Card.DrawPile) - end - - 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 end function GameLogic:action() - self:trigger(fk.GameStart) - local room = self.room + self:trigger(fk.GameStart) + local room = self.room - for _, p in ipairs(room.players) do - self:trigger(fk.DrawInitialCards, p, { num = 4 }) - end + for _, p in ipairs(room.players) do + self:trigger(fk.DrawInitialCards, p, { num = 4 }) + end - while true do - self:trigger(fk.TurnStart, room.current) - if room.game_finished then break end - room.current = room.current:getNextAlive() - end + while true do + self:trigger(fk.TurnStart, room.current) + if room.game_finished then break end + room.current = room.current:getNextAlive() + end end ---@param skill TriggerSkill function GameLogic:addTriggerSkill(skill) - if skill == nil or table.contains(self.skills, skill.name) then - return - end + if skill == nil or table.contains(self.skills, skill.name) then + return + end - table.insert(self.skills, skill.name) + 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) + 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) + 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) + 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 + 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 ---@param event Event ---@param target ServerPlayer ---@param data any function GameLogic:trigger(event, target, data) - local room = self.room - local broken = false - local skills = self.skill_table[event] or {} - local skills_to_refresh = self.refresh_skill_table[event] or {} - local player = target + local room = self.room + local broken = false + local skills = self.skill_table[event] or {} + local skills_to_refresh = self.refresh_skill_table[event] or {} + local player = target - self.event_stack:push({event, target, data}) + self.event_stack:push({event, target, data}) - if target == nil then - for _, skill in ipairs(skills_to_refresh) do - if skill:canRefresh(event, target, player, data) then - skill:refresh(event, target, player, data) - end - end - - for _, skill in ipairs(skills) do - if skill:triggerable(event, target, player, data) then - broken = skill:trigger(event, target, player, data) - if broken then break end - end - end - - self.event_stack:pop() - return broken + if target == nil then + for _, skill in ipairs(skills_to_refresh) do + if skill:canRefresh(event, target, player, data) then + skill:refresh(event, target, player, data) + end end - 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 - - ---@param a TriggerSkill - ---@param b TriggerSkill - local compare_func = function (a, b) - return a.priority_table[event] > b.priority_table[event] - end - table.sort(skills, compare_func) - - repeat do - local triggerable_skills = {} ---@type table - local priority_table = {} ---@type number[] - for _, skill in ipairs(skills) do - if skill:triggerable(event, target, player, data) then - local priority = skill.priority_table[event] - if triggerable_skills[priority] == nil then - triggerable_skills[priority] = {} - end - table.insert(triggerable_skills[priority], skill) - if not table.contains(priority_table, priority) then - table.insert(priority_table, priority) - end - end - end - - for _, priority in ipairs(priority_table) do - local triggerables = triggerable_skills[priority] - local skill_names = {} ---@type string[] - for _, skill in ipairs(triggerables) do - table.insert(skill_names, skill.name) - end - - while #skill_names > 0 do - local skill_name = room:askForChoice(player, skill_names, "trigger") - local skill = triggerables[table.indexOf(skill_names, skill_name)] - broken = skill:trigger(event, target, player, data) - if broken then break end - table.removeOne(skill_names, skill_name) - table.removeOne(triggerables, skill) - end - end - + for _, skill in ipairs(skills) do + if skill:triggerable(event, target, player, data) then + broken = skill:trigger(event, target, player, data) if broken then break end - - player = player.next - end until player == target + end + end self.event_stack:pop() return broken + end + + 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 + + ---@param a TriggerSkill + ---@param b TriggerSkill + local compare_func = function (a, b) + return a.priority_table[event] > b.priority_table[event] + end + table.sort(skills, compare_func) + + repeat do + local triggerable_skills = {} ---@type table + local priority_table = {} ---@type number[] + for _, skill in ipairs(skills) do + if skill:triggerable(event, target, player, data) then + local priority = skill.priority_table[event] + if triggerable_skills[priority] == nil then + triggerable_skills[priority] = {} + end + table.insert(triggerable_skills[priority], skill) + if not table.contains(priority_table, priority) then + table.insert(priority_table, priority) + end + end + end + + for _, priority in ipairs(priority_table) do + local triggerables = triggerable_skills[priority] + local skill_names = {} ---@type string[] + for _, skill in ipairs(triggerables) do + table.insert(skill_names, skill.name) + end + + while #skill_names > 0 do + local skill_name = room:askForChoice(player, skill_names, "trigger") + local skill = triggerables[table.indexOf(skill_names, skill_name)] + broken = skill:trigger(event, target, player, data) + if broken then break end + table.removeOne(skill_names, skill_name) + table.removeOne(triggerables, skill) + end + end + + if broken then break end + + player = player.next + end until player == target + + self.event_stack:pop() + return broken end return GameLogic diff --git a/lua/server/lobby.lua b/lua/server/lobby.lua index 2b5064f8..103243db 100644 --- a/lua/server/lobby.lua +++ b/lua/server/lobby.lua @@ -6,63 +6,63 @@ fk.lobby_callback = {} local db = fk.ServerInstance:getDatabase() function Lobby:initialize(_lobby) - self.lobby = _lobby - self.lobby.callback = function(_self, command, jsonData) - local cb = fk.lobby_callback[command] - if (type(cb) == "function") then - cb(jsonData) - else - print("Lobby error: Unknown command " .. command); - end + self.lobby = _lobby + self.lobby.callback = function(_self, command, jsonData) + local cb = fk.lobby_callback[command] + if (type(cb) == "function") then + cb(jsonData) + else + print("Lobby error: Unknown command " .. command); end + end end fk.lobby_callback["UpdateAvatar"] = function(jsonData) - -- jsonData: [ int uid, string newavatar ] - local data = json.decode(jsonData) - local id, avatar = data[1], data[2] - local sql = "UPDATE userinfo SET avatar='%s' WHERE id=%d;" - Sql.exec(db, string.format(sql, avatar, id)) - local player = fk.ServerInstance:findPlayer(id) - player:setAvatar(avatar) - player:doNotify("UpdateAvatar", avatar) + -- jsonData: [ int uid, string newavatar ] + local data = json.decode(jsonData) + local id, avatar = data[1], data[2] + local sql = "UPDATE userinfo SET avatar='%s' WHERE id=%d;" + Sql.exec(db, string.format(sql, avatar, id)) + local player = fk.ServerInstance:findPlayer(id) + player:setAvatar(avatar) + player:doNotify("UpdateAvatar", avatar) end fk.lobby_callback["UpdatePassword"] = function(jsonData) - -- jsonData: [ int uid, string oldpassword, int newpassword ] - local data = json.decode(jsonData) - local id, old, new = data[1], data[2], data[3] - local sql_find = "SELECT password FROM userinfo WHERE id=%d;" - local sql_update = "UPDATE userinfo SET password='%s' WHERE id=%d;" + -- jsonData: [ int uid, string oldpassword, int newpassword ] + local data = json.decode(jsonData) + local id, old, new = data[1], data[2], data[3] + local sql_find = "SELECT password FROM userinfo WHERE id=%d;" + local sql_update = "UPDATE userinfo SET password='%s' WHERE id=%d;" - local passed = false - local result = Sql.exec_select(db, string.format(sql_find, id)) - passed = (result["password"][1] == sha256(old)) - if passed then - Sql.exec(db, string.format(sql_update, sha256(new), id)) - end + local passed = false + local result = Sql.exec_select(db, string.format(sql_find, id)) + passed = (result["password"][1] == sha256(old)) + if passed then + Sql.exec(db, string.format(sql_update, sha256(new), id)) + end - local player = fk.ServerInstance:findPlayer(tonumber(id)) - player:doNotify("UpdatePassword", passed and "1" or "0") + local player = fk.ServerInstance:findPlayer(tonumber(id)) + player:doNotify("UpdatePassword", passed and "1" or "0") end fk.lobby_callback["CreateRoom"] = function(jsonData) - -- jsonData: [ int uid, string name, int capacity ] - local data = json.decode(jsonData) - local owner = fk.ServerInstance:findPlayer(tonumber(data[1])) - local roomName = data[2] - local capacity = data[3] - fk.ServerInstance:createRoom(owner, roomName, capacity) + -- jsonData: [ int uid, string name, int capacity ] + local data = json.decode(jsonData) + local owner = fk.ServerInstance:findPlayer(tonumber(data[1])) + local roomName = data[2] + local capacity = data[3] + fk.ServerInstance:createRoom(owner, roomName, capacity) end fk.lobby_callback["EnterRoom"] = function(jsonData) - -- jsonData: [ int uid, int roomId ] - local data = json.decode(jsonData) - local player = fk.ServerInstance:findPlayer(tonumber(data[1])) - local room = fk.ServerInstance:findRoom(tonumber(data[2])) - room:addPlayer(player) + -- jsonData: [ int uid, int roomId ] + local data = json.decode(jsonData) + local player = fk.ServerInstance:findPlayer(tonumber(data[1])) + local room = fk.ServerInstance:findRoom(tonumber(data[2])) + room:addPlayer(player) end function CreateRoom(_room) - LobbyInstance = Lobby:new(_room) + LobbyInstance = Lobby:new(_room) end diff --git a/lua/server/room.lua b/lua/server/room.lua index 82a5db69..aab919bd 100644 --- a/lua/server/room.lua +++ b/lua/server/room.lua @@ -21,75 +21,75 @@ fk.room_callback = {} ---@param _room fk.Room function Room:initialize(_room) - self.room = _room - self.room.callback = function(_self, command, jsonData) - local cb = fk.room_callback[command] - if (type(cb) == "function") then - cb(jsonData) - else - print("Lobby error: Unknown command " .. command); - end + self.room = _room + self.room.callback = function(_self, command, jsonData) + local cb = fk.room_callback[command] + if (type(cb) == "function") then + cb(jsonData) + else + print("Lobby error: Unknown command " .. command); end + end - self.room.startGame = function(_self) - self:run() - end + self.room.startGame = function(_self) + self:run() + end - self.players = {} - self.alive_players = {} - self.current = nil - self.game_finished = false - self.timeout = _room:getTimeout() - self.tag = {} - self.draw_pile = {} - self.discard_pile = {} - self.processing_area = {} - self.void = {} - self.card_place = {} + self.players = {} + self.alive_players = {} + self.current = nil + self.game_finished = false + self.timeout = _room:getTimeout() + self.tag = {} + self.draw_pile = {} + self.discard_pile = {} + self.processing_area = {} + self.void = {} + self.card_place = {} end -- When this function returns, the Room(C++) thread stopped. function Room:run() - for _, p in fk.qlist(self.room:getPlayers()) do - local player = ServerPlayer:new(p) - player.state = p:getStateString() - player.room = self - table.insert(self.players, player) - end + for _, p in fk.qlist(self.room:getPlayers()) do + local player = ServerPlayer:new(p) + player.state = p:getStateString() + player.room = self + table.insert(self.players, player) + end - self.logic = GameLogic:new(self) - self.logic:run() + self.logic = GameLogic:new(self) + self.logic:run() end ---@param player ServerPlayer ---@param property string function Room:broadcastProperty(player, property) - for _, p in ipairs(self.players) do - self:notifyProperty(p, player, property) - end + for _, p in ipairs(self.players) do + self:notifyProperty(p, player, property) + end end ---@param p ServerPlayer ---@param player ServerPlayer ---@param property string function Room:notifyProperty(p, player, property) - p:doNotify("PropertyUpdate", json.encode{ - player:getId(), - property, - player[property], - }) + p:doNotify("PropertyUpdate", json.encode{ + player:getId(), + property, + player[property], + }) end ---@param command string ---@param jsonData string ---@param players ServerPlayer[] @ default all players function Room:doBroadcastNotify(command, jsonData, players) - players = players or self.players - local tolist = fk.SPlayerList() - for _, p in ipairs(players) do - tolist:append(p.serverplayer) - end - self.room:doBroadcastNotify(tolist, command, jsonData) + players = players or self.players + local tolist = fk.SPlayerList() + for _, p in ipairs(players) do + tolist:append(p.serverplayer) + end + self.room:doBroadcastNotify(tolist, command, jsonData) end ---@param player ServerPlayer @@ -98,261 +98,261 @@ end ---@param wait boolean @ default true ---@return string | nil function Room:doRequest(player, command, jsonData, wait) - if wait == nil then wait = true end - player:doRequest(command, jsonData, self.timeout) + if wait == nil then wait = true end + player:doRequest(command, jsonData, self.timeout) - if wait then - return player:waitForReply(self.timeout) - end + if wait then + return player:waitForReply(self.timeout) + end end ---@param command string ---@param players ServerPlayer[] 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 + 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 + 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 ---@param players ServerPlayer | ServerPlayer[] ---@param command string function Room:notifyMoveFocus(players, command) - if (players.class) then - players = {players} - end + if (players.class) then + players = {players} + end - local ids = {} - for _, p in ipairs(players) do - table.insert(ids, p:getId()) - end + local ids = {} + for _, p in ipairs(players) do + table.insert(ids, p:getId()) + end - self:doBroadcastNotify("MoveFocus", json.encode{ - ids, - command - }) + self:doBroadcastNotify("MoveFocus", json.encode{ + ids, + command + }) end function Room:adjustSeats() - local players = {} - local p = 0 + local players = {} + local p = 0 - for i = 1, #self.players do - if self.players[i].role == "lord" then - p = i - break - end - end - for j = p, #self.players do - table.insert(players, self.players[j]) - end - for j = 1, p - 1 do - table.insert(players, self.players[j]) + for i = 1, #self.players do + if self.players[i].role == "lord" then + p = i + break end + end + for j = p, #self.players do + table.insert(players, self.players[j]) + end + for j = 1, p - 1 do + table.insert(players, self.players[j]) + end - self.players = players + self.players = players - local player_circle = {} - for i = 1, #self.players do - self.players[i].seat = i - table.insert(player_circle, self.players[i]:getId()) - end + local player_circle = {} + for i = 1, #self.players do + self.players[i].seat = i + table.insert(player_circle, self.players[i]:getId()) + end - self:doBroadcastNotify("ArrangeSeats", json.encode(player_circle)) + self:doBroadcastNotify("ArrangeSeats", json.encode(player_circle)) end function Room:shuffleDrawPile() - if #self.draw_pile + #self.discard_pile == 0 then - return - end + if #self.draw_pile + #self.discard_pile == 0 then + return + end - table.insertTable(self.draw_pile, self.discard_pile) - for _, id in ipairs(self.discard_pile) do - self:setCardArea(id, Card.DrawPile) - end - self.discard_pile = {} - table.shuffle(self.draw_pile) + table.insertTable(self.draw_pile, self.discard_pile) + for _, id in ipairs(self.discard_pile) do + self:setCardArea(id, Card.DrawPile) + end + self.discard_pile = {} + table.shuffle(self.draw_pile) end ---@param num integer ---@param from string ---@return integer[] function Room:getNCards(num, from) - from = from or "top" - assert(from == "top" or from == "bottom") + from = from or "top" + assert(from == "top" or from == "bottom") - local cardIds = {} - while num > 0 do - if #self.draw_pile < 1 then - self:shuffleDrawPile() - end - - local index = from == "top" and 1 or #self.draw_pile - table.insert(cardIds, self.draw_pile[index]) - table.remove(self.draw_pile, index) - - num = num - 1 + local cardIds = {} + while num > 0 do + if #self.draw_pile < 1 then + self:shuffleDrawPile() end - return cardIds + local index = from == "top" and 1 or #self.draw_pile + table.insert(cardIds, self.draw_pile[index]) + table.remove(self.draw_pile, index) + + num = num - 1 + end + + return cardIds end ---@param cardId integer ---@param cardArea CardArea function Room:setCardArea(cardId, cardArea) - self.card_place[cardId] = cardArea + self.card_place[cardId] = cardArea end ---@param cardId integer ---@return CardArea function Room:getCardArea(cardId) - return self.card_place[cardId] or Card.Unknown + return self.card_place[cardId] or Card.Unknown end ---@param players ServerPlayer[] ---@param card_moves CardsMoveStruct[] ---@param forceVisible boolean function Room:notifyMoveCards(players, card_moves, forceVisible) - 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 - -- local to = self:getPlayerById(move.to) + 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 + -- local to = self:getPlayerById(move.to) - -- forceVisible make the move visible - -- FIXME: move.moveInfo is an array, fix this - move.moveVisible = (forceVisible) - -- if move is relevant to player, it should be open - or ((move.from == p:getId()) or (move.to == p:getId() and move.toArea ~= Card.PlayerSpecial)) - -- cards move from/to equip/judge/discard/processing should be open - or move.moveInfo.fromArea == Card.PlayerEquip - or move.toArea == Card.PlayerEquip - or move.moveInfo.fromArea == Card.PlayerJudge - or move.toArea == Card.PlayerJudge - or move.moveInfo.fromArea == Card.DiscardPile - or move.toArea == Card.DiscardPile - or move.moveInfo.fromArea == Card.Processing - or move.toArea == Card.Processing - -- TODO: PlayerSpecial - - if not move.moveVisible then - for _, info in ipairs(move.moveInfo) do - info.cardId = -1 - end - end + -- forceVisible make the move visible + -- FIXME: move.moveInfo is an array, fix this + move.moveVisible = (forceVisible) + -- if move is relevant to player, it should be open + or ((move.from == p:getId()) or (move.to == p:getId() and move.toArea ~= Card.PlayerSpecial)) + -- cards move from/to equip/judge/discard/processing should be open + or move.moveInfo.fromArea == Card.PlayerEquip + or move.toArea == Card.PlayerEquip + or move.moveInfo.fromArea == Card.PlayerJudge + or move.toArea == Card.PlayerJudge + or move.moveInfo.fromArea == Card.DiscardPile + or move.toArea == Card.DiscardPile + or move.moveInfo.fromArea == Card.Processing + or move.toArea == Card.Processing + -- TODO: PlayerSpecial + + if not move.moveVisible then + for _, info in ipairs(move.moveInfo) do + info.cardId = -1 end - p:doNotify("MoveCards", json.encode(arg)) + end end + p:doNotify("MoveCards", json.encode(arg)) + end end ---@vararg CardsMoveInfo ---@return boolean function Room:moveCards(...) - ---@type CardsMoveStruct[] - local cardsMoveStructs = {} - local infoCheck = function(info) - assert(table.contains({ Card.PlayerHand, Card.PlayerEquip, Card.PlayerJudge, Card.PlayerSpecial, Card.Processing, Card.DrawPile, Card.DiscardPile, Card.Void }, info.toArea)) - assert(info.toArea ~= Card.PlayerSpecial or type(info.specialName) == "string") - assert(type(info.moveReason) == "number") + ---@type CardsMoveStruct[] + local cardsMoveStructs = {} + local infoCheck = function(info) + assert(table.contains({ Card.PlayerHand, Card.PlayerEquip, Card.PlayerJudge, Card.PlayerSpecial, Card.Processing, Card.DrawPile, Card.DiscardPile, Card.Void }, info.toArea)) + assert(info.toArea ~= Card.PlayerSpecial or type(info.specialName) == "string") + assert(type(info.moveReason) == "number") + end + + for _, cardsMoveInfo in ipairs({...}) do + if #cardsMoveInfo.ids > 0 then + infoCheck(cardsMoveInfo) + + ---@type MoveInfo[] + local infos = {} + for _, id in ipairs(cardsMoveInfo.ids) do + table.insert(infos, { cardId = id, fromArea = self:getCardArea(id) }) + end + + ---@type CardsMoveStruct + local cardsMoveStruct = { + moveInfo = infos, + from = cardsMoveInfo.from, + to = cardsMoveInfo.to, + toArea = cardsMoveInfo.toArea, + moveReason = cardsMoveInfo.moveReason, + proposer = cardsMoveInfo.proposer, + skillName = cardsMoveInfo.skillName, + moveVisible = cardsMoveInfo.moveVisible, + specialName = cardsMoveInfo.specialName, + specialVisible = cardsMoveInfo.specialVisible, + } + + table.insert(cardsMoveStructs, cardsMoveStruct) end + end - for _, cardsMoveInfo in ipairs({...}) do - if #cardsMoveInfo.ids > 0 then - infoCheck(cardsMoveInfo) + if #cardsMoveStructs < 1 then + return false + end - ---@type MoveInfo[] - local infos = {} - for _, id in ipairs(cardsMoveInfo.ids) do - table.insert(infos, { cardId = id, fromArea = self:getCardArea(id) }) - end - - ---@type CardsMoveStruct - local cardsMoveStruct = { - moveInfo = infos, - from = cardsMoveInfo.from, - to = cardsMoveInfo.to, - toArea = cardsMoveInfo.toArea, - moveReason = cardsMoveInfo.moveReason, - proposer = cardsMoveInfo.proposer, - skillName = cardsMoveInfo.skillName, - moveVisible = cardsMoveInfo.moveVisible, - specialName = cardsMoveInfo.specialName, - specialVisible = cardsMoveInfo.specialVisible, - } - - table.insert(cardsMoveStructs, cardsMoveStruct) + if self.logic:trigger(fk.BeforeCardsMove, nil, cardsMoveStructs) then + return false + end + + self:notifyMoveCards(self.players, cardsMoveStructs) + + for _, data in ipairs(cardsMoveStructs) do + if #data.moveInfo > 0 then + infoCheck(data) + + ---@param info MoveInfo + for _, info in ipairs(data.moveInfo) do + local realFromArea = self:getCardArea(info.cardId) + local playerAreas = { Player.Hand, Player.Equip, Player.Judge, Player.Special } + + if table.contains(playerAreas, realFromArea) and data.from then + self:getPlayerById(data.from):removeCards(realFromArea, { info.cardId }, data.specialName) + elseif realFromArea ~= Card.Unknown then + local fromAreaIds = {} + if realFromArea == Card.Processing then + fromAreaIds = self.processing_area + elseif realFromArea == Card.DrawPile then + fromAreaIds = self.draw_pile + elseif realFromArea == Card.DiscardPile then + fromAreaIds = self.discard_pile + elseif realFromArea == Card.Void then + fromAreaIds = self.void + end + + table.removeOne(fromAreaIds, info.cardId) end - end - if #cardsMoveStructs < 1 then - return false - end + if table.contains(playerAreas, data.toArea) and data.to then + self:getPlayerById(data.to):addCards(data.toArea, { info.cardId }, data.specialName) + else + local toAreaIds = {} + if data.toArea == Card.Processing then + toAreaIds = self.processing_area + elseif data.toArea == Card.DrawPile then + toAreaIds = self.draw_pile + elseif data.toArea == Card.DiscardPile then + toAreaIds = self.discard_pile + elseif data.toArea == Card.Void then + toAreaIds = self.void + end - if self.logic:trigger(fk.BeforeCardsMove, nil, cardsMoveStructs) then - return false - end - - self:notifyMoveCards(self.players, cardsMoveStructs) - - for _, data in ipairs(cardsMoveStructs) do - if #data.moveInfo > 0 then - infoCheck(data) - - ---@param info MoveInfo - for _, info in ipairs(data.moveInfo) do - local realFromArea = self:getCardArea(info.cardId) - local playerAreas = { Player.Hand, Player.Equip, Player.Judge, Player.Special } - - if table.contains(playerAreas, realFromArea) and data.from then - self:getPlayerById(data.from):removeCards(realFromArea, { info.cardId }, data.specialName) - elseif realFromArea ~= Card.Unknown then - local fromAreaIds = {} - if realFromArea == Card.Processing then - fromAreaIds = self.processing_area - elseif realFromArea == Card.DrawPile then - fromAreaIds = self.draw_pile - elseif realFromArea == Card.DiscardPile then - fromAreaIds = self.discard_pile - elseif realFromArea == Card.Void then - fromAreaIds = self.void - end - - table.removeOne(fromAreaIds, info.cardId) - end - - if table.contains(playerAreas, data.toArea) and data.to then - self:getPlayerById(data.to):addCards(data.toArea, { info.cardId }, data.specialName) - else - local toAreaIds = {} - if data.toArea == Card.Processing then - toAreaIds = self.processing_area - elseif data.toArea == Card.DrawPile then - toAreaIds = self.draw_pile - elseif data.toArea == Card.DiscardPile then - toAreaIds = self.discard_pile - elseif data.toArea == Card.Void then - toAreaIds = self.void - end - - table.insert(toAreaIds, toAreaIds == Card.DrawPile and 1 or #toAreaIds + 1, info.cardId) - end - self:setCardArea(info.cardId, data.toArea) - end + table.insert(toAreaIds, toAreaIds == Card.DrawPile and 1 or #toAreaIds + 1, info.cardId) end + self:setCardArea(info.cardId, data.toArea) + end end + end - self.logic:trigger(fk.AfterCardsMove, nil, cardsMoveStructs) - return true + self.logic:trigger(fk.AfterCardsMove, nil, cardsMoveStructs) + return true end ---@param player ServerPlayer @@ -361,17 +361,17 @@ end ---@param fromPlace "top"|"bottom" ---@return integer[] function Room:drawCards(player, num, skillName, fromPlace) - local topCards = self:getNCards(num, fromPlace) - self:moveCards({ - ids = topCards, - to = player:getId(), - toArea = Card.PlayerHand, - moveReason = fk.ReasonDraw, - proposer = player:getId(), - skillName = skillName, - }) + local topCards = self:getNCards(num, fromPlace) + self:moveCards({ + ids = topCards, + to = player:getId(), + toArea = Card.PlayerHand, + moveReason = fk.ReasonDraw, + proposer = player:getId(), + skillName = skillName, + }) - return { table.unpack(topCards) } + return { table.unpack(topCards) } end ---@param player ServerPlayer @@ -380,145 +380,161 @@ end ---@param includeEquip boolean ---@param skillName string function Room:askForDiscard(player, minNum, maxNum, includeEquip, skillName) - if minNum < 1 then - return nil - end + if minNum < 1 then + return nil + end - local hands = player:getCardIds(Player.Hand) - local toDiscard = {} - for i = 1, minNum do - local randomId = hands[math.random(1, #hands)] - table.insert(toDiscard, randomId) - table.removeOne(hands, randomId) - end + local hands = player:getCardIds(Player.Hand) + local toDiscard = {} + for i = 1, minNum do + local randomId = hands[math.random(1, #hands)] + table.insert(toDiscard, randomId) + table.removeOne(hands, randomId) + end - self:moveCards({ - ids = toDiscard, - from = player:getId(), - toArea = Card.DiscardPile, - moveReason = fk.ReasonDiscard, - proposer = player:getId(), - skillName = skillName - }) + self:moveCards({ + ids = toDiscard, + from = player:getId(), + toArea = Card.DiscardPile, + moveReason = fk.ReasonDiscard, + proposer = player:getId(), + skillName = skillName + }) end ---@param id integer ---@return ServerPlayer function Room:getPlayerById(id) - assert(type(id) == "number") + assert(type(id) == "number") - for _, p in ipairs(self.players) do - if p:getId() == id then - return p - end + for _, p in ipairs(self.players) do + if p:getId() == id then + return p end + end - error("cannot find player by " .. id) + error("cannot find player by " .. id) +end + +---@param playerIds integer[] +function Room:sortPlayersByAction(playerIds) + +end + +function Room:deadPlayerFilter(playerIds) + local newPlayerIds = {} + for _, playerId in ipairs(playerIds) do + if self:getPlayerById(playerId):isAlive() then + table.insert(newPlayerIds, playerId) + end + end + + return newPlayerIds end ---@param sortBySeat boolean ---@return ServerPlayer[] function Room:getAlivePlayers(sortBySeat) - sortBySeat = sortBySeat or true + sortBySeat = sortBySeat or true - local alivePlayers = {} - for _, player in ipairs(self.players) do - if player:isAlive() then - table.insert(alivePlayers, player) - end + local alivePlayers = {} + for _, player in ipairs(self.players) do + if player:isAlive() then + table.insert(alivePlayers, player) end + end - return alivePlayers + return alivePlayers end ---@param player ServerPlayer ---@param sortBySeat boolean ---@return ServerPlayer[] function Room:getOtherPlayers(player, sortBySeat) - local alivePlayers = self:getAlivePlayers(sortBySeat) - for _, p in ipairs(alivePlayers) do - if p:getId() == player:getId() then - table.removeOne(alivePlayers, player) - break - end + local alivePlayers = self:getAlivePlayers(sortBySeat) + for _, p in ipairs(alivePlayers) do + if p:getId() == player:getId() then + table.removeOne(alivePlayers, player) + break end + end - return alivePlayers + return alivePlayers end ---@return ServerPlayer | null 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 + 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 + return nil end ---@param expect ServerPlayer ---@return ServerPlayer[] function Room:getOtherPlayers(expect) - local ret = {table.unpack(self.players)} - table.removeOne(ret, expect) - return ret + local ret = {table.unpack(self.players)} + table.removeOne(ret, expect) + return ret end ---@param player ServerPlayer ---@param generals string[] ---@return string function Room:askForGeneral(player, generals) - local command = "AskForGeneral" - self:notifyMoveFocus(player, command) + local command = "AskForGeneral" + self:notifyMoveFocus(player, command) - if #generals == 1 then return generals[1] end - local defaultChoice = generals[1] + 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 + 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 + return defaultChoice end function Room:gameOver() - self.game_finished = true - -- dosomething - self.room:gameOver() + self.game_finished = true + -- dosomething + self.room:gameOver() end ---@param player ServerPlayer ---@param choices string[] ---@param skill_name string function Room:askForChoice(player, choices, skill_name, data) - if #choices == 1 then return choices[1] end - local command = "AskForChoice" - self:notifyMoveFocus(player, skill_name) - local result = self:doRequest(player, command, json.encode{ - choices, skill_name - }) - if result == "" then result = choices[1] end - return result + if #choices == 1 then return choices[1] end + local command = "AskForChoice" + self:notifyMoveFocus(player, skill_name) + local result = self:doRequest(player, command, json.encode{ + choices, skill_name + }) + if result == "" then result = choices[1] end + return result end ---@param player ServerPlayer ---@param skill_name string ---@return boolean function Room:askForSkillInvoke(player, skill_name, data) - local command = "AskForSkillInvoke" - self:notifyMoveFocus(player, skill_name) - local invoked = false - local result = self:doRequest(player, command, skill_name) - if result ~= "" then invoked = true end - return invoked + local command = "AskForSkillInvoke" + self:notifyMoveFocus(player, skill_name) + local invoked = false + local result = self:doRequest(player, command, skill_name) + if result ~= "" then invoked = true end + return invoked end ---@param player ServerPlayer @@ -528,39 +544,39 @@ end ---@param damageStruct DamageStruct|null ---@return boolean function Room:changeHp(player, num, reason, skillName, damageStruct) - if num == 0 then - return false - end - assert(reason == nil or table.contains({ "loseHp", "damage", "recover" }, reason)) + if num == 0 then + return false + end + assert(reason == nil or table.contains({ "loseHp", "damage", "recover" }, reason)) - ---@type HpChangedData - local data = { - num = num, - reason = reason, - skillName = skillName, + ---@type HpChangedData + local data = { + num = num, + reason = reason, + skillName = skillName, + } + + if self.logic:trigger(fk.BeforeHpChanged, player, data) then + return false + end + + assert(not (data.reason == "recover" and data.num < 0)) + player.hp = math.min(player.hp + data.num, player.maxHp) + + self.logic:trigger(fk.HpChanged, player, data) + + if player.hp < 1 then + ---@type DyingStruct + local dyingStruct = { + who = player:getId(), + damage = damageStruct, } + self:enterDying(dyingStruct) + elseif player.dying then + player.dying = false + end - if self.logic:trigger(fk.BeforeHpChanged, player, data) then - return false - end - - assert(not (data.reason == "recover" and data.num < 0)) - player.hp = math.min(player.hp + data.num, player.maxHp) - - self.logic:trigger(fk.HpChanged, player, data) - - if player.hp < 1 then - ---@type DyingStruct - local dyingStruct = { - who = player:getId(), - damage = damageStruct, - } - self:enterDying(dyingStruct) - elseif player.dying then - player.dying = false - end - - return true + return true end ---@param player ServerPlayer @@ -568,216 +584,437 @@ end ---@param skillName string ---@return boolean function Room:loseHp(player, num, skillName) - if num == nil then - num = 1 - elseif num < 1 then - return false - end + if num == nil then + num = 1 + elseif num < 1 then + return false + end - ---@type HpLostData - local data = { - num = num, - skillName = skillName, - } - if self.logic:trigger(fk.PreHpLost, player, data) or data.num < 1 then - return false - end + ---@type HpLostData + local data = { + num = num, + skillName = skillName, + } + if self.logic:trigger(fk.PreHpLost, player, data) or data.num < 1 then + return false + end - if not self:changeHp(player, -num, "loseHp", skillName) then - return false - end + if not self:changeHp(player, -num, "loseHp", skillName) then + return false + end - self.logic:trigger(fk.HpLost, player, data) - return true + self.logic:trigger(fk.HpLost, player, data) + return true end ---@param player ServerPlayer ---@param num integer ---@return boolean function Room:changeMaxHp(player, num) - if num == 0 then - return false - end + if num == 0 then + return false + end - player.maxHp = math.max(player.maxHp + num, 0) - local diff = player.hp - player.maxHp - if diff > 0 then - if not self:changeHp(player, -diff) then - player.hp = player.hp - diff - end + player.maxHp = math.max(player.maxHp + num, 0) + local diff = player.hp - player.maxHp + if diff > 0 then + if not self:changeHp(player, -diff) then + player.hp = player.hp - diff end + end - if player.maxHp == 0 then - self:killPlayer({ who = player:getId() }) - end + if player.maxHp == 0 then + self:killPlayer({ who = player:getId() }) + end - self.logic:trigger(fk.MaxHpChanged, player, { num = num }) - return true + self.logic:trigger(fk.MaxHpChanged, player, { num = num }) + return true end ---@param damageStruct DamageStruct ---@return boolean function Room:damage(damageStruct) - if damageStruct.damage < 1 then - return false + if damageStruct.damage < 1 then + return false + end + + assert(type(damageStruct.to) == "number") + + local stages = { + [fk.PreDamage] = damageStruct.from, + [fk.DamageCaused] = damageStruct.from, + [fk.DamageInflicted] = damageStruct.to, + } + + for event, playerId in ipairs(stages) do + local player = playerId and self:getPlayerById(playerId) or nil + if self.logic:trigger(event, player, damageStruct) or damageStruct.damage < 1 then + return false end assert(type(damageStruct.to) == "number") + end - local stages = { - [fk.PreDamage] = damageStruct.from, - [fk.DamageCaused] = damageStruct.from, - [fk.DamageInflicted] = damageStruct.to, - } + assert(self:getPlayerById(damageStruct.to)) + local victim = self:getPlayerById(damageStruct.to) + if not victim:isAlive() then + return false + end - for event, playerId in ipairs(stages) do - local player = playerId and self:getPlayerById(playerId) or nil - if self.logic:trigger(event, player, damageStruct) or damageStruct.damage < 1 then - return false - end + if not self:changeHp(victim, -damageStruct.damage, "damage", damageStruct.skillName, damageStruct) then + return false + end - assert(type(damageStruct.to) == "number") - end + stages = { + [fk.Damage] = damageStruct.from, + [fk.Damaged] = damageStruct.to, + [fk.DamageFinished] = damageStruct.from, + } - assert(self:getPlayerById(damageStruct.to)) - local victim = self:getPlayerById(damageStruct.to) - if not victim:isAlive() then - return false - end + for event, playerId in ipairs(stages) do + local player = playerId and self:getPlayerById(playerId) or nil + self.logic:trigger(event, player, damageStruct) + end - if not self:changeHp(victim, -damageStruct.damage, "damage", damageStruct.skillName, damageStruct) then - return false - end - - stages = { - [fk.Damage] = damageStruct.from, - [fk.Damaged] = damageStruct.to, - [fk.DamageFinished] = damageStruct.from, - } - - for event, playerId in ipairs(stages) do - local player = playerId and self:getPlayerById(playerId) or nil - self.logic:trigger(event, player, damageStruct) - end - - return true + return true end ---@param recoverStruct RecoverStruct ---@return boolean function Room:recover(recoverStruct) - if recoverStruct.num < 1 then - return false - end + if recoverStruct.num < 1 then + return false + end - local who = self:getPlayerById(recoverStruct.who) - if self.logic:trigger(fk.PreHpRecover, who, recoverStruct) or recoverStruct.num < 1 then - return false - end + local who = self:getPlayerById(recoverStruct.who) + if self.logic:trigger(fk.PreHpRecover, who, recoverStruct) or recoverStruct.num < 1 then + return false + end - if not self:changeHp(who, recoverStruct.num, "recover", recoverStruct.skillName) then - return false - end + if not self:changeHp(who, recoverStruct.num, "recover", recoverStruct.skillName) then + return false + end - self.logic:trigger(fk.HpRecover, who, recoverStruct) - return true + self.logic:trigger(fk.HpRecover, who, recoverStruct) + return true end ---@param dyingStruct DyingStruct function Room:enterDying(dyingStruct) - local dyingPlayer = self:getPlayerById(dyingStruct.who) - dyingPlayer.dying = true - self.logic:trigger(fk.EnterDying, dyingPlayer, dyingStruct) + local dyingPlayer = self:getPlayerById(dyingStruct.who) + dyingPlayer.dying = true + self.logic:trigger(fk.EnterDying, dyingPlayer, dyingStruct) + + if dyingPlayer.hp < 1 then + local alivePlayers = self:getAlivePlayers() + for _, player in ipairs(alivePlayers) do + self.logic:trigger(fk.Dying, player, dyingStruct) + + if player.hp > 0 then + break + end + end if dyingPlayer.hp < 1 then - local alivePlayers = self:getAlivePlayers() - for _, player in ipairs(alivePlayers) do - self.logic:trigger(fk.Dying, player, dyingStruct) - - if player.hp > 0 then - break - end - end - - if dyingPlayer.hp < 1 then - ---@type DeathStruct - local deathData = { - who = dyingPlayer:getId(), - damage = dyingStruct.damage, - } - self:killPlayer(deathData) - end + ---@type DeathStruct + local deathData = { + who = dyingPlayer:getId(), + damage = dyingStruct.damage, + } + self:killPlayer(deathData) end - - self.logic:trigger(fk.AfterDying, dyingPlayer, dyingStruct) + end + + self.logic:trigger(fk.AfterDying, dyingPlayer, dyingStruct) end ---@param deathStruct DeathStruct function Room:killPlayer(deathStruct) - print(self:getPlayerById(deathStruct.who).general .. " is dead") - self:gameOver() + print(self:getPlayerById(deathStruct.who).general .. " is dead") + self:gameOver() +end + +---@param room Room +---@param cardUseEvent CardUseStruct +---@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 + if not cardUseEvent.tos then + return false + end + + room:sortPlayersByAction(cardUseEvent.tos) + local aimGroup = AimGroup:initAimGroup(TargetGroup:getRealTargets(cardUseEvent.tos)) + + local collaboratorsIndex = {} + local firstTarget = true + repeat + local toId = AimGroup:getUndoneOrDoneTargets(aimGroup)[1] + ---@type AimStruct + local aimStruct + local initialEvent = false + collaboratorsIndex[toId] = collaboratorsIndex[toId] or 0 + + if not aimEventCollaborators[toId] or collaboratorsIndex[toId] >= #aimEventCollaborators[toId] then + aimStruct = { + from = cardUseEvent.from, + cardId = cardUseEvent.cardId, + to = toId, + targetGroup = cardUseEvent.tos, + nullifiedTargets = cardUseEvent.nullifiedTargets or {}, + tos = aimGroup, + firstTarget = firstTarget, + additionalDamage = cardUseEvent.addtionalDamage + } + + collaboratorsIndex[toId] = 1 + initialEvent = true + else + aimStruct = aimEventCollaborators[toId][collaboratorsIndex[toId]] + aimStruct.from = cardUseEvent.from + aimStruct.cardId = cardUseEvent.cardId + aimStruct.tos = aimGroup + aimStruct.targetGroup = cardUseEvent.tos + aimStruct.nullifiedTargets = cardUseEvent.nullifiedTargets or {} + aimStruct.firstTarget = firstTarget + end + + 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 + return false + end + AimGroup:removeDeadTargets(room, aimStruct) + + local aimEventTargetGroup = aimStruct.targetGroup + if aimEventTargetGroup then + room:sortPlayersByAction(aimEventTargetGroup) + end + + cardUseEvent.from = aimStruct.from + cardUseEvent.tos = aimEventTargetGroup + cardUseEvent.nullifiedTargets = aimStruct.nullifiedTargets + + if #AimGroup:getAllTargets(aimStruct.tos) == 0 then + return false + end + + local cancelledTargets = AimGroup:getCancelledTargets(aimStruct.tos) + if #cancelledTargets > 0 then + for _, target in ipairs(cancelledTargets) do + aimEventCollaborators[target] = {} + collaboratorsIndex[target] = 0 + end + end + aimStruct.tos[AimGroup.Cancelled] = {} + + aimEventCollaborators[toId] = aimEventCollaborators[toId] or {} + if not room:getPlayerById(toId):isAlive() then + if initialEvent then + table.insert(aimEventCollaborators[toId], aimStruct) + else + aimEventCollaborators[toId][collaboratorsIndex[toId]] = aimStruct + end + end + + AimGroup:setTargetDone(aimStruct.tos, toId) + aimGroup = aimStruct.tos + until #AimGroup:getUndoneOrDoneTargets(aimGroup) == 0 + end + + return true +end + +---@param cardUseEvent CardUseStruct +---@return boolean +function Room:useCard(cardUseEvent) + self:moveCards({ + ids = { cardUseEvent.cardId }, + from = cardUseEvent.customFrom or cardUseEvent.from, + toArea = Card.Processing, + moveReason = fk.ReasonUse, + }) + + if Fk:getCardById(cardUseEvent.cardId).skill then + Fk:getCardById(cardUseEvent.cardId).skill:onUse(self, cardUseEvent) + end + if self.logic:trigger(fk.PreCardUse, self:getPlayerById(cardUseEvent.from), cardUseEvent) then + return false + end + + if not cardUseEvent.extraUse then + self:getPlayerById(cardUseEvent.from):addCardUseHistory(Fk:getCardById(cardUseEvent.cardId).trueName, 1) + end + + if cardUseEvent.responseToEvent then + cardUseEvent.responseToEvent.cardIdsResponded = cardUseEvent.responseToEvent.cardIdsResponded or {} + table.insert(cardUseEvent.responseToEvent.cardIdsResponded, cardUseEvent.cardId) + end + + for _, event in ipairs({ fk.AfterCardUseDeclared, fk.AfterCardTargetDeclared, fk.BeforeCardUseEffect, fk.CardUsing }) do + -- TODO: need to complete the cards for response + + self.logic:trigger(event, self:getPlayerById(cardUseEvent.from), cardUseEvent) + if event == fk.CardUsing then + ---@type table + local aimEventCollaborators = {} + if cardUseEvent.tos and not onAim(self, cardUseEvent, aimEventCollaborators) then + break + end + + if Fk:getCardById(cardUseEvent.cardId).type == Card.TypeEquip then + if self:getCardArea(cardUseEvent.cardId) ~= Card.Processing then + break + end + + if self:getPlayerById(TargetGroup:getRealTargets(cardUseEvent.tos)[1]).dead then + self.moveCards({ + ids = { cardUseEvent.cardId }, + toArea = Card.DiscardPile, + moveReason = fk.ReasonPutIntoDiscardPile, + }) + else + local target = TargetGroup:getRealTargets(cardUseEvent.tos)[1] + local existingEquipId = self:getPlayerById(target):getEquipment(Fk:getCardById(cardUseEvent.cardId).sub_type) + if existingEquipId then + self:moveCards( + { + ids = { existingEquipId }, + from = target, + toArea = Card.DiscardPile, + moveReason = fk.ReasonPutIntoDiscardPile, + }, + { + ids = { cardUseEvent.cardId }, + to = target, + toArea = Card.PlayerEquip, + moveReason = fk.ReasonUse, + } + ) + else + self:moveCards({ + ids = { cardUseEvent.cardId }, + to = target, + toArea = Card.PlayerEquip, + moveReason = fk.ReasonUse, + }) + end + end + + break + elseif Fk:getCardById(cardUseEvent.cardId).sub_type == Card.SubtypeDelayedTrick then + if self:getCardArea(cardUseEvent.cardId) ~= Card.Processing then + break + end + + local target = TargetGroup:getRealTargets(cardUseEvent.tos)[1] + if not self:getPlayerById(target).dead then + local findSameCard = false + for _, cardId in ipairs(self:getPlayerById(target):getCardIds(Player.Equip)) do + if Fk:getCardById(cardId).trueName == Fk:getCardById(cardUseEvent.cardId) then + findSameCard = true + end + end + + if not findSameCard then + self:moveCards({ + ids = { cardUseEvent.cardId }, + to = target, + toArea = Card.PlayerJudge, + moveReason = fk.ReasonUse, + }) + + break + end + end + + self:moveCards({ + ids = { cardUseEvent.cardId }, + toArea = Card.DiscardPile, + moveReason = fk.ReasonPutIntoDiscardPile, + }) + + break + end + + if Fk:getCardById(cardUseEvent.cardId).skill then + Fk:getCardById(cardUseEvent.cardId).skill:onEffect(self, cardUseEvent) + end + end + end + + self.logic:trigger(fk.CardUseFinished, self:getPlayerById(cardUseEvent.from), cardUseEvent) + if self:getCardArea(cardUseEvent.cardId) == Card.Processing then + self:moveCards({ + ids = { cardUseEvent.cardId }, + toArea = Card.DiscardPile, + moveReason = fk.ReasonPutIntoDiscardPile, + }) + end end fk.room_callback["QuitRoom"] = function(jsonData) - -- jsonData: [ int uid ] - local data = json.decode(jsonData) - local player = fk.ServerInstance:findPlayer(tonumber(data[1])) - local room = player:getRoom() - if not room:isLobby() then - room:removePlayer(player) - end + -- jsonData: [ int uid ] + local data = json.decode(jsonData) + local player = fk.ServerInstance:findPlayer(tonumber(data[1])) + local room = player:getRoom() + if not room:isLobby() then + room:removePlayer(player) + end end fk.room_callback["AddRobot"] = function(jsonData) - -- jsonData: [ int uid ] - local data = json.decode(jsonData) - local player = fk.ServerInstance:findPlayer(tonumber(data[1])) - local room = player:getRoom() - - if not room:isLobby() then - room:addRobot(player) - end + -- jsonData: [ int uid ] + local data = json.decode(jsonData) + local player = fk.ServerInstance:findPlayer(tonumber(data[1])) + local room = player:getRoom() + + if not room:isLobby() then + room:addRobot(player) + end end fk.room_callback["PlayerRunned"] = function(jsonData) - -- jsonData: [ int runner_id, int robot_id ] - -- note: this function is not called by Router. - -- note: when this function is called, the room must be started - local data = json.decode(jsonData) - local runner = data[1] - local robot = data[2] - for _, p in ipairs(RoomInstance.players) do - if p:getId() == runner then - p.serverplayer = RoomInstance.room:findPlayer(robot) - p.id = p.serverplayer:getId() - end + -- jsonData: [ int runner_id, int robot_id ] + -- note: this function is not called by Router. + -- note: when this function is called, the room must be started + local data = json.decode(jsonData) + local runner = data[1] + local robot = data[2] + for _, p in ipairs(RoomInstance.players) do + if p:getId() == runner then + p.serverplayer = RoomInstance.room:findPlayer(robot) + p.id = p.serverplayer:getId() end + end end fk.room_callback["PlayerStateChanged"] = function(jsonData) - -- jsonData: [ int uid, string stateString ] - -- note: this function is not called by Router. - -- note: when this function is called, the room must be started - local data = json.decode(jsonData) - local id = data[1] - local stateString = data[2] - RoomInstance:getPlayerById(id).state = stateString + -- jsonData: [ int uid, string stateString ] + -- note: this function is not called by Router. + -- note: when this function is called, the room must be started + local data = json.decode(jsonData) + local id = data[1] + local stateString = data[2] + RoomInstance:getPlayerById(id).state = stateString end fk.room_callback["RoomDeleted"] = function(jsonData) - debug.sethook(function () - error("Room is deleted when running") - end, "l") + debug.sethook(function () + error("Room is deleted when running") + end, "l") end fk.room_callback["DoLuaScript"] = function(jsonData) - -- jsonData: [ int uid, string luaScript ] - -- warning: only use this in debugging mode. - if not DebugMode then return end - local data = json.decode(jsonData) - assert(load(data[2]))() + -- jsonData: [ int uid, string luaScript ] + -- warning: only use this in debugging mode. + if not DebugMode then return end + local data = json.decode(jsonData) + assert(load(data[2]))() end function CreateRoom(_room) - RoomInstance = Room:new(_room) + RoomInstance = Room:new(_room) end diff --git a/lua/server/serverplayer.lua b/lua/server/serverplayer.lua index cea10be3..debf21bd 100644 --- a/lua/server/serverplayer.lua +++ b/lua/server/serverplayer.lua @@ -12,30 +12,30 @@ local ServerPlayer = Player:subclass("ServerPlayer") function ServerPlayer:initialize(_self) - Player.initialize(self) - self.serverplayer = _self - self.id = _self:getId() - self.room = nil + Player.initialize(self) + self.serverplayer = _self + self.id = _self:getId() + self.room = nil - self.next = nil + self.next = nil - -- Below are for doBroadcastRequest - self.request_data = "" - self.client_reply = "" - self.default_reply = "" - self.reply_ready = false - self.phases = {} + -- Below are for doBroadcastRequest + self.request_data = "" + self.client_reply = "" + self.default_reply = "" + self.reply_ready = false + self.phases = {} end ---@return integer function ServerPlayer:getId() - return self.id + return self.id end ---@param command string ---@param jsonData string function ServerPlayer:doNotify(command, jsonData) - self.serverplayer:doNotify(command, jsonData) + self.serverplayer:doNotify(command, jsonData) end --- Send a request to client, and allow client to reply within *timeout* seconds. @@ -45,10 +45,10 @@ end ---@param jsonData string ---@param timeout integer 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) + timeout = timeout or self.room.timeout + self.client_reply = "" + self.reply_ready = false + self.serverplayer:doRequest(command, jsonData, timeout) end --- Wait for at most *timeout* seconds for reply from client. @@ -57,153 +57,153 @@ end ---@param timeout integer @ seconds to wait ---@return string @ JSON data 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 + 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 ---@param skill Skill function ServerPlayer:hasSkill(skill) - return table.contains(self.player_skills, skill) + return table.contains(self.player_skills, skill) end function ServerPlayer:isAlive() - return self.dead == false + return self.dead == false end function ServerPlayer:getNextAlive() - if #self.room.alive_players == 0 then - return self - end + if #self.room.alive_players == 0 then + return self + end - local ret = self.next - while ret.dead do - ret = ret.next - end - return ret + local ret = self.next + while ret.dead do + ret = ret.next + end + return ret end function ServerPlayer:turnOver() - self.faceup = not self.faceup - self.room:broadcastProperty(self, "faceup") + self.faceup = not self.faceup + self.room:broadcastProperty(self, "faceup") - -- TODO: log - self.room.logic:trigger(fk.TurnedOver, self) + -- TODO: log + self.room.logic:trigger(fk.TurnedOver, self) end ---@param from_phase Phase ---@param to_phase Phase function ServerPlayer:changePhase(from_phase, to_phase) - local room = self.room - local logic = room.logic - self.phase = Player.PhaseNone + local room = self.room + local logic = room.logic + self.phase = Player.PhaseNone - local phase_change = { - from = from_phase, - to = to_phase - } + local phase_change = { + from = from_phase, + to = to_phase + } - local skip = logic:trigger(fk.EventPhaseChanging, self, phase_change) - if skip and to_phase ~= Player.NotActive then - self.phase = from_phase - return true - end + local skip = logic:trigger(fk.EventPhaseChanging, self, phase_change) + if skip and to_phase ~= Player.NotActive then + self.phase = from_phase + return true + end - self.phase = to_phase - room:notifyProperty(self, self, "phase") + self.phase = to_phase + room:notifyProperty(self, self, "phase") - if #self.phases > 0 then - table.remove(self.phases, 1) - end - - if not logic:trigger(fk.EventPhaseStart, self) then - if self.phase ~= Player.NotActive then - logic:trigger(fk.EventPhaseProceeding, self) - end - end + if #self.phases > 0 then + table.remove(self.phases, 1) + end + if not logic:trigger(fk.EventPhaseStart, self) then if self.phase ~= Player.NotActive then - logic:trigger(fk.EventPhaseEnd, self) + logic:trigger(fk.EventPhaseProceeding, self) end + end - return false + if self.phase ~= Player.NotActive then + logic:trigger(fk.EventPhaseEnd, self) + end + + return false end ---@param phase_table Phase[] function ServerPlayer:play(phase_table) - phase_table = phase_table or {} - if #phase_table > 0 then - if not table.contains(phase_table, Player.NotActive) then - table.insert(phase_table, Player.NotActive) - end - else - phase_table = { - Player.RoundStart, Player.Start, - Player.Judge, Player.Draw, Player.Play, Player.Discard, - Player.Finish, Player.NotActive, - } + phase_table = phase_table or {} + if #phase_table > 0 then + if not table.contains(phase_table, Player.NotActive) then + table.insert(phase_table, Player.NotActive) + end + else + phase_table = { + Player.RoundStart, Player.Start, + Player.Judge, Player.Draw, Player.Play, Player.Discard, + Player.Finish, Player.NotActive, + } + end + + self.phases = phase_table + self.phase_state = {} + + local phases = self.phases + local phase_state = self.phase_state + local room = self.room + + for i = 1, #phases do + phase_state[i] = { + phase = phases[i], + skipped = false + } + end + + for i = 1, #phases do + if self.dead then + self:changePhase(self.phase, Player.NotActive) + break end - self.phases = phase_table - self.phase_state = {} + self.phase_index = i + local phase_change = { + from = self.phase, + to = phases[i] + } - local phases = self.phases - local phase_state = self.phase_state - local room = self.room + local logic = self.room.logic + self.phase = Player.PhaseNone - for i = 1, #phases do - phase_state[i] = { - phase = phases[i], - skipped = false - } + local skip = logic:trigger(fk.EventPhaseChanging, self, phase_change) + phases[i] = phase_change.to + phase_state[i].phase = phases[i] + + self.phase = phases[i] + room:notifyProperty(self, self, "phase") + + local cancel_skip = true + if phases[i] ~= Player.NotActive and (phase_state[i].skipped or skip) then + cancel_skip = logic:trigger(fk.EventPhaseSkipping, self) end - for i = 1, #phases do - if self.dead then - self:changePhase(self.phase, Player.NotActive) - break + if (not skip) or (cancel_skip) then + if not logic:trigger(fk.EventPhaseStart, self) then + if self.phase ~= Player.NotActive then + logic:trigger(fk.EventPhaseProceeding, self) end + end - self.phase_index = i - local phase_change = { - from = self.phase, - to = phases[i] - } - - local logic = self.room.logic - self.phase = Player.PhaseNone - - local skip = logic:trigger(fk.EventPhaseChanging, self, phase_change) - phases[i] = phase_change.to - phase_state[i].phase = phases[i] - - self.phase = phases[i] - room:notifyProperty(self, self, "phase") - - local cancel_skip = true - if phases[i] ~= Player.NotActive and (phase_state[i].skipped or skip) then - cancel_skip = logic:trigger(fk.EventPhaseSkipping, self) - end - - if (not skip) or (cancel_skip) then - if not logic:trigger(fk.EventPhaseStart, self) then - if self.phase ~= Player.NotActive then - logic:trigger(fk.EventPhaseProceeding, self) - end - end - - if self.phase ~= Player.NotActive then - logic:trigger(fk.EventPhaseEnd, self) - else break end - end + if self.phase ~= Player.NotActive then + logic:trigger(fk.EventPhaseEnd, self) + else break end end + end end return ServerPlayer diff --git a/lua/server/system_enum.lua b/lua/server/system_enum.lua index ddb13229..4fab41b7 100644 --- a/lua/server/system_enum.lua +++ b/lua/server/system_enum.lua @@ -10,6 +10,9 @@ ---@alias DyingStruct { who: integer, damage: DamageStruct } ---@alias DeathStruct { who: integer, damage: DamageStruct } +---@alias CardUseStruct { from: integer, tos: TargetGroup, cardId: integer, toCardId: integer|null, responseToEvent: CardUseStruct|null, nullifiedTargets: interger[]|null, extraUse: boolean|null, disresponsiveList: integer[]|null, unoffsetableList: integer[]|null, addtionalDamage: integer|null, customFrom: integer|null, cardIdsResponded: integer[]|null } +---@alias AimStruct { from: integer, cardId: integer, tos: AimGroup, to: integer, targetGroup: TargetGroup|null, nullifiedTargets: integer[]|null, firstTarget: boolean, additionalDamage: integer|null, disresponsive: boolean|null, unoffsetableList: boolean|null } +---@alias CardEffectEvent { from: integer, tos: TargetGroup, cardId: integer, toCardId: integer|null, responseToEvent: CardUseStruct|null, nullifiedTargets: interger[]|null, extraUse: boolean|null, disresponsiveList: integer[]|null, unoffsetableList: integer[]|null, addtionalDamage: integer|null, customFrom: integer|null, cardIdsResponded: integer[]|null } ---@alias MoveReason integer @@ -21,6 +24,8 @@ fk.ReasonPut = 5 fk.ReasonPutIntoDiscardPile = 6 fk.ReasonPrey = 7 fk.ReasonExchange = 8 +fk.ReasonUse = 9 +fk.ReasonResonpse = 10 ---@alias DamageType integer diff --git a/packages/standard/game_rule.lua b/packages/standard/game_rule.lua index a82eb846..c08d29fe 100644 --- a/packages/standard/game_rule.lua +++ b/packages/standard/game_rule.lua @@ -1,118 +1,133 @@ GameRule = fk.CreateTriggerSkill{ - name = "game_rule", - events = { - fk.GameStart, fk.DrawInitialCards, fk.TurnStart, - fk.EventPhaseProceeding, fk.EventPhaseEnd, fk.EventPhaseChanging, - }, - priority = 0, + name = "game_rule", + events = { + fk.GameStart, fk.DrawInitialCards, fk.TurnStart, + fk.EventPhaseProceeding, fk.EventPhaseEnd, fk.EventPhaseChanging, + }, + priority = 0, - can_trigger = function(self, event, target, player, data) - return (target == player) or (target == nil) - end, + can_trigger = function(self, event, target, player, data) + return (target == player) or (target == nil) + end, - on_trigger = function(self, event, target, player, data) - if RoomInstance.tag["SkipGameRule"] then - RoomInstance.tag["SkipGameRule"] = false - return false + on_trigger = function(self, event, target, player, data) + if RoomInstance.tag["SkipGameRule"] then + RoomInstance.tag["SkipGameRule"] = false + return false + end + + if target == nil then + if event == fk.GameStart then + print("Game started") + RoomInstance.tag["FirstRound"] = true + end + return false + end + + local room = player.room + switch(event, { + [fk.DrawInitialCards] = function() + if data.num > 0 then + -- TODO: need a new function to call the UI + local cardIds = room:getNCards(data.num) + player:addCards(Player.Hand, cardIds) + local move_to_notify = {} ---@type CardsMoveStruct + move_to_notify.toArea = Card.PlayerHand + move_to_notify.to = player:getId() + move_to_notify.moveInfo = {} + for _, id in ipairs(cardIds) do + table.insert(move_to_notify.moveInfo, + { cardId = id, fromArea = Card.DrawPile }) + end + room:notifyMoveCards(room.players, {move_to_notify}) + + for _, id in ipairs(cardIds) do + room:setCardArea(id, Card.PlayerHand) end - if target == nil then - if event == fk.GameStart then - print("Game started") - RoomInstance.tag["FirstRound"] = true - end - return false - end - - local room = player.room - switch(event, { - [fk.DrawInitialCards] = function() - if data.num > 0 then - -- TODO: need a new function to call the UI - local cardIds = room:getNCards(data.num) - player:addCards(Player.Hand, cardIds) - local move_to_notify = {} ---@type CardsMoveStruct - move_to_notify.toArea = Card.PlayerHand - move_to_notify.to = player:getId() - move_to_notify.moveInfo = {} - for _, id in ipairs(cardIds) do - table.insert(move_to_notify.moveInfo, - { cardId = id, fromArea = Card.DrawPile }) - end - room:notifyMoveCards(room.players, {move_to_notify}) - - for _, id in ipairs(cardIds) do - room:setCardArea(id, Card.PlayerHand) - end - - room.logic:trigger(fk.AfterDrawInitialCards, player, data) - end - end, - [fk.TurnStart] = function() - player = room.current - if room.tag["FirstRound"] == true then - room.tag["FirstRound"] = false - player:setFlag("Global_FirstRound") - end - - -- TODO: send log - - player:addMark("Global_TurnCount") - if not player.faceup then - player:setFlag("-Global_FirstRound") - player:turnOver() - elseif not player.dead then - player:play() - end - end, - [fk.EventPhaseProceeding] = function() - switch(player.phase, { - [Player.PhaseNone] = function() - error("You should never proceed PhaseNone") - end, - [Player.RoundStart] = function() - - end, - [Player.Start] = function() - - end, - [Player.Judge] = function() - - end, - [Player.Draw] = function() - room:drawCards(player, 2, self.name) - end, - [Player.Play] = function() - room:askForSkillInvoke(player, "rule") - end, - [Player.Discard] = function() - local discardNum = #player:getCardIds(Player.Hand) - player:getMaxCards() - if discardNum > 0 then - room:askForDiscard(player, discardNum, discardNum, false, self.name) - end - end, - [Player.Finish] = function() - - end, - [Player.NotActive] = function() - - end, - }) - end, - [fk.EventPhaseEnd] = function() - if player.phase == Player.Play then - -- TODO: clear history - end - end, - [fk.EventPhaseChanging] = function() - -- TODO: copy but dont copy all - end, - default = function() - print("game_rule: Event=" .. event) - room:askForSkillInvoke(player, "rule") - end, - }) - return false + room.logic:trigger(fk.AfterDrawInitialCards, player, data) + end end, + [fk.TurnStart] = function() + player = room.current + if room.tag["FirstRound"] == true then + room.tag["FirstRound"] = false + player:setFlag("Global_FirstRound") + end + + -- TODO: send log + + player:addMark("Global_TurnCount") + if not player.faceup then + player:setFlag("-Global_FirstRound") + player:turnOver() + elseif not player.dead then + player:play() + end + end, + [fk.EventPhaseProceeding] = function() + switch(player.phase, { + [Player.PhaseNone] = function() + error("You should never proceed PhaseNone") + end, + [Player.RoundStart] = function() + + end, + [Player.Start] = function() + + end, + [Player.Judge] = function() + + end, + [Player.Draw] = function() + room:drawCards(player, 2, self.name) + end, + [Player.Play] = function() + while not player.dead do + local result = room:doRequest(player, "PlayCard", player:getId()) + if result == "" then break end + + local data = json.decode(result) + local card = data.card + local targets = data.targets + local use = {} ---@type CardUseStruct + use.from = player:getId() + use.tos = {} + for _, target in ipairs(targets) do + table.insert(use.tos, { target }) + end + use.cardId = card + room:useCard(use) + end + end, + [Player.Discard] = function() + local discardNum = #player:getCardIds(Player.Hand) - player:getMaxCards() + if discardNum > 0 then + room:askForDiscard(player, discardNum, discardNum, false, self.name) + end + end, + [Player.Finish] = function() + + end, + [Player.NotActive] = function() + + end, + }) + end, + [fk.EventPhaseEnd] = function() + if player.phase == Player.Play then + -- TODO: clear history + end + end, + [fk.EventPhaseChanging] = function() + -- TODO: copy but dont copy all + end, + default = function() + print("game_rule: Event=" .. event) + room:askForSkillInvoke(player, "rule") + end, + }) + return false + end, } diff --git a/packages/standard/init.lua b/packages/standard/init.lua index 3198ce68..41b97242 100644 --- a/packages/standard/init.lua +++ b/packages/standard/init.lua @@ -3,161 +3,161 @@ extension.metadata = require "packages.standard.metadata" dofile "packages/standard/game_rule.lua" Fk:loadTranslationTable{ - ["standard"] = "标准包", - ["wei"] = "魏", - ["shu"] = "蜀", - ["wu"] = "吴", - ["qun"] = "群", + ["standard"] = "标准包", + ["wei"] = "魏", + ["shu"] = "蜀", + ["wu"] = "吴", + ["qun"] = "群", } local caocao = General:new(extension, "caocao", "wei", 4) extension:addGeneral(caocao) Fk:loadTranslationTable{ - ["caocao"] = "曹操", + ["caocao"] = "曹操", } local simayi = General:new(extension, "simayi", "wei", 3) extension:addGeneral(simayi) Fk:loadTranslationTable{ - ["simayi"] = "司马懿", + ["simayi"] = "司马懿", } local xiahoudun = General:new(extension, "xiahoudun", "wei", 4) extension:addGeneral(xiahoudun) Fk:loadTranslationTable{ - ["xiahoudun"] = "夏侯惇", + ["xiahoudun"] = "夏侯惇", } local zhangliao = General:new(extension, "zhangliao", "wei", 4) extension:addGeneral(zhangliao) Fk:loadTranslationTable{ - ["zhangliao"] = "张辽", + ["zhangliao"] = "张辽", } local xuchu = General:new(extension, "xuchu", "wei", 4) extension:addGeneral(xuchu) Fk:loadTranslationTable{ - ["xuchu"] = "许褚", + ["xuchu"] = "许褚", } local guojia = General:new(extension, "guojia", "wei", 4) extension:addGeneral(guojia) Fk:loadTranslationTable{ - ["guojia"] = "郭嘉", + ["guojia"] = "郭嘉", } local zhenji = General:new(extension, "zhenji", "wei", 3) extension:addGeneral(zhenji) Fk:loadTranslationTable{ - ["zhenji"] = "甄姬", + ["zhenji"] = "甄姬", } local liubei = General:new(extension, "liubei", "shu", 4) extension:addGeneral(liubei) Fk:loadTranslationTable{ - ["liubei"] = "刘备", + ["liubei"] = "刘备", } local guanyu = General:new(extension, "guanyu", "shu", 4) extension:addGeneral(guanyu) Fk:loadTranslationTable{ - ["guanyu"] = "关羽", + ["guanyu"] = "关羽", } local zhangfei = General:new(extension, "zhangfei", "shu", 4) extension:addGeneral(zhangfei) Fk:loadTranslationTable{ - ["zhangfei"] = "张飞", + ["zhangfei"] = "张飞", } local zhugeliang = General:new(extension, "zhugeliang", "shu", 3) extension:addGeneral(zhugeliang) Fk:loadTranslationTable{ - ["zhugeliang"] = "诸葛亮", + ["zhugeliang"] = "诸葛亮", } local zhaoyun = General:new(extension, "zhaoyun", "shu", 4) extension:addGeneral(zhaoyun) Fk:loadTranslationTable{ - ["zhaoyun"] = "赵云", + ["zhaoyun"] = "赵云", } local machao = General:new(extension, "machao", "shu", 4) extension:addGeneral(machao) Fk:loadTranslationTable{ - ["machao"] = "马超", + ["machao"] = "马超", } local huangyueying = General:new(extension, "huangyueying", "shu", 3) extension:addGeneral(huangyueying) Fk:loadTranslationTable{ - ["huangyueying"] = "黄月英", + ["huangyueying"] = "黄月英", } local sunquan = General:new(extension, "sunquan", "wu", 4) extension:addGeneral(sunquan) Fk:loadTranslationTable{ - ["sunquan"] = "孙权", + ["sunquan"] = "孙权", } local ganning = General:new(extension, "ganning", "wu", 4) extension:addGeneral(ganning) Fk:loadTranslationTable{ - ["ganning"] = "甘宁", + ["ganning"] = "甘宁", } local lvmeng = General:new(extension, "lvmeng", "wu", 4) extension:addGeneral(lvmeng) Fk:loadTranslationTable{ - ["lvmeng"] = "吕蒙", + ["lvmeng"] = "吕蒙", } local huanggai = General:new(extension, "huanggai", "wu", 4) extension:addGeneral(huanggai) Fk:loadTranslationTable{ - ["huanggai"] = "黄盖", + ["huanggai"] = "黄盖", } local zhouyu = General:new(extension, "zhouyu", "wu", 3) extension:addGeneral(zhouyu) Fk:loadTranslationTable{ - ["zhouyu"] = "周瑜", + ["zhouyu"] = "周瑜", } local daqiao = General:new(extension, "daqiao", "wu", 3) extension:addGeneral(daqiao) Fk:loadTranslationTable{ - ["daqiao"] = "大乔", + ["daqiao"] = "大乔", } local luxun = General:new(extension, "luxun", "wu", 3) extension:addGeneral(luxun) Fk:loadTranslationTable{ - ["luxun"] = "陆逊", + ["luxun"] = "陆逊", } local sunshangxiang = General:new(extension, "sunshangxiang", "wu", 3) extension:addGeneral(sunshangxiang) Fk:loadTranslationTable{ - ["sunshangxiang"] = "孙尚香", + ["sunshangxiang"] = "孙尚香", } local huatuo = General:new(extension, "huatuo", "qun", 3) extension:addGeneral(huatuo) Fk:loadTranslationTable{ - ["huatuo"] = "华佗", + ["huatuo"] = "华佗", } local lvbu = General:new(extension, "lvbu", "qun", 4) extension:addGeneral(lvbu) Fk:loadTranslationTable{ - ["lvbu"] = "吕布", + ["lvbu"] = "吕布", } local diaochan = General:new(extension, "diaochan", "qun", 3) extension:addGeneral(diaochan) Fk:loadTranslationTable{ - ["diaochan"] = "貂蝉", + ["diaochan"] = "貂蝉", } return extension diff --git a/packages/standard/metadata.lua b/packages/standard/metadata.lua index 51cf16a8..e4c0793f 100644 --- a/packages/standard/metadata.lua +++ b/packages/standard/metadata.lua @@ -1,14 +1,14 @@ return { - name = "standard", - author = "official", - description = "", - collaborators = { - program = {}, - designer = {}, - cv = {}, - illustrator = {}, - }, + name = "standard", + author = "official", + description = "", + collaborators = { + program = {}, + designer = {}, + cv = {}, + illustrator = {}, + }, - dependencies = {}, - extra_files = {}, + dependencies = {}, + extra_files = {}, } diff --git a/packages/standard_cards/init.lua b/packages/standard_cards/init.lua index 145e4e2f..7243f6e3 100644 --- a/packages/standard_cards/init.lua +++ b/packages/standard_cards/init.lua @@ -2,510 +2,523 @@ local extension = Package:new("standard_cards", Package.CardPack) extension.metadata = require "packages.standard_cards.metadata" Fk:loadTranslationTable{ - ["standard_cards"] = "标+EX" + ["standard_cards"] = "标+EX" } local slash = fk.CreateBasicCard{ - name = "slash", - number = 7, - suit = Card.Spade, + name = "slash", + number = 7, + suit = Card.Spade, } Fk:loadTranslationTable{ - ["slash"] = "杀", + ["slash"] = "杀", } 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, + 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.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.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 jink = fk.CreateBasicCard{ - name = "jink", - suit = Card.Heart, - number = 2, + name = "jink", + suit = Card.Heart, + number = 2, } Fk:loadTranslationTable{ - ["jink"] = "闪", + ["jink"] = "闪", } extension:addCards({ - jink, - jink:clone(Card.Heart, 2), - jink:clone(Card.Heart, 13), + 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 peach = fk.CreateBasicCard{ - name = "peach", - suit = Card.Heart, - number = 3, + name = "peach", + suit = Card.Heart, + number = 3, } Fk:loadTranslationTable{ - ["peach"] = "桃", + ["peach"] = "桃", } 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), + 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 dismantlement = fk.CreateTrickCard{ - name = "dismantlement", - suit = Card.Spade, - number = 3, + name = "dismantlement", + suit = Card.Spade, + number = 3, } Fk:loadTranslationTable{ - ["dismantlement"] = "过河拆桥", + ["dismantlement"] = "过河拆桥", } extension:addCards({ - dismantlement, - dismantlement:clone(Card.Spade, 4), - dismantlement:clone(Card.Spade, 12), + 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 snatch = fk.CreateTrickCard{ - name = "snatch", - suit = Card.Spade, - number = 3, + name = "snatch", + suit = Card.Spade, + number = 3, } Fk:loadTranslationTable{ - ["snatch"] = "顺手牵羊", + ["snatch"] = "顺手牵羊", } extension:addCards({ - snatch, - snatch:clone(Card.Spade, 4), - snatch:clone(Card.Spade, 11), + 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 duel = fk.CreateTrickCard{ - name = "duel", - suit = Card.Spade, - number = 1, + name = "duel", + suit = Card.Spade, + number = 1, } Fk:loadTranslationTable{ - ["duel"] = "决斗", + ["duel"] = "决斗", } extension:addCards({ - duel, + duel, - duel:clone(Card.Club, 1), + duel:clone(Card.Club, 1), - duel:clone(Card.Diamond, 1), + duel:clone(Card.Diamond, 1), }) local collateral = fk.CreateTrickCard{ - name = "collateral", - suit = Card.Club, - number = 12, + name = "collateral", + suit = Card.Club, + number = 12, } Fk:loadTranslationTable{ - ["collateral"] = "借刀杀人", + ["collateral"] = "借刀杀人", } extension:addCards({ - collateral, - collateral:clone(Card.Club, 13), + collateral, + collateral:clone(Card.Club, 13), }) +local exNihiloSkill = fk.CreateActiveSkill{ + name = "ex_nihilo_skill", + on_use = function(self, room, cardUseEvent) + if not cardUseEvent.tos or #TargetGroup:getRealTargets(cardUseEvent.tos) == 0 then + cardUseEvent.tos = { { cardUseEvent.from } } + end + end, + on_effect = function(self, room, cardEffectEvent) + room:drawCards(room:getPlayerById(TargetGroup:getRealTargets(cardEffectEvent.tos)[1]), 2, "ex_nihilo") + end +} + local exNihilo = fk.CreateTrickCard{ - name = "ex_nihilo", - suit = Card.Heart, - number = 7, + name = "ex_nihilo", + suit = Card.Heart, + number = 7, + skill = exNihiloSkill, } Fk:loadTranslationTable{ - ["ex_nihilo"] = "无中生有", + ["ex_nihilo"] = "无中生有", } extension:addCards({ - exNihilo, - exNihilo:clone(Card.Heart, 8), - exNihilo:clone(Card.Heart, 9), - exNihilo:clone(Card.Heart, 11), + exNihilo, + exNihilo:clone(Card.Heart, 8), + exNihilo:clone(Card.Heart, 9), + exNihilo:clone(Card.Heart, 11), }) local nullification = fk.CreateTrickCard{ - name = "nullification", - suit = Card.Spade, - number = 11, + name = "nullification", + suit = Card.Spade, + number = 11, } Fk:loadTranslationTable{ - ["nullification"] = "无懈可击", + ["nullification"] = "无懈可击", } extension:addCards({ - nullification, + nullification, - nullification:clone(Card.Club, 12), - nullification:clone(Card.Club, 13), + nullification:clone(Card.Club, 12), + nullification:clone(Card.Club, 13), - nullification:clone(Card.Diamond, 12), + nullification:clone(Card.Diamond, 12), }) local savageAssault = fk.CreateTrickCard{ - name = "savage_assault", - suit = Card.Spade, - number = 7, + name = "savage_assault", + suit = Card.Spade, + number = 7, } Fk:loadTranslationTable{ - ["savage_assault"] = "南蛮入侵", + ["savage_assault"] = "南蛮入侵", } extension:addCards({ - savageAssault, - savageAssault:clone(Card.Spade, 13), - savageAssault:clone(Card.Club, 7), + savageAssault, + savageAssault:clone(Card.Spade, 13), + savageAssault:clone(Card.Club, 7), }) local archeryAttack = fk.CreateTrickCard{ - name = "archery_attack", - suit = Card.Heart, - number = 1, + name = "archery_attack", + suit = Card.Heart, + number = 1, } Fk:loadTranslationTable{ - ["archery_attack"] = "万箭齐发", + ["archery_attack"] = "万箭齐发", } extension:addCards({ - archeryAttack, + archeryAttack, }) local godSalvation = fk.CreateTrickCard{ - name = "god_salvation", - suit = Card.Heart, - number = 1, + name = "god_salvation", + suit = Card.Heart, + number = 1, } Fk:loadTranslationTable{ - ["god_salvation"] = "桃园结义", + ["god_salvation"] = "桃园结义", } extension:addCards({ - godSalvation, + godSalvation, }) local amazingGrace = fk.CreateTrickCard{ - name = "amazing_grace", - suit = Card.Heart, - number = 3, + name = "amazing_grace", + suit = Card.Heart, + number = 3, } Fk:loadTranslationTable{ - ["amazing_grace"] = "五谷丰登", + ["amazing_grace"] = "五谷丰登", } extension:addCards({ - amazingGrace, - amazingGrace:clone(Card.Heart, 4), + amazingGrace, + amazingGrace:clone(Card.Heart, 4), }) local lightning = fk.CreateDelayedTrickCard{ - name = "lightning", - suit = Card.Spade, - number = 1, + name = "lightning", + suit = Card.Spade, + number = 1, } Fk:loadTranslationTable{ - ["lightning"] = "闪电", + ["lightning"] = "闪电", } extension:addCards({ - lightning, - lightning:clone(Card.Heart, 12), + lightning, + lightning:clone(Card.Heart, 12), }) local indulgence = fk.CreateDelayedTrickCard{ - name = "indulgence", - suit = Card.Spade, - number = 6, + name = "indulgence", + suit = Card.Spade, + number = 6, } Fk:loadTranslationTable{ - ["indulgence"] = "乐不思蜀", + ["indulgence"] = "乐不思蜀", } extension:addCards({ - indulgence, - indulgence:clone(Card.Club, 6), - indulgence:clone(Card.Heart, 6), + indulgence, + indulgence:clone(Card.Club, 6), + indulgence:clone(Card.Heart, 6), }) local crossbow = fk.CreateWeapon{ - name = "crossbow", - suit = Card.Club, - number = 1, + name = "crossbow", + suit = Card.Club, + number = 1, } Fk:loadTranslationTable{ - ["crossbow"] = "诸葛连弩", + ["crossbow"] = "诸葛连弩", } extension:addCards({ - crossbow, - crossbow:clone(Card.Diamond, 1), + crossbow, + crossbow:clone(Card.Diamond, 1), }) local qingGang = fk.CreateWeapon{ - name = "qinggang_sword", - suit = Card.Spade, - number = 6, + name = "qinggang_sword", + suit = Card.Spade, + number = 6, } Fk:loadTranslationTable{ - ["qinggang_sword"] = "青釭剑", + ["qinggang_sword"] = "青釭剑", } extension:addCards({ - qingGang, + qingGang, }) local iceSword = fk.CreateWeapon{ - name = "ice_sword", - suit = Card.Spade, - number = 2, + name = "ice_sword", + suit = Card.Spade, + number = 2, } Fk:loadTranslationTable{ - ["ice_sword"] = "寒冰剑", + ["ice_sword"] = "寒冰剑", } extension:addCards({ - iceSword, + iceSword, }) local doubleSwords = fk.CreateWeapon{ - name = "double_swords", - suit = Card.Spade, - number = 2, + name = "double_swords", + suit = Card.Spade, + number = 2, } Fk:loadTranslationTable{ - ["double_swords"] = "雌雄双股剑", + ["double_swords"] = "雌雄双股剑", } extension:addCards({ - doubleSwords, + doubleSwords, }) local blade = fk.CreateWeapon{ - name = "blade", - suit = Card.Spade, - number = 5, + name = "blade", + suit = Card.Spade, + number = 5, } Fk:loadTranslationTable{ - ["blade"] = "青龙偃月刀", + ["blade"] = "青龙偃月刀", } extension:addCards({ - blade, + blade, }) local spear = fk.CreateWeapon{ - name = "spear", - suit = Card.Spade, - number = 12, + name = "spear", + suit = Card.Spade, + number = 12, } Fk:loadTranslationTable{ - ["spear"] = "丈八蛇矛", + ["spear"] = "丈八蛇矛", } extension:addCards({ - spear, + spear, }) local axe = fk.CreateWeapon{ - name = "axe", - suit = Card.Diamond, - number = 5, + name = "axe", + suit = Card.Diamond, + number = 5, } Fk:loadTranslationTable{ - ["axe"] = "贯石斧", + ["axe"] = "贯石斧", } extension:addCards({ - axe, + axe, }) local halberd = fk.CreateWeapon{ - name = "halberd", - suit = Card.Diamond, - number = 12, + name = "halberd", + suit = Card.Diamond, + number = 12, } Fk:loadTranslationTable{ - ["halberd"] = "方天画戟", + ["halberd"] = "方天画戟", } extension:addCards({ - halberd, + halberd, }) local kylinBow = fk.CreateWeapon{ - name = "kylin_bow", - suit = Card.Heart, - number = 5, + name = "kylin_bow", + suit = Card.Heart, + number = 5, } Fk:loadTranslationTable{ - ["kylin_bow"] = "麒麟弓", + ["kylin_bow"] = "麒麟弓", } extension:addCards({ - kylinBow, + kylinBow, }) local eightDiagram = fk.CreateArmor{ - name = "eight_diagram", - suit = Card.Spade, - number = 2, + name = "eight_diagram", + suit = Card.Spade, + number = 2, } Fk:loadTranslationTable{ - ["eight_diagram"] = "八卦阵", + ["eight_diagram"] = "八卦阵", } extension:addCards({ - eightDiagram, - eightDiagram:clone(Card.Club, 2), + eightDiagram, + eightDiagram:clone(Card.Club, 2), }) local niohShield = fk.CreateArmor{ - name = "nioh_shield", - suit = Card.Club, - number = 2, + name = "nioh_shield", + suit = Card.Club, + number = 2, } Fk:loadTranslationTable{ - ["nioh_shield"] = "仁王盾", + ["nioh_shield"] = "仁王盾", } extension:addCards({ - niohShield, + niohShield, }) local diLu = fk.CreateDefensiveRide{ - name = "dilu", - suit = Card.Club, - number = 5, + name = "dilu", + suit = Card.Club, + number = 5, } Fk:loadTranslationTable{ - ["dilu"] = "的卢", + ["dilu"] = "的卢", } extension:addCards({ - diLu, + diLu, }) local jueYing = fk.CreateDefensiveRide{ - name = "jueying", - suit = Card.Spade, - number = 5, + name = "jueying", + suit = Card.Spade, + number = 5, } Fk:loadTranslationTable{ - ["jueying"] = "绝影", + ["jueying"] = "绝影", } extension:addCards({ - jueYing, + jueYing, }) local zhuaHuangFeiDian = fk.CreateDefensiveRide{ - name = "zhuahuangfeidian", - suit = Card.Heart, - number = 13, + name = "zhuahuangfeidian", + suit = Card.Heart, + number = 13, } Fk:loadTranslationTable{ - ["zhuahuangfeidian"] = "爪黄飞电", + ["zhuahuangfeidian"] = "爪黄飞电", } extension:addCards({ - zhuaHuangFeiDian, + zhuaHuangFeiDian, }) local chiTu = fk.CreateOffensiveRide{ - name = "chitu", - suit = Card.Heart, - number = 5, + name = "chitu", + suit = Card.Heart, + number = 5, } Fk:loadTranslationTable{ - ["chitu"] = "赤兔", + ["chitu"] = "赤兔", } extension:addCards({ - chiTu, + chiTu, }) local daYuan = fk.CreateOffensiveRide{ - name = "dayuan", - suit = Card.Spade, - number = 13, + name = "dayuan", + suit = Card.Spade, + number = 13, } Fk:loadTranslationTable{ - ["dayuan"] = "大宛", + ["dayuan"] = "大宛", } extension:addCards({ - daYuan, + daYuan, }) local ziXing = fk.CreateOffensiveRide{ - name = "zixing", - suit = Card.Heart, - number = 5, + name = "zixing", + suit = Card.Heart, + number = 5, } Fk:loadTranslationTable{ - ["zixing"] = "紫骍", + ["zixing"] = "紫骍", } extension:addCards({ - ziXing, + ziXing, }) return extension diff --git a/packages/standard_cards/metadata.lua b/packages/standard_cards/metadata.lua index bb56d252..b127e681 100644 --- a/packages/standard_cards/metadata.lua +++ b/packages/standard_cards/metadata.lua @@ -1,14 +1,14 @@ return { - name = "standard_cards", - author = "official", - description = "", - collaborators = { - program = {}, - designer = {}, - cv = {}, - illustrator = {}, - }, + name = "standard_cards", + author = "official", + description = "", + collaborators = { + program = {}, + designer = {}, + cv = {}, + illustrator = {}, + }, - dependencies = {}, - extra_files = {}, + dependencies = {}, + extra_files = {}, } diff --git a/qml/Config.qml b/qml/Config.qml index 17d08bad..db9a5b72 100644 --- a/qml/Config.qml +++ b/qml/Config.qml @@ -1,13 +1,13 @@ import QtQuick 2.15 QtObject { - // Client configuration + // Client configuration - // Player property of client - property string screenName: "" - property string password: "" + // Player property of client + property string screenName: "" + property string password: "" - // Client data - property int roomCapacity: 0 - property int roomTimeout: 0 + // Client data + property int roomCapacity: 0 + property int roomTimeout: 0 } diff --git a/qml/GlobalPopups/CreateRoom.qml b/qml/GlobalPopups/CreateRoom.qml index 8a6383bd..0fa8d71e 100644 --- a/qml/GlobalPopups/CreateRoom.qml +++ b/qml/GlobalPopups/CreateRoom.qml @@ -3,62 +3,62 @@ import QtQuick.Controls 2.0 import QtQuick.Layouts 1.15 Item { - id: root + id: root - width: childrenRect.width - height: childrenRect.height + width: childrenRect.width + height: childrenRect.height - signal finished() + signal finished() - ColumnLayout { - spacing: 20 + ColumnLayout { + spacing: 20 - RowLayout { - anchors.rightMargin: 8 - spacing: 16 - Text { - text: "Room Name" - } - TextField { - id: roomName - font.pixelSize: 18 - text: Self.screenName + "'s Room" - } - } - - RowLayout { - anchors.rightMargin: 8 - spacing: 16 - Text { - text: "Player num" - } - SpinBox { - id: playerNum - from: 2 - to: 8 - } - } - - RowLayout { - anchors.rightMargin: 8 - spacing: 16 - Button { - text: "OK" - onClicked: { - root.finished(); - mainWindow.busy = true; - ClientInstance.notifyServer( - "CreateRoom", - JSON.stringify([roomName.text, playerNum.value]) - ); - } - } - Button { - text: "Cancel" - onClicked: { - root.finished(); - } - } - } + RowLayout { + anchors.rightMargin: 8 + spacing: 16 + Text { + text: "Room Name" + } + TextField { + id: roomName + font.pixelSize: 18 + text: Self.screenName + "'s Room" + } } + + RowLayout { + anchors.rightMargin: 8 + spacing: 16 + Text { + text: "Player num" + } + SpinBox { + id: playerNum + from: 2 + to: 8 + } + } + + RowLayout { + anchors.rightMargin: 8 + spacing: 16 + Button { + text: "OK" + onClicked: { + root.finished(); + mainWindow.busy = true; + ClientInstance.notifyServer( + "CreateRoom", + JSON.stringify([roomName.text, playerNum.value]) + ); + } + } + Button { + text: "Cancel" + onClicked: { + root.finished(); + } + } + } + } } diff --git a/qml/GlobalPopups/EditProfile.qml b/qml/GlobalPopups/EditProfile.qml index c1ceb0d6..acb5b666 100644 --- a/qml/GlobalPopups/EditProfile.qml +++ b/qml/GlobalPopups/EditProfile.qml @@ -3,98 +3,98 @@ import QtQuick.Controls 2.0 import QtQuick.Layouts 1.15 Item { - id: root + id: root - width: childrenRect.width - height: childrenRect.height + width: childrenRect.width + height: childrenRect.height - signal finished() + signal finished() - ColumnLayout { - spacing: 20 + ColumnLayout { + spacing: 20 - RowLayout { - anchors.rightMargin: 8 - spacing: 16 - Text { - text: "Username" - } - Text { - text: Self.screenName - font.pixelSize: 18 - } - } - - RowLayout { - anchors.rightMargin: 8 - spacing: 16 - Text { - text: "Avatar" - } - TextField { - id: avatarName - font.pixelSize: 18 - text: Self.avatar - } - } - - RowLayout { - anchors.rightMargin: 8 - spacing: 16 - Text { - text: "Old Password" - } - TextField { - id: oldPassword - echoMode: TextInput.Password - passwordCharacter: "*" - } - } - - RowLayout { - anchors.rightMargin: 8 - spacing: 16 - Text { - text: "New Password" - } - TextField { - id: newPassword - echoMode: TextInput.Password - passwordCharacter: "*" - } - } - - RowLayout { - anchors.rightMargin: 8 - spacing: 16 - Button { - text: "Update Avatar" - enabled: avatarName.text !== "" - onClicked: { - mainWindow.busy = true; - ClientInstance.notifyServer( - "UpdateAvatar", - JSON.stringify([avatarName.text]) - ); - } - } - Button { - text: "Update Password" - enabled: oldPassword.text !== "" && newPassword.text !== "" - onClicked: { - mainWindow.busy = true; - ClientInstance.notifyServer( - "UpdatePassword", - JSON.stringify([oldPassword.text, newPassword.text]) - ); - } - } - Button { - text: "Exit" - onClicked: { - root.finished(); - } - } - } + RowLayout { + anchors.rightMargin: 8 + spacing: 16 + Text { + text: "Username" + } + Text { + text: Self.screenName + font.pixelSize: 18 + } } + + RowLayout { + anchors.rightMargin: 8 + spacing: 16 + Text { + text: "Avatar" + } + TextField { + id: avatarName + font.pixelSize: 18 + text: Self.avatar + } + } + + RowLayout { + anchors.rightMargin: 8 + spacing: 16 + Text { + text: "Old Password" + } + TextField { + id: oldPassword + echoMode: TextInput.Password + passwordCharacter: "*" + } + } + + RowLayout { + anchors.rightMargin: 8 + spacing: 16 + Text { + text: "New Password" + } + TextField { + id: newPassword + echoMode: TextInput.Password + passwordCharacter: "*" + } + } + + RowLayout { + anchors.rightMargin: 8 + spacing: 16 + Button { + text: "Update Avatar" + enabled: avatarName.text !== "" + onClicked: { + mainWindow.busy = true; + ClientInstance.notifyServer( + "UpdateAvatar", + JSON.stringify([avatarName.text]) + ); + } + } + Button { + text: "Update Password" + enabled: oldPassword.text !== "" && newPassword.text !== "" + onClicked: { + mainWindow.busy = true; + ClientInstance.notifyServer( + "UpdatePassword", + JSON.stringify([oldPassword.text, newPassword.text]) + ); + } + } + Button { + text: "Exit" + onClicked: { + root.finished(); + } + } + } + } } diff --git a/qml/GlobalPopups/Test.qml b/qml/GlobalPopups/Test.qml index 001f9e78..a893f772 100644 --- a/qml/GlobalPopups/Test.qml +++ b/qml/GlobalPopups/Test.qml @@ -1,5 +1,5 @@ import QtQuick 2.0 Text { - text: "dsdsd" + text: "dsdsd" } \ No newline at end of file diff --git a/qml/Logic.js b/qml/Logic.js index 050dd905..f1b16800 100644 --- a/qml/Logic.js +++ b/qml/Logic.js @@ -1,53 +1,53 @@ var callbacks = {}; callbacks["NetworkDelayTest"] = function(jsonData) { - ClientInstance.notifyServer("Setup", JSON.stringify([ - config.screenName, - config.password - ])); + ClientInstance.notifyServer("Setup", JSON.stringify([ + config.screenName, + config.password + ])); } callbacks["ErrorMsg"] = function(jsonData) { - console.log("ERROR: " + jsonData); - toast.show(jsonData, 5000); - mainWindow.busy = false; + console.log("ERROR: " + jsonData); + toast.show(jsonData, 5000); + mainWindow.busy = false; } callbacks["BackToStart"] = function(jsonData) { - while (mainStack.depth > 1) { - mainStack.pop(); - } + while (mainStack.depth > 1) { + mainStack.pop(); + } } callbacks["EnterLobby"] = function(jsonData) { - // depth == 1 means the lobby page is not present in mainStack - if (mainStack.depth === 1) { - mainStack.push(lobby); - } else { - mainStack.pop(); - } - mainWindow.busy = false; + // depth == 1 means the lobby page is not present in mainStack + if (mainStack.depth === 1) { + mainStack.push(lobby); + } else { + mainStack.pop(); + } + mainWindow.busy = false; } callbacks["EnterRoom"] = function(jsonData) { - // jsonData: int capacity, int timeout - let data = JSON.parse(jsonData); - config.roomCapacity = data[0]; - config.roomTimeout = data[1]; - mainStack.push(room); - mainWindow.busy = false; + // jsonData: int capacity, int timeout + let data = JSON.parse(jsonData); + config.roomCapacity = data[0]; + config.roomTimeout = data[1]; + mainStack.push(room); + mainWindow.busy = false; } callbacks["UpdateRoomList"] = function(jsonData) { - let current = mainStack.currentItem; // should be lobby - current.roomModel.clear(); - JSON.parse(jsonData).forEach(function(room) { - current.roomModel.append({ - roomId: room[0], - roomName: room[1], - gameMode: room[2], - playerNum: room[3], - capacity: room[4], - }); + let current = mainStack.currentItem; // should be lobby + current.roomModel.clear(); + JSON.parse(jsonData).forEach(function(room) { + current.roomModel.append({ + roomId: room[0], + roomName: room[1], + gameMode: room[2], + playerNum: room[3], + capacity: room[4], }); + }); } diff --git a/qml/Pages/CardsOverview.qml b/qml/Pages/CardsOverview.qml index db582201..46e82c63 100644 --- a/qml/Pages/CardsOverview.qml +++ b/qml/Pages/CardsOverview.qml @@ -4,49 +4,49 @@ import QtQuick.Controls 2.0 import "RoomElement" Item { - id: root + id: root - property bool loaded: false + property bool loaded: false - ListView { - width: Math.floor(root.width / 98) * 98 - height: parent.height - anchors.centerIn: parent - ScrollBar.vertical: ScrollBar {} - model: ListModel { - id: packages - } + ListView { + width: Math.floor(root.width / 98) * 98 + height: parent.height + anchors.centerIn: parent + ScrollBar.vertical: ScrollBar {} + model: ListModel { + id: packages + } - delegate: ColumnLayout { - Text { text: Backend.translate(name) } - GridLayout { - columns: root.width / 98 - Repeater { - model: JSON.parse(Backend.getCards(name)) - CardItem { - autoBack: false - Component.onCompleted: { - let data = JSON.parse(Backend.getCardData(modelData)); - setData(data); - } - } - } + delegate: ColumnLayout { + Text { text: Backend.translate(name) } + GridLayout { + columns: root.width / 98 + Repeater { + model: JSON.parse(Backend.callLuaFunction("GetCards", [name])) + CardItem { + autoBack: false + Component.onCompleted: { + let data = JSON.parse(Backend.callLuaFunction("GetCardData", [modelData])); + setData(data); } + } } + } } + } - Button { - text: "Quit" - anchors.right: parent.right - onClicked: { - mainStack.pop(); - } + Button { + text: "Quit" + anchors.right: parent.right + onClicked: { + mainStack.pop(); } + } - function loadPackages() { - if (loaded) return; - let packs = JSON.parse(Backend.getAllCardPack()); - packs.forEach((name) => packages.append({ name: name })); - loaded = true; - } + function loadPackages() { + if (loaded) return; + let packs = JSON.parse(Backend.callLuaFunction("GetAllCardPack", [])); + packs.forEach((name) => packages.append({ name: name })); + loaded = true; + } } diff --git a/qml/Pages/GeneralsOverview.qml b/qml/Pages/GeneralsOverview.qml index d2fd23c5..622008cf 100644 --- a/qml/Pages/GeneralsOverview.qml +++ b/qml/Pages/GeneralsOverview.qml @@ -4,50 +4,50 @@ import QtQuick.Controls 2.0 import "RoomElement" Item { - id: root + id: root - property bool loaded: false + property bool loaded: false - ListView { - width: Math.floor(root.width / 98) * 98 - height: parent.height - anchors.centerIn: parent - ScrollBar.vertical: ScrollBar {} - model: ListModel { - id: packages - } + ListView { + width: Math.floor(root.width / 98) * 98 + height: parent.height + anchors.centerIn: parent + ScrollBar.vertical: ScrollBar {} + model: ListModel { + id: packages + } - delegate: ColumnLayout { - Text { text: Backend.translate(name) } - GridLayout { - columns: root.width / 98 - Repeater { - model: JSON.parse(Backend.getGenerals(name)) - GeneralCardItem { - autoBack: false - Component.onCompleted: { - let data = JSON.parse(Backend.getGeneralData(modelData)); - name = modelData; - kingdom = data.kingdom; - } - } - } + delegate: ColumnLayout { + Text { text: Backend.translate(name) } + GridLayout { + columns: root.width / 98 + Repeater { + model: JSON.parse(Backend.callLuaFunction("GetGenerals", [name])) + GeneralCardItem { + autoBack: false + Component.onCompleted: { + let data = JSON.parse(Backend.callLuaFunction("GetGeneralData", [modelData])); + name = modelData; + kingdom = data.kingdom; } + } } + } } + } - Button { - text: "Quit" - anchors.right: parent.right - onClicked: { - mainStack.pop(); - } + Button { + text: "Quit" + anchors.right: parent.right + onClicked: { + mainStack.pop(); } + } - function loadPackages() { - if (loaded) return; - let packs = JSON.parse(Backend.getAllGeneralPack()); - packs.forEach((name) => packages.append({ name: name })); - loaded = true; - } + function loadPackages() { + if (loaded) return; + let packs = JSON.parse(Backend.callLuaFunction("GetAllGeneralPack", [])); + packs.forEach((name) => packages.append({ name: name })); + loaded = true; + } } diff --git a/qml/Pages/Init.qml b/qml/Pages/Init.qml index 32622c56..0d513301 100644 --- a/qml/Pages/Init.qml +++ b/qml/Pages/Init.qml @@ -2,50 +2,50 @@ import QtQuick 2.15 import QtQuick.Controls 2.0 Item { - id: root + id: root - Frame { - id: join_server - anchors.centerIn: parent - Column { - spacing: 8 - TextField { - id: server_addr - text: "127.0.0.1" - } - TextField { - id: screenNameEdit - text: "player" - } - /*TextField { - id: avatarEdit - text: "liubei" - }*/ - TextField { - id: passwordEdit - text: "" - echoMode: TextInput.Password - passwordCharacter: "*" - } - Button { - text: "Join Server" - onClicked: { - config.screenName = screenNameEdit.text; - config.password = passwordEdit.text; - mainWindow.busy = true; - Backend.joinServer(server_addr.text); - } - } - Button { - text: "Console start" - onClicked: { - config.screenName = screenNameEdit.text; - config.password = passwordEdit.text; - mainWindow.busy = true; - Backend.startServer(9527); - Backend.joinServer("127.0.0.1"); - } - } + Frame { + id: join_server + anchors.centerIn: parent + Column { + spacing: 8 + TextField { + id: server_addr + text: "127.0.0.1" + } + TextField { + id: screenNameEdit + text: "player" + } + /*TextField { + id: avatarEdit + text: "liubei" + }*/ + TextField { + id: passwordEdit + text: "" + echoMode: TextInput.Password + passwordCharacter: "*" + } + Button { + text: "Join Server" + onClicked: { + config.screenName = screenNameEdit.text; + config.password = passwordEdit.text; + mainWindow.busy = true; + Backend.joinServer(server_addr.text); } + } + Button { + text: "Console start" + onClicked: { + config.screenName = screenNameEdit.text; + config.password = passwordEdit.text; + mainWindow.busy = true; + Backend.startServer(9527); + Backend.joinServer("127.0.0.1"); + } + } } + } } diff --git a/qml/Pages/Lobby.qml b/qml/Pages/Lobby.qml index 06786652..280611f5 100644 --- a/qml/Pages/Lobby.qml +++ b/qml/Pages/Lobby.qml @@ -5,154 +5,154 @@ import QtQuick.Layouts 1.15 import "Logic.js" as Logic Item { - id: root - property alias roomModel: roomModel - Component { - id: roomDelegate - - RowLayout { - width: roomList.width * 0.9 - spacing: 16 - Text { - text: roomId - } - - Text { - horizontalAlignment: Text.AlignHCenter - Layout.fillWidth: true - text: roomName - } - - Text { - text: gameMode - } - - Text { - color: (playerNum == capacity) ? "red" : "black" - text: playerNum + "/" + capacity - } - - Text { - text: "Enter" - font.underline: true - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: { parent.color = "blue" } - onExited: { parent.color = "black" } - onClicked: { - mainWindow.busy = true; - ClientInstance.notifyServer( - "EnterRoom", - JSON.stringify([roomId]) - ); - } - } - } - } - } - - ListModel { - id: roomModel - } + id: root + property alias roomModel: roomModel + Component { + id: roomDelegate RowLayout { - anchors.fill: parent - Rectangle { - Layout.preferredWidth: root.width * 0.7 - Layout.fillHeight: true - color: "#e2e2e1" - radius: 4 - Text { - width: parent.width - horizontalAlignment: Text.AlignHCenter - text: "Room List" - } - ListView { - height: parent.height * 0.9 - width: parent.width * 0.95 - contentHeight: roomDelegate.height * count - ScrollBar.vertical: ScrollBar {} - anchors.centerIn: parent - id: roomList - delegate: roomDelegate - model: roomModel - } - } + width: roomList.width * 0.9 + spacing: 16 + Text { + text: roomId + } - ColumnLayout { - Button { - text: "Edit Profile" - onClicked: { - globalPopup.source = "EditProfile.qml"; - globalPopup.open(); - } - } - Button { - text: "Create Room" - onClicked: { - globalPopup.source = "CreateRoom.qml"; - globalPopup.open(); - } - } - Button { - text: "Generals Overview" - onClicked: { - mainStack.push(generalsOverview); - mainStack.currentItem.loadPackages(); - } - } - Button { - text: "Cards Overview" - onClicked: { - mainStack.push(cardsOverview); - mainStack.currentItem.loadPackages(); - } - } - Button { - text: "Scenarios Overview" - } - Button { - text: "About" - } - Button { - text: "Exit Lobby" - onClicked: { - toast.show("Goodbye."); - Backend.quitLobby(); - mainStack.pop(); - } - } + Text { + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + text: roomName + } + + Text { + text: gameMode + } + + Text { + color: (playerNum == capacity) ? "red" : "black" + text: playerNum + "/" + capacity + } + + Text { + text: "Enter" + font.underline: true + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { parent.color = "blue" } + onExited: { parent.color = "black" } + onClicked: { + mainWindow.busy = true; + ClientInstance.notifyServer( + "EnterRoom", + JSON.stringify([roomId]) + ); + } } + } + } + } + + ListModel { + id: roomModel + } + + RowLayout { + anchors.fill: parent + Rectangle { + Layout.preferredWidth: root.width * 0.7 + Layout.fillHeight: true + color: "#e2e2e1" + radius: 4 + Text { + width: parent.width + horizontalAlignment: Text.AlignHCenter + text: "Room List" + } + ListView { + height: parent.height * 0.9 + width: parent.width * 0.95 + contentHeight: roomDelegate.height * count + ScrollBar.vertical: ScrollBar {} + anchors.centerIn: parent + id: roomList + delegate: roomDelegate + model: roomModel + } } - Loader { - id: lobby_dialog - z: 1000 - onSourceChanged: { - if (item === null) - return; - item.finished.connect(function(){ - source = ""; - }); - item.widthChanged.connect(function(){ - lobby_dialog.moveToCenter(); - }); - item.heightChanged.connect(function(){ - lobby_dialog.moveToCenter(); - }); - moveToCenter(); + ColumnLayout { + Button { + text: "Edit Profile" + onClicked: { + globalPopup.source = "EditProfile.qml"; + globalPopup.open(); } + } + Button { + text: "Create Room" + onClicked: { + globalPopup.source = "CreateRoom.qml"; + globalPopup.open(); + } + } + Button { + text: "Generals Overview" + onClicked: { + mainStack.push(generalsOverview); + mainStack.currentItem.loadPackages(); + } + } + Button { + text: "Cards Overview" + onClicked: { + mainStack.push(cardsOverview); + mainStack.currentItem.loadPackages(); + } + } + Button { + text: "Scenarios Overview" + } + Button { + text: "About" + } + Button { + text: "Exit Lobby" + onClicked: { + toast.show("Goodbye."); + Backend.quitLobby(); + mainStack.pop(); + } + } + } + } - function moveToCenter() - { - item.x = Math.round((root.width - item.width) / 2); - item.y = Math.round(root.height * 0.67 - item.height / 2); - } + Loader { + id: lobby_dialog + z: 1000 + onSourceChanged: { + if (item === null) + return; + item.finished.connect(function(){ + source = ""; + }); + item.widthChanged.connect(function(){ + lobby_dialog.moveToCenter(); + }); + item.heightChanged.connect(function(){ + lobby_dialog.moveToCenter(); + }); + moveToCenter(); } - Component.onCompleted: { - toast.show("Welcome to FreeKill lobby!"); + function moveToCenter() + { + item.x = Math.round((root.width - item.width) / 2); + item.y = Math.round(root.height * 0.67 - item.height / 2); } + } + + Component.onCompleted: { + toast.show("Welcome to FreeKill lobby!"); + } } diff --git a/qml/Pages/Logic.js b/qml/Pages/Logic.js index 0c0831c7..e1578741 100644 --- a/qml/Pages/Logic.js +++ b/qml/Pages/Logic.js @@ -1,13 +1,13 @@ callbacks["UpdateAvatar"] = function(jsonData) { - mainWindow.busy = false; - Self.avatar = jsonData; - toast.show("Update avatar done."); + mainWindow.busy = false; + Self.avatar = jsonData; + toast.show("Update avatar done."); } callbacks["UpdatePassword"] = function(jsonData) { - mainWindow.busy = false; - if (jsonData === "1") - toast.show("Update password done."); - else - toast.show("Old password wrong!", 5000); + mainWindow.busy = false; + if (jsonData === "1") + toast.show("Update password done."); + else + toast.show("Old password wrong!", 5000); } diff --git a/qml/Pages/MetroButton.qml b/qml/Pages/MetroButton.qml index 78d6cc97..3027cf0a 100644 --- a/qml/Pages/MetroButton.qml +++ b/qml/Pages/MetroButton.qml @@ -1,69 +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 + 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 + signal clicked - id: button - width: icon.width + title.implicitWidth + padding * 2 - height: Math.max(icon.height, title.implicitHeight) + padding * 2 + 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 + 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 } - 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" - } + 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 0a5c1d16..5c1ff15d 100644 --- a/qml/Pages/Room.qml +++ b/qml/Pages/Room.qml @@ -5,325 +5,347 @@ import "RoomElement" import "RoomLogic.js" as Logic Item { - id: roomScene + id: roomScene - property int playerNum: 0 - property var dashboardModel + property int playerNum: 0 + property var dashboardModel - property bool isOwner: false - property bool isStarted: false + property bool isOwner: false + property bool isStarted: false - property alias popupBox: popupBox - property alias promptText: prompt.text + property alias popupBox: popupBox + property alias promptText: prompt.text + + property var selected_targets: [] + + // tmp + Row { + Button{text:"摸1牌" + onClicked:{ + Logic.moveCards([{ + from:Logic.Player.DrawPile, + to:Logic.Player.PlaceHand, + cards:[1], + }]) + }} + Button{text:"弃1牌" + onClicked:{Logic.moveCards([{ + to:Logic.Player.DrawPile, + from:Logic.Player.PlaceHand, + cards:[1], + }])}} + } + Button { + text: "quit" + anchors.top: parent.top + anchors.right: parent.right + onClicked: { + ClientInstance.clearPlayers(); + ClientInstance.notifyServer("QuitRoom", "[]"); + } + } + Button { + text: "add robot" + visible: dashboardModel.isOwner && !isStarted + anchors.centerIn: parent + onClicked: { + ClientInstance.notifyServer("AddRobot", "[]"); + } + } + + states: [ + State { name: "notactive" }, // Normal status + State { name: "playing" }, // Playing cards in playing phase + State { name: "responding" }, // all requests need to operate dashboard + State { name: "replying" } // requests only operate a popup window + ] + state: "notactive" + transitions: [ + Transition { + from: "*"; to: "notactive" + ScriptAction { + script: { + promptText = ""; + progress.visible = false; + okCancel.visible = false; + endPhaseButton.visible = false; + + dashboard.disableAllCards(); + if (dashboard.pending_skill !== "") + dashboard.stopPending(); + selected_targets = []; + + if (popupBox.item != null) { + popupBox.item.finished(); + } + } + } + }, + + Transition { + from: "*"; to: "playing" + ScriptAction { + script: { + dashboard.enableCards(); + progress.visible = true; + okCancel.visible = true; + endPhaseButton.visible = true; + } + } + }, + + Transition { + from: "*"; to: "responding" + ScriptAction { + script: { + progress.visible = true; + okCancel.visible = true; + } + } + }, + + Transition { + from: "*"; to: "replying" + ScriptAction { + script: { + progress.visible = true; + } + } + } + ] + + /* Layout: + * +---------------------+ + * | Photos, get more | + * | in arrangePhotos() | + * | tablePile | + * | progress,prompt,btn | + * +---------------------+ + * | dashboard | + * +---------------------+ + */ + + ListModel { + id: photoModel + } + + Item { + id: roomArea + width: roomScene.width + height: roomScene.height - dashboard.height + + Repeater { + id: photos + model: photoModel + Photo { + playerid: model.id + 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 + faceup: model.faceup + chained: model.chained + drank: model.drank + isOwner: model.isOwner + + onSelectedChanged: { + Logic.updateSelectedTargets(playerid, selected); + } + } + } + + onWidthChanged: Logic.arrangePhotos(); + onHeightChanged: Logic.arrangePhotos(); + + InvisibleCardArea { + id: drawPile + x: parent.width / 2 + y: roomScene.height / 2 + } + + TablePile { + id: tablePile + width: parent.width * 0.6 + height: 150 + x: parent.width * 0.2 + y: parent.height * 0.6 + } + } + + Dashboard { + id: dashboard + width: roomScene.width + anchors.top: roomArea.bottom + + self.playerid: dashboardModel.id + self.general: dashboardModel.general + self.screenName: dashboardModel.screenName + self.role: dashboardModel.role + self.kingdom: dashboardModel.kingdom + self.netstate: dashboardModel.netstate + self.maxHp: dashboardModel.maxHp + self.hp: dashboardModel.hp + self.seatNumber: dashboardModel.seatNumber + self.isDead: dashboardModel.isDead + self.dying: dashboardModel.dying + self.faceup: dashboardModel.faceup + self.chained: dashboardModel.chained + self.drank: dashboardModel.drank + self.isOwner: dashboardModel.isOwner + + onSelectedChanged: { + Logic.updateSelectedTargets(self.playerid, selected); + } + + onCardSelected: { + Logic.enableTargets(card); + } + } + + Item { + id: controls + anchors.bottom: dashboard.top + anchors.bottomMargin: -40 + width: roomScene.width + + Text { + id: prompt + visible: progress.visible + anchors.bottom: progress.top + anchors.bottomMargin: 8 + anchors.horizontalCenter: progress.horizontalCenter + } + + ProgressBar { + id: progress + width: parent.width * 0.6 + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: okCancel.top + anchors.bottomMargin: 8 + from: 0.0 + to: 100.0 + + visible: false + NumberAnimation on value { + running: progress.visible + from: 100.0 + to: 0.0 + duration: config.roomTimeout * 1000 + + onFinished: { + roomScene.state = "notactive" + } + } + } - // tmp Row { - Button{text:"摸1牌" - onClicked:{ - Logic.moveCards([{ - from:Logic.Player.DrawPile, - to:Logic.Player.PlaceHand, - cards:[1], - }]) - }} - Button{text:"弃1牌" - onClicked:{Logic.moveCards([{ - to:Logic.Player.DrawPile, - from:Logic.Player.PlaceHand, - cards:[1], - }])}} + id: okCancel + anchors.bottom: parent.bottom + anchors.horizontalCenter: progress.horizontalCenter + spacing: 20 + visible: false + + Button { + id: okButton + text: "OK" + onClicked: Logic.doOkButton(); + } + + Button { + id: cancelButton + text: "Cancel" + onClicked: Logic.doCancelButton(); + } } + Button { - text: "quit" - anchors.top: parent.top - anchors.right: parent.right - onClicked: { - ClientInstance.clearPlayers(); - ClientInstance.notifyServer("QuitRoom", "[]"); - } + id: endPhaseButton + text: "End" + anchors.bottom: parent.bottom + anchors.bottomMargin: 40 + anchors.right: parent.right + anchors.rightMargin: 30 + visible: false; + onClicked: Logic.doCancelButton(); } - Button { - text: "add robot" - visible: dashboardModel.isOwner && !isStarted - anchors.centerIn: parent - onClicked: { - ClientInstance.notifyServer("AddRobot", "[]"); - } + } + + 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(); } - states: [ - State { name: "notactive" }, // Normal status - State { name: "playing" }, // Playing cards in playing phase - State { name: "responding" }, // all requests need to operate dashboard - State { name: "replying" } // requests only operate a popup window - ] - state: "notactive" - transitions: [ - Transition { - from: "*"; to: "notactive" - ScriptAction { - script: { - promptText = ""; - progress.visible = false; - okCancel.visible = false; - endPhaseButton.visible = false; + function moveToCenter() + { + item.x = Math.round((roomArea.width - item.width) / 2); + item.y = Math.round(roomArea.height * 0.67 - item.height / 2); + } + } - if (popupBox.item != null) { - popupBox.item.finished(); - } - } - } - }, + Component.onCompleted: { + toast.show("Sucesessfully entered room."); - Transition { - from: "*"; to: "playing" - ScriptAction { - script: { - progress.visible = true; - okCancel.visible = true; - endPhaseButton.visible = true; - } - } - }, - - Transition { - from: "*"; to: "responding" - ScriptAction { - script: { - progress.visible = true; - okCancel.visible = true; - } - } - }, - - Transition { - from: "*"; to: "replying" - ScriptAction { - script: { - progress.visible = true; - } - } - } - ] - - /* Layout: - * +---------------------+ - * | Photos, get more | - * | in arrangePhotos() | - * | tablePile | - * | progress,prompt,btn | - * +---------------------+ - * | dashboard | - * +---------------------+ - */ - - ListModel { - id: photoModel + dashboardModel = { + id: Self.id, + general: Self.avatar, + screenName: Self.screenName, + role: "unknown", + kingdom: "qun", + netstate: "online", + maxHp: 0, + hp: 0, + seatNumber: 1, + isDead: false, + dying: false, + faceup: true, + chained: false, + drank: false, + isOwner: false } - Item { - id: roomArea - width: roomScene.width - height: roomScene.height - dashboard.height + playerNum = config.roomCapacity; - Repeater { - id: photos - model: photoModel - Photo { - 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 - faceup: model.faceup - chained: model.chained - drank: model.drank - isOwner: model.isOwner - } - } - - onWidthChanged: Logic.arrangePhotos(); - onHeightChanged: Logic.arrangePhotos(); - - InvisibleCardArea { - id: drawPile - x: parent.width / 2 - y: roomScene.height / 2 - } - - TablePile { - id: tablePile - width: parent.width * 0.6 - height: 150 - x: parent.width * 0.2 - y: parent.height * 0.6 - } + let i; + for (i = 1; i < playerNum; i++) { + 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, + faceup: true, + chained: false, + drank: false, + isOwner: false + }); } - Dashboard { - id: dashboard - width: roomScene.width - anchors.top: roomArea.bottom - - self.general: dashboardModel.general - self.screenName: dashboardModel.screenName - self.role: dashboardModel.role - self.kingdom: dashboardModel.kingdom - self.netstate: dashboardModel.netstate - self.maxHp: dashboardModel.maxHp - self.hp: dashboardModel.hp - self.seatNumber: dashboardModel.seatNumber - self.isDead: dashboardModel.isDead - self.dying: dashboardModel.dying - self.faceup: dashboardModel.faceup - self.chained: dashboardModel.chained - self.drank: dashboardModel.drank - self.isOwner: dashboardModel.isOwner - } - - Item { - id: controls - anchors.bottom: dashboard.top - anchors.bottomMargin: -40 - width: roomScene.width - - Text { - id: prompt - visible: progress.visible - anchors.bottom: progress.top - anchors.bottomMargin: 8 - anchors.horizontalCenter: progress.horizontalCenter - } - - ProgressBar { - id: progress - width: parent.width * 0.6 - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: okCancel.top - anchors.bottomMargin: 8 - from: 0.0 - to: 100.0 - - visible: false - NumberAnimation on value { - running: progress.visible - from: 100.0 - to: 0.0 - duration: config.roomTimeout * 1000 - - onFinished: { - roomScene.state = "notactive" - } - } - } - - Row { - id: okCancel - anchors.bottom: parent.bottom - anchors.horizontalCenter: progress.horizontalCenter - spacing: 20 - visible: false - - Button { - id: okButton - text: "OK" - onClicked: Logic.doOkButton(); - } - - Button { - id: cancelButton - text: "Cancel" - onClicked: Logic.doCancelButton(); - } - } - - Button { - id: endPhaseButton - text: "End" - anchors.bottom: parent.bottom - anchors.bottomMargin: 40 - anchors.right: parent.right - anchors.rightMargin: 30 - visible: false; - onClicked: Logic.doCancelButton(); - } - } - - 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."); - - dashboardModel = { - id: Self.id, - general: Self.avatar, - screenName: Self.screenName, - role: "unknown", - kingdom: "qun", - netstate: "online", - maxHp: 0, - hp: 0, - seatNumber: 1, - isDead: false, - dying: false, - faceup: true, - chained: false, - drank: false, - isOwner: false - } - - playerNum = config.roomCapacity; - - let i; - for (i = 1; i < playerNum; i++) { - 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, - faceup: true, - chained: false, - drank: false, - isOwner: false - }); - } - - Logic.arrangePhotos(); - } + Logic.arrangePhotos(); + } } diff --git a/qml/Pages/RoomElement/CardArea.qml b/qml/Pages/RoomElement/CardArea.qml index c2ce9ae7..d3b92e98 100644 --- a/qml/Pages/RoomElement/CardArea.qml +++ b/qml/Pages/RoomElement/CardArea.qml @@ -3,75 +3,75 @@ import QtQuick 2.15 // CardArea stores CardItem. Item { - property var cards: [] - property int length: 0 + property var cards: [] + property int length: 0 - id: root + id: root - function add(inputs) - { - if (inputs instanceof Array) { - cards.push(...inputs); - } else { - cards.push(inputs); + function add(inputs) + { + if (inputs instanceof Array) { + cards.push(...inputs); + } else { + cards.push(inputs); + } + length = cards.length; + } + + function remove(outputs) + { + 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); + i--; + break; } - length = cards.length; + } + } + length = cards.length; + return result; + } + + function updateCardPosition(animated) + { + let i, card; + + let overflow = false; + for (i = 0; i < cards.length; i++) { + card = cards[i]; + card.origX = i * card.width; + if (card.origX + card.width >= root.width) { + overflow = true; + break; + } + card.origY = 0; } - function remove(outputs) - { - 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); - i--; - break; - } - } - } - length = cards.length; - return result; + if (overflow) { + // TODO: Adjust cards in multiple lines if there are too many cards + 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; + card.origY = 0; + } } - function updateCardPosition(animated) - { - let i, card; - - let overflow = false; - for (i = 0; i < cards.length; i++) { - card = cards[i]; - card.origX = i * card.width; - if (card.origX + card.width >= root.width) { - overflow = true; - break; - } - card.origY = 0; - } - - if (overflow) { - // TODO: Adjust cards in multiple lines if there are too many cards - 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; - card.origY = 0; - } - } - - let parentPos = roomScene.mapFromItem(root, 0, 0); - for (i = 0; i < cards.length; i++) { - card = cards[i]; - card.origX += parentPos.x; - card.origY += parentPos.y; - } - - if (animated) { - for (i = 0; i < cards.length; i++) - cards[i].goBack(true); - } + let parentPos = roomScene.mapFromItem(root, 0, 0); + for (i = 0; i < cards.length; i++) { + card = cards[i]; + card.origX += parentPos.x; + card.origY += parentPos.y; } + + if (animated) { + for (i = 0; i < cards.length; i++) + cards[i].goBack(true); + } + } } diff --git a/qml/Pages/RoomElement/CardItem.qml b/qml/Pages/RoomElement/CardItem.qml index d9d15b1b..df784dcc 100644 --- a/qml/Pages/RoomElement/CardItem.qml +++ b/qml/Pages/RoomElement/CardItem.qml @@ -13,242 +13,242 @@ import "../skin-bank.js" as SkinBank */ Item { - id: root - width: 93 - height: 130 + id: root + width: 93 + height: 130 - // properties for the view - property string suit: "club" - property int number: 7 - property string name: "slash" - property string subtype: "" - property string color: "" // only use when suit is empty - property string footnote: "" // footnote, e.g. "A use card to B" - 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 + // properties for the view + property string suit: "club" + property int number: 7 + property string name: "slash" + property string subtype: "" + property string color: "" // only use when suit is empty + property string footnote: "" // footnote, e.g. "A use card to B" + 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() { - if (suit != "") - return (suit == "heart" || suit == "diamond") ? "red" : "black"; - else return color; + function getColor() { + if (suit != "") + return (suit == "heart" || suit == "diamond") ? "red" : "black"; + else return color; + } + + // properties for animation and game system + 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: 1 + property bool isClicked: false + property bool moveAborted: false + property alias goBackAnim: goBackAnimation + property int goBackDuration: 500 + + signal toggleDiscards() + signal clicked() + signal doubleClicked() + signal thrown() + signal released() + signal entered() + signal exited() + signal moveFinished() + signal generalChanged() // For choose general freely + signal hoverChanged(bool enter) + + RectangularGlow { + id: glowItem + anchors.fill: parent + glowRadius: 8 + spread: 0 + color: "#88FFFFFF" + cornerRadius: 8 + visible: false + } + + Image { + id: cardItem + source: known ? (name != "" ? SkinBank.CARD_DIR + name : "") + : (SkinBank.CARD_DIR + "card-back") + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + } + + Image { + id: suitItem + visible: known + source: suit != "" ? SkinBank.CARD_SUIT_DIR + suit : "" + x: 3 + y: 19 + width: 21 + height: 17 + } + + Image { + id: numberItem + visible: known + source: (suit != "" && number > 0) ? SkinBank.CARD_DIR + + "number/" + root.getColor() + "/" + number : "" + x: 0 + y: 0 + width: 27 + height: 28 + } + + Image { + id: colorItem + visible: known && suit == "" + source: (visible && color != "") ? SkinBank.CARD_SUIT_DIR + "/" + color : "" + x: 1 + } + + GlowText { + id: footnoteItem + text: footnote + x: 6 + y: parent.height - height - 6 + width: root.width - x * 2 + color: "#E4D5A0" + visible: footnoteVisible + wrapMode: Text.WrapAnywhere + horizontalAlignment: Text.AlignHCenter + font.family: "FZLiBian-S02" + font.pixelSize: 14 + glow.color: "black" + glow.spread: 1 + glow.radius: 1 + glow.samples: 12 + } + + Rectangle { + visible: !root.selectable + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.5) + 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(); } - // properties for animation and game system - 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: 1 - property bool isClicked: false - property bool moveAborted: false - property alias goBackAnim: goBackAnimation - property int goBackDuration: 500 - - signal toggleDiscards() - signal clicked() - signal doubleClicked() - signal thrown() - signal released() - signal entered() - signal exited() - signal moveFinished() - signal generalChanged() // For choose general freely - signal hoverChanged(bool enter) - - RectangularGlow { - id: glowItem - anchors.fill: parent - glowRadius: 8 - spread: 0 - color: "#88FFFFFF" - cornerRadius: 8 - visible: false + onEntered: { + parent.entered(); + if (draggable) { + glow.visible = true; + root.z++; + } } - Image { - id: cardItem - source: known ? (name != "" ? SkinBank.CARD_DIR + name : "") - : (SkinBank.CARD_DIR + "card-back") - anchors.fill: parent - fillMode: Image.PreserveAspectCrop + onExited: { + parent.exited(); + if (draggable) { + glow.visible = false; + root.z--; + } } - Image { - id: suitItem - visible: known - source: suit != "" ? SkinBank.CARD_SUIT_DIR + suit : "" - x: 3 - y: 19 - width: 21 - height: 17 + onClicked: { + selected = selectable ? !selected : false; + parent.clicked(); + } + } + + ParallelAnimation { + id: goBackAnimation + + PropertyAnimation { + target: root + property: "x" + to: origX + easing.type: Easing.OutQuad + duration: goBackDuration } - Image { - id: numberItem - visible: known - source: (suit != "" && number > 0) ? SkinBank.CARD_DIR - + "number/" + root.getColor() + "/" + number : "" - x: 0 - y: 0 - width: 27 - height: 28 + PropertyAnimation { + target: root + property: "y" + to: origY + easing.type: Easing.OutQuad + duration: goBackDuration } - Image { - id: colorItem - visible: known && suit == "" - source: (visible && color != "") ? SkinBank.CARD_SUIT_DIR + "/" + color : "" - x: 1 + SequentialAnimation { + PropertyAnimation { + target: root + property: "opacity" + to: 1 + easing.type: Easing.OutQuad + duration: goBackDuration * 0.8 + } + + PropertyAnimation { + target: root + property: "opacity" + to: origOpacity + easing.type: Easing.OutQuad + duration: goBackDuration * 0.2 + } } - GlowText { - id: footnoteItem - text: footnote - x: 6 - y: parent.height - height - 6 - width: root.width - x * 2 - color: "#E4D5A0" - visible: footnoteVisible - wrapMode: Text.WrapAnywhere - horizontalAlignment: Text.AlignHCenter - font.family: "FZLiBian-S02" - font.pixelSize: 14 - glow.color: "black" - glow.spread: 1 - glow.radius: 1 - glow.samples: 12 + onStopped: { + if (!moveAborted) + root.moveFinished(); } + } - Rectangle { - visible: !root.selectable - anchors.fill: parent - color: Qt.rgba(0, 0, 0, 0.5) - opacity: 0.7 + function setData(data) + { + cid = data.cid; + name = data.name; + suit = data.suit; + number = data.number; + color = data.color; + } + + function toData() + { + let data = { + cid: cid, + name: name, + suit: suit, + number: number, + color: color + }; + return data; + } + + function goBack(animated) + { + if (animated) { + moveAborted = true; + goBackAnimation.stop(); + moveAborted = false; + goBackAnimation.start(); + } else { + x = origX; + y = origY; + opacity = origOpacity; } + } - 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 - - PropertyAnimation { - target: root - property: "x" - to: origX - easing.type: Easing.OutQuad - duration: goBackDuration - } - - PropertyAnimation { - target: root - property: "y" - to: origY - easing.type: Easing.OutQuad - duration: goBackDuration - } - - SequentialAnimation { - PropertyAnimation { - target: root - property: "opacity" - to: 1 - easing.type: Easing.OutQuad - duration: goBackDuration * 0.8 - } - - PropertyAnimation { - target: root - property: "opacity" - to: origOpacity - easing.type: Easing.OutQuad - duration: goBackDuration * 0.2 - } - } - - onStopped: { - if (!moveAborted) - root.moveFinished(); - } - } - - function setData(data) - { - cid = data.cid; - name = data.name; - suit = data.suit; - number = data.number; - color = data.color; - } - - function toData() - { - let data = { - cid: cid, - name: name, - suit: suit, - number: number, - color: color - }; - return data; - } - - function goBack(animated) - { - if (animated) { - moveAborted = true; - goBackAnimation.stop(); - moveAborted = false; - goBackAnimation.start(); - } else { - x = origX; - y = origY; - opacity = origOpacity; - } - } - - function destroyOnStop() - { - root.moveFinished.connect(function(){ - root.destroy(); - }); - } + function destroyOnStop() + { + root.moveFinished.connect(function(){ + root.destroy(); + }); + } } diff --git a/qml/Pages/RoomElement/ChoiceBox.qml b/qml/Pages/RoomElement/ChoiceBox.qml index ead7e80d..4242ba83 100644 --- a/qml/Pages/RoomElement/ChoiceBox.qml +++ b/qml/Pages/RoomElement/ChoiceBox.qml @@ -2,33 +2,33 @@ import QtQuick 2.15 import ".." GraphicsBox { - property var options: [] - property string skill_name: "" - property int result + property var options: [] + property string skill_name: "" + property int result - id: root - title.text: skill_name + ": Please choose" - width: Math.max(140, body.width + 20) - height: body.height + title.height + 20 + id: root + title.text: skill_name + ": Please choose" + width: Math.max(140, body.width + 20) + height: body.height + title.height + 20 - Column { - id: body - x: 10 - y: title.height + 5 - spacing: 10 + Column { + id: body + x: 10 + y: title.height + 5 + spacing: 10 - Repeater { - model: options + Repeater { + model: options - MetroButton { - text: modelData - anchors.horizontalCenter: parent.horizontalCenter + MetroButton { + text: modelData + anchors.horizontalCenter: parent.horizontalCenter - onClicked: { - result = index; - root.close(); - } - } + onClicked: { + result = index; + root.close(); } + } } + } } diff --git a/qml/Pages/RoomElement/ChooseGeneralBox.qml b/qml/Pages/RoomElement/ChooseGeneralBox.qml index dd906ad9..853df57b 100644 --- a/qml/Pages/RoomElement/ChooseGeneralBox.qml +++ b/qml/Pages/RoomElement/ChooseGeneralBox.qml @@ -3,175 +3,175 @@ 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 + property alias generalList: generalList + // property var generalList: [] + property int choiceNum: 1 + property var choices: [] + property var selectedItem: [] + property bool loaded: false - ListModel { - id: generalList + 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) + } + } } - 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(); - } - } + Item { + id: splitLine + width: parent.width - 80 + height: 6 + anchors.horizontalCenter: parent.horizontalCenter + clip: true } - Repeater { - id: generalCardList - model: generalList + Item { + width: parent.width + height: 165 - GeneralCardItem { - name: model.name - selectable: true - draggable: true + Row { + id: resultArea + anchors.centerIn: parent + spacing: 10 - 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(); - } + Repeater { + id: resultList + model: choiceNum - onReleased: { - if (!isClicked) - arrangeCards(); - } + Rectangle { + color: "#1D1E19" + radius: 3 + width: 93 + height: 130 + } } + } } - function arrangeCards() - { - let item, i; + Item { + id: buttonArea + width: parent.width + height: 40 - selectedItem = []; - for (i = 0; i < generalList.count; i++) { - item = generalCardList.itemAt(i); - if (item.y > splitLine.y) - selectedItem.push(item); + 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); + } } - - selectedItem.sort((a, b) => a.x - b.x); - - if (selectedItem.length > choiceNum) - selectedItem.splice(choiceNum, selectedItem.length - choiceNum); - + 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); } - 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); - } - } + selectedItem.sort((a, b) => a.x - b.x); - fightButton.enabled = (choices.length == choiceNum); + if (selectedItem.length > choiceNum) + selectedItem.splice(choiceNum, selectedItem.length - choiceNum); - for (i = 0; i < generalCardList.count; i++) { - item = generalCardList.itemAt(i); - if (selectedItem.indexOf(item) != -1) - continue; + updatePosition(); + } - 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); - } - } + 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/Dashboard.qml b/qml/Pages/RoomElement/Dashboard.qml index cac9f336..e70bcf7c 100644 --- a/qml/Pages/RoomElement/Dashboard.qml +++ b/qml/Pages/RoomElement/Dashboard.qml @@ -3,29 +3,166 @@ import QtQuick.Layouts 1.1 import QtGraphicalEffects 1.0 RowLayout { - id: root + id: root - property alias self: selfPhoto - property alias handcardArea: handcardAreaItem - property alias equipArea: selfPhoto.equipArea - property alias delayedTrickArea: selfPhoto.delayedTrickArea - property alias specialArea: selfPhoto.specialArea + property alias self: selfPhoto + property alias handcardArea: handcardAreaItem + property alias equipArea: selfPhoto.equipArea + property alias delayedTrickArea: selfPhoto.delayedTrickArea + property alias specialArea: selfPhoto.specialArea - Item { - width: 40 + property bool selected: selfPhoto.selected + + property bool is_pending: false + property string pending_skill: "" + property var pending_card + property var pendings: [] // int[], store cid + property int selected_card: -1 + + signal cardSelected(var card) + + Item { + width: 40 + } + + HandcardArea { + id: handcardAreaItem + Layout.fillWidth: true + Layout.preferredHeight: 130 + Layout.alignment: Qt.AlignVCenter + } + + Photo { + id: selfPhoto + handcards: handcardAreaItem.length + } + + Item { width: 5 } + + Connections { + target: handcardAreaItem + function onCardSelected(cardId, selected) { + dashboard.selectCard(cardId, selected); + } + } + + function disableAllCards() { + handcardAreaItem.enableCards([]); + } + + function unSelectAll(expectId) { + handcardAreaItem.unselectAll(expectId); + } + + function enableCards() { + // TODO: expand pile + let ids = [], cards = handcardAreaItem.cards; + for (let i = 0; i < cards.length; i++) { + if (JSON.parse(Backend.callLuaFunction("CanUseCard", [cards[i].cid, Self.id]))) + ids.push(cards[i].cid); + } + handcardAreaItem.enableCards(ids) + } + + function selectCard(cardId, selected) { + if (pending_skill !== "") { + if (selected) { + pendings.push(cardId); + } else { + pendings.splice(pendings.indexOf(cardId), 1); + } + + updatePending(); + } else { + if (selected) { + handcardAreaItem.unselectAll(cardId); + selected_card = cardId; + } else { + handcardAreaItem.unselectAll(); + selected_card = -1; + } + cardSelected(selected_card); + } + } + + function getSelectedCard() { + if (pending_skill !== "") { + return JSON.stringify({ + skill: pending_skill, + subcards: pendings + }); + } else { + return selected_card; + } + } + + function updatePending() { + if (pending_skill === "") return; + + let enabled_cards = []; + + handcardAreaItem.cards.forEach(function(card) { + if (card.selected || Router.vs_view_filter(pending_skill, pendings, card.cid)) + enabled_cards.push(card.cid); + }); + handcardAreaItem.enableCards(enabled_cards); + + let equip; + for (let i = 0; i < 5; i++) { + equip = equipAreaItem.equips.itemAt(i); + if (equip.selected || equip.cid !== -1 && + Router.vs_view_filter(pending_skill, pendings, equip.cid)) + enabled_cards.push(equip.cid); + } + equipAreaItem.enableCards(enabled_cards); + + if (Router.vs_can_view_as(pending_skill, pendings)) { + pending_card = { + skill: pending_skill, + subcards: pendings + }; + cardSelected(JSON.stringify(pending_card)); + } else { + pending_card = -1; + cardSelected(pending_card); + } + } + + function startPending(skill_name) { + pending_skill = skill_name; + pendings = []; + handcardAreaItem.unselectAll(); + + // TODO: expand pile + + // TODO: equipment + + updatePending(); + } + + function deactivateSkillButton() { + for (let i = 0; i < headSkills.length; i++) { + headSkillButtons.itemAt(i).pressed = false; + } + } + + function stopPending() { + pending_skill = ""; + pending_card = -1; + + // TODO: expand pile + + let equip; + for (let i = 0; i < 5; i++) { + equip = equipAreaItem.equips.itemAt(i); + if (equip.name !== "") { + equip.selected = false; + equip.selectable = false; + } } - HandcardArea { - id: handcardAreaItem - Layout.fillWidth: true - Layout.preferredHeight: 130 - Layout.alignment: Qt.AlignVCenter - } - - Photo { - id: selfPhoto - handcards: handcardAreaItem.length - } - - Item { width: 5 } + pendings = []; + handcardAreaItem.adjustCards(); + cardSelected(-1); + } } diff --git a/qml/Pages/RoomElement/GeneralCardItem.qml b/qml/Pages/RoomElement/GeneralCardItem.qml index b5aeb10f..a4a703a0 100644 --- a/qml/Pages/RoomElement/GeneralCardItem.qml +++ b/qml/Pages/RoomElement/GeneralCardItem.qml @@ -13,12 +13,12 @@ import "../skin-bank.js" as SkinBank */ 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] + 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/GlowText.qml b/qml/Pages/RoomElement/GlowText.qml index 243f8b3f..69b6b658 100644 --- a/qml/Pages/RoomElement/GlowText.qml +++ b/qml/Pages/RoomElement/GlowText.qml @@ -2,29 +2,29 @@ import QtQuick 2.15 import QtGraphicalEffects 1.0 Item { - property alias text: textItem.text - property alias color: textItem.color - property alias font: textItem.font - property alias fontSizeMode: textItem.fontSizeMode - property alias horizontalAlignment: textItem.horizontalAlignment - property alias verticalAlignment: textItem.verticalAlignment - property alias style: textItem.style - property alias styleColor: textItem.styleColor - property alias wrapMode: textItem.wrapMode - property alias lineHeight: textItem.lineHeight - property alias glow: glowItem + property alias text: textItem.text + property alias color: textItem.color + property alias font: textItem.font + property alias fontSizeMode: textItem.fontSizeMode + property alias horizontalAlignment: textItem.horizontalAlignment + property alias verticalAlignment: textItem.verticalAlignment + property alias style: textItem.style + property alias styleColor: textItem.styleColor + property alias wrapMode: textItem.wrapMode + property alias lineHeight: textItem.lineHeight + property alias glow: glowItem - width: textItem.implicitWidth - height: textItem.implicitHeight + width: textItem.implicitWidth + height: textItem.implicitHeight - Text { - id: textItem - anchors.fill: parent - } + Text { + id: textItem + anchors.fill: parent + } - Glow { - id: glowItem - source: textItem - anchors.fill: textItem - } + Glow { + id: glowItem + source: textItem + anchors.fill: textItem + } } diff --git a/qml/Pages/RoomElement/GraphicsBox.qml b/qml/Pages/RoomElement/GraphicsBox.qml index afb479b3..21b10102 100644 --- a/qml/Pages/RoomElement/GraphicsBox.qml +++ b/qml/Pages/RoomElement/GraphicsBox.qml @@ -2,52 +2,52 @@ import QtQuick 2.15 import QtGraphicalEffects 1.0 Item { - property alias title: titleItem - signal accepted() //Read result - signal finished() //Close the box + property alias title: titleItem + signal accepted() //Read result + signal finished() //Close the box - id: root + id: root - Rectangle { - id: background - anchors.fill: parent - color: "#B0000000" - radius: 5 - border.color: "#A6967A" - border.width: 1 - } + 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 - } + 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 - } + 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 - } + MouseArea { + anchors.fill: parent + drag.target: parent + drag.axis: Drag.XAndYAxis + } - function close() - { - accepted(); - finished(); - } + function close() + { + accepted(); + finished(); + } } diff --git a/qml/Pages/RoomElement/HandcardArea.qml b/qml/Pages/RoomElement/HandcardArea.qml index 839bd81e..2c6815ee 100644 --- a/qml/Pages/RoomElement/HandcardArea.qml +++ b/qml/Pages/RoomElement/HandcardArea.qml @@ -2,130 +2,130 @@ import QtQuick 2.15 import "../../util.js" as Utility Item { - property alias cards: cardArea.cards - property alias length: cardArea.length - property var selectedCards: [] + property alias cards: cardArea.cards + property alias length: cardArea.length + property var selectedCards: [] - signal cardSelected(int cardId, bool selected) + signal cardSelected(int cardId, bool selected) - id: area + id: area - CardArea { - anchors.fill: parent - id: cardArea - onLengthChanged: area.updateCardPosition(true); + CardArea { + anchors.fill: parent + id: cardArea + onLengthChanged: area.updateCardPosition(true); + } + + function add(inputs) + { + cardArea.add(inputs); + if (inputs instanceof Array) { + for (let i = 0; i < inputs.length; i++) + filterInputCard(inputs[i]); + } else { + filterInputCard(inputs); + } + } + + function filterInputCard(card) + { + card.autoBack = true; + card.draggable = true; + card.selectable = false; + card.clicked.connect(adjustCards); + } + + function remove(outputs) + { + let result = cardArea.remove(outputs); + let card; + for (let i = 0; i < result.length; i++) { + card = result[i]; + card.draggable = false; + card.selectable = false; + card.selectedChanged.disconnect(adjustCards); + } + return result; + } + + function enableCards(cardIds) + { + let card, i; + for (i = 0; i < cards.length; i++) { + card = cards[i]; + card.selectable = cardIds.contains(card.cid); + if (!card.selectable) { + card.selected = false; + unselectCard(card); + } + } + } + + function updateCardPosition(animated) + { + cardArea.updateCardPosition(false); + + let i, card; + for (i = 0; i < cards.length; i++) { + card = cards[i]; + if (card.selected) + card.origY -= 20; } - function add(inputs) - { - cardArea.add(inputs); - if (inputs instanceof Array) { - for (let i = 0; i < inputs.length; i++) - filterInputCard(inputs[i]); - } else { - filterInputCard(inputs); - } + if (animated) { + for (i = 0; i < cards.length; i++) + cards[i].goBack(true) } + } - function filterInputCard(card) - { - card.autoBack = true; - card.draggable = true; - card.selectable = false; - card.clicked.connect(adjustCards); + function adjustCards() + { + area.updateCardPosition(true); + + for (let i = 0; i < cards.length; i++) { + let card = cards[i]; + if (card.selected) { + if (!selectedCards.contains(card)) + selectCard(card); + } else { + if (selectedCards.contains(card)) + unselectCard(card); + } } + } - function remove(outputs) - { - let result = cardArea.remove(outputs); - let card; - for (let i = 0; i < result.length; i++) { - card = result[i]; - card.draggable = false; - card.selectable = false; - card.selectedChanged.disconnect(adjustCards); - } - return result; + function selectCard(card) + { + selectedCards.push(card); + cardSelected(card.cid, true); + } + + function unselectCard(card) + { + for (let i = 0; i < selectedCards.length; i++) { + if (selectedCards[i] === card) { + selectedCards.splice(i, 1); + cardSelected(card.cid, false); + break; + } } + } - function enableCards(cardIds) - { - let card, i; - for (i = 0; i < cards.length; i++) { - card = cards[i]; - card.selectable = cardIds.contains(card.cid); - if (!card.selectable) { - card.selected = false; - unselectCard(card); - } - } + function unselectAll(exceptId) { + let card = undefined; + for (let i = 0; i < selectedCards.length; i++) { + if (selectedCards[i].cid !== exceptId) { + selectedCards[i].selected = false; + } else { + card = selectedCards[i]; + card.selected = true; + } } - - function updateCardPosition(animated) - { - cardArea.updateCardPosition(false); - - let i, card; - for (i = 0; i < cards.length; i++) { - card = cards[i]; - if (card.selected) - card.origY -= 20; - } - - if (animated) { - for (i = 0; i < cards.length; i++) - cards[i].goBack(true) - } - } - - function adjustCards() - { - area.updateCardPosition(true); - - for (let i = 0; i < cards.length; i++) { - let card = cards[i]; - if (card.selected) { - if (!selectedCards.contains(card)) - selectCard(card); - } else { - if (selectedCards.contains(card)) - unselectCard(card); - } - } - } - - function selectCard(card) - { - selectedCards.push(card); - cardSelected(card.cid, true); - } - - function unselectCard(card) - { - for (let i = 0; i < selectedCards.length; i++) { - if (selectedCards[i] === card) { - selectedCards.splice(i, 1); - cardSelected(card.cid, false); - break; - } - } - } - - function unselectAll(exceptId) { - let card = undefined; - for (let i = 0; i < selectedCards.length; i++) { - if (selectedCards[i].cid !== exceptId) { - selectedCards[i].selected = false; - } else { - card = selectedCards[i]; - card.selected = true; - } - } - if (card === undefined) { - selectedCards = []; - } else { - selectedCards = [card]; - } - updateCardPosition(true); + if (card === undefined) { + selectedCards = []; + } else { + selectedCards = [card]; } + updateCardPosition(true); + } } diff --git a/qml/Pages/RoomElement/IndicatorLine.qml b/qml/Pages/RoomElement/IndicatorLine.qml index 9c4ad5dc..d0d92f9c 100644 --- a/qml/Pages/RoomElement/IndicatorLine.qml +++ b/qml/Pages/RoomElement/IndicatorLine.qml @@ -1,101 +1,101 @@ import QtQuick 2.15 Item { - property point start: Qt.point(0, 0) - property var end: [] - property alias running: pointToAnimation.running - property color color: "#96943D" - property real ratio: 0 - property int lineWidth: 6 + property point start: Qt.point(0, 0) + property var end: [] + property alias running: pointToAnimation.running + property color color: "#96943D" + property real ratio: 0 + property int lineWidth: 6 - signal finished() + signal finished() - id: root - anchors.fill: parent + id: root + anchors.fill: parent - Repeater { - model: end + Repeater { + model: end - Rectangle { - width: 6 - height: Math.sqrt(Math.pow(modelData.x - start.x, 2) + Math.pow(modelData.y - start.y, 2)) * ratio - x: start.x - y: start.y - antialiasing: true + Rectangle { + width: 6 + height: Math.sqrt(Math.pow(modelData.x - start.x, 2) + Math.pow(modelData.y - start.y, 2)) * ratio + x: start.x + y: start.y + antialiasing: true - gradient: Gradient { - GradientStop { - position: 0 - color: Qt.rgba(255, 255, 255, 0) - } - GradientStop { - position: 1 - color: Qt.rgba(200, 200, 200, 0.12) - } - } - - Rectangle { - anchors.horizontalCenter: parent.horizontalCenter - width: 3 - height: parent.height - antialiasing: true - - gradient: Gradient { - GradientStop { - position: 0 - color: Qt.rgba(255, 255, 255, 0) - } - GradientStop { - position: 1 - color: Qt.lighter(root.color) - } - } - } - - transform: Rotation { - angle: 0 - - Component.onCompleted: { - var dx = modelData.x - start.x; - var dy = modelData.y - start.y; - if (dx > 0) { - angle = Math.atan2(dy, dx) / Math.PI * 180 - 90; - } else if (dx < 0) { - angle = Math.atan2(dy, dx) / Math.PI * 180 + 270; - } else if (dy < 0) { - angle = 180; - } - } - } + gradient: Gradient { + GradientStop { + position: 0 + color: Qt.rgba(255, 255, 255, 0) } + GradientStop { + position: 1 + color: Qt.rgba(200, 200, 200, 0.12) + } + } + + Rectangle { + anchors.horizontalCenter: parent.horizontalCenter + width: 3 + height: parent.height + antialiasing: true + + gradient: Gradient { + GradientStop { + position: 0 + color: Qt.rgba(255, 255, 255, 0) + } + GradientStop { + position: 1 + color: Qt.lighter(root.color) + } + } + } + + transform: Rotation { + angle: 0 + + Component.onCompleted: { + var dx = modelData.x - start.x; + var dy = modelData.y - start.y; + if (dx > 0) { + angle = Math.atan2(dy, dx) / Math.PI * 180 - 90; + } else if (dx < 0) { + angle = Math.atan2(dy, dx) / Math.PI * 180 + 270; + } else if (dy < 0) { + angle = 180; + } + } + } + } + } + + SequentialAnimation { + id: pointToAnimation + + PropertyAnimation { + target: root + property: "ratio" + to: 1 + easing.type: Easing.OutCubic + duration: 200 } - SequentialAnimation { - id: pointToAnimation - - PropertyAnimation { - target: root - property: "ratio" - to: 1 - easing.type: Easing.OutCubic - duration: 200 - } - - PauseAnimation { - duration: 200 - } - - PropertyAnimation { - target: root - property: "opacity" - to: 0 - easing.type: Easing.InQuart - duration: 300 - } - - onStopped: { - root.visible = false; - root.finished(); - } + PauseAnimation { + duration: 200 } + + PropertyAnimation { + target: root + property: "opacity" + to: 0 + easing.type: Easing.InQuart + duration: 300 + } + + onStopped: { + root.visible = false; + root.finished(); + } + } } diff --git a/qml/Pages/RoomElement/InvisibleCardArea.qml b/qml/Pages/RoomElement/InvisibleCardArea.qml index 67561bb2..10e7cc1d 100644 --- a/qml/Pages/RoomElement/InvisibleCardArea.qml +++ b/qml/Pages/RoomElement/InvisibleCardArea.qml @@ -1,111 +1,111 @@ import QtQuick 2.15 Item { - property var cards: [] - property int length: 0 - property var pendingInput: [] - property bool checkExisting: false + property var cards: [] + property int length: 0 + property var pendingInput: [] + property bool checkExisting: false - id: root + id: root - function add(inputs) + function add(inputs) + { + let card; + if (inputs instanceof Array) { + for (let i = 0; i < inputs.length; i++) { + card = inputs[i]; + pendingInput.push(card); + cards.push(card.toData()); + } + + if (checkExisting) + length = cards.length; + else + length += inputs.length; + } else { + pendingInput.push(inputs); + cards.push(inputs.toData()); + + if (checkExisting) + length = cards.length; + else + length++; + } + } + + function _contains(cid) + { + if (!checkExisting) + return true; + + for (let i = 0; i < cards.length; i++) { - let card; - if (inputs instanceof Array) { - for (let i = 0; i < inputs.length; i++) { - card = inputs[i]; - pendingInput.push(card); - cards.push(card.toData()); - } + if (cards[i].cid === cid) + return true; + } + return false; + } - if (checkExisting) - length = cards.length; - else - length += inputs.length; - } else { - pendingInput.push(inputs); - cards.push(inputs.toData()); + function remove(outputs) + { + let component = Qt.createComponent("CardItem.qml"); + if (component.status !== Component.Ready) + return []; - if (checkExisting) - length = cards.length; - else - length++; + 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(Backend.callLuaFunction("GetCardData", [outputs[i]])) + state.x = parentPos.x; + state.y = parentPos.y; + state.opacity = 0; + card = component.createObject(roomScene, state); + card.x -= card.width / 2; + card.x += (i - outputs.length / 2) * 15; + card.y -= card.height / 2; + items.push(card); + if (checkExisting) { + //@to-do: remove it from cards + cards.splice(i, 1); + i--; } + } + } + if (checkExisting) + length = cards.length; + else + length -= outputs.length; + return items; + } + + function updateCardPosition(animated) + { + let i, card; + + if (animated) { + 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); + card.origY = parentPos.y - card.height / 2; + card.origOpacity = 0; + card.destroyOnStop(); + } + + for (i = 0; i < pendingInput.length; i++) + pendingInput[i].goBack(true); + } else { + for (i = 0; i < pendingInput.length; i++) { + card = pendingInput[i]; + card.x = parentPos.x - card.width / 2; + card.y = parentPos.y - card.height / 2; + card.opacity = 1; + card.destroy(); + } } - function _contains(cid) - { - if (!checkExisting) - return true; - - for (let i = 0; i < cards.length; i++) - { - if (cards[i].cid === cid) - return true; - } - return false; - } - - function remove(outputs) - { - let component = Qt.createComponent("CardItem.qml"); - if (component.status !== Component.Ready) - return []; - - 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(Backend.getCardData(outputs[i])) - state.x = parentPos.x; - state.y = parentPos.y; - state.opacity = 0; - card = component.createObject(roomScene, state); - card.x -= card.width / 2; - card.x += (i - outputs.length / 2) * 15; - card.y -= card.height / 2; - items.push(card); - if (checkExisting) { - //@to-do: remove it from cards - cards.splice(i, 1); - i--; - } - } - } - if (checkExisting) - length = cards.length; - else - length -= outputs.length; - return items; - } - - function updateCardPosition(animated) - { - let i, card; - - if (animated) { - 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); - card.origY = parentPos.y - card.height / 2; - card.origOpacity = 0; - card.destroyOnStop(); - } - - for (i = 0; i < pendingInput.length; i++) - pendingInput[i].goBack(true); - } else { - for (i = 0; i < pendingInput.length; i++) { - card = pendingInput[i]; - card.x = parentPos.x - card.width / 2; - card.y = parentPos.y - card.height / 2; - card.opacity = 1; - card.destroy(); - } - } - - pendingInput = []; - } + pendingInput = []; + } } diff --git a/qml/Pages/RoomElement/Photo.qml b/qml/Pages/RoomElement/Photo.qml index 7214dd3d..5b8647dc 100644 --- a/qml/Pages/RoomElement/Photo.qml +++ b/qml/Pages/RoomElement/Photo.qml @@ -5,309 +5,377 @@ import "PhotoElement" import "../skin-bank.js" as SkinBank Item { - id: root - width: 175 - height: 233 - scale: 0.8 - property string general: "" - property string screenName: "" - property string role: "unknown" - property string kingdom: "qun" - property string netstate: "online" - property alias handcards: handcardAreaItem.length - property int maxHp: 0 - property int hp: 0 - property int seatNumber: 1 - property bool isDead: false - property bool dying: false - property bool faceup: true - property bool chained: false - property bool drank: false - property bool isOwner: false - property string status: "normal" + id: root + width: 175 + height: 233 + scale: 0.8 + property int playerid + property string general: "" + property string screenName: "" + property string role: "unknown" + property string kingdom: "qun" + property string netstate: "online" + property alias handcards: handcardAreaItem.length + property int maxHp: 0 + property int hp: 0 + property int seatNumber: 1 + property bool isDead: false + property bool dying: false + property bool faceup: true + property bool chained: false + property bool drank: false + property bool isOwner: false + property string status: "normal" - property alias handcardArea: handcardAreaItem - property alias equipArea: equipAreaItem - property alias delayedTrickArea: delayedTrickAreaItem - property alias specialArea: handcardAreaItem + property alias handcardArea: handcardAreaItem + property alias equipArea: equipAreaItem + property alias delayedTrickArea: delayedTrickAreaItem + property alias specialArea: handcardAreaItem - property alias progressBar: progressBar - property alias progressTip: progressTip.text + property alias progressBar: progressBar + property alias progressTip: progressTip.text - property bool selectable: false - property bool selected: false + property bool selectable: false + property bool selected: false - Behavior on x { - NumberAnimation { duration: 600; easing.type: Easing.InOutQuad } + Behavior on x { + NumberAnimation { duration: 600; easing.type: Easing.InOutQuad } + } + + Behavior on y { + NumberAnimation { duration: 600; easing.type: Easing.InOutQuad } + } + + states: [ + State { name: "normal" }, + State { name: "candidate" }, + State { name: "playing" } + //State { name: "responding" }, + //State { name: "sos" } + ] + + state: "normal" + transitions: [ + Transition { + from: "*"; to: "normal" + ScriptAction { + script: { + animPlaying.stop(); + animSelectable.stop(); + animSelected.stop(); + } + } + }, + + Transition { + from: "*"; to: "playing" + ScriptAction { + script: { + animPlaying.start(); + } + } + }, + + Transition { + from: "*"; to: "candidate" + ScriptAction { + script: { + animSelectable.start(); + animSelected.start(); + } + } } + ] - Behavior on y { - NumberAnimation { duration: 600; easing.type: Easing.InOutQuad } - } + PixmapAnimation { + id: animPlaying + source: "playing" + anchors.centerIn: parent + loop: true + scale: 1.1 + visible: root.state === "playing" + } - PixmapAnimation { - id: animFrame - source: "selected" - anchors.centerIn: parent - loop: true - scale: 1.1 - } + PixmapAnimation { + id: animSelected + source: "selected" + anchors.centerIn: parent + loop: true + scale: 1.1 + visible: root.state === "candidate" && selected + } - Image { - id: back - source: SkinBank.PHOTO_BACK_DIR + root.kingdom - } + Image { + id: back + source: SkinBank.PHOTO_BACK_DIR + root.kingdom + } + + Text { + id: generalName + x: 5 + y: 28 + font.family: "FZLiBian-S02" + font.pixelSize: 22 + opacity: 0.7 + horizontalAlignment: Text.AlignHCenter + lineHeight: 18 + lineHeightMode: Text.FixedHeight + color: "white" + width: 24 + wrapMode: Text.WordWrap + text: "" + } + + HpBar { + id: hp + x: 8 + value: root.hp + maxValue: root.maxHp + anchors.bottom: parent.bottom + anchors.bottomMargin: 36 + } + + Image { + id: generalImage + width: 138 + height: 222 + smooth: true + visible: false + fillMode: Image.PreserveAspectCrop + source: (general != "") ? SkinBank.GENERAL_DIR + general : "" + } + + Rectangle { + id: photoMask + x: 31 + y: 5 + width: 138 + height: 222 + radius: 8 + visible: false + } + + OpacityMask { + anchors.fill: photoMask + source: generalImage + maskSource: photoMask + } + + Colorize { + anchors.fill: photoMask + source: generalImage + saturation: 0 + 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 { + visible: equipAreaItem.length > 0 + source: SkinBank.PHOTO_DIR + "equipbg" + x: 31 + y: 121 + } + + Image { + source: root.status != "normal" ? SkinBank.STATUS_DIR + root.status : "" + x: -6 + } + + Image { + id: turnedOver + visible: !root.faceup + source: SkinBank.PHOTO_DIR + "faceturned" + x: 29; y: 5 + } + + EquipArea { + id: equipAreaItem + + x: 31 + y: 139 + } + + Image { + id: chain + visible: root.chained + source: SkinBank.PHOTO_DIR + "chain" + anchors.horizontalCenter: parent.horizontalCenter + y: 72 + } + + Image { + // id: saveme + visible: root.isDead || root.dying + source: SkinBank.DEATH_DIR + (root.isDead ? root.role : "saveme") + anchors.centerIn: photoMask + } + + Image { + id: netstat + source: SkinBank.STATE_DIR + root.netstate + x: photoMask.x + y: photoMask.y + } + + Image { + id: handcardNum + source: SkinBank.PHOTO_DIR + "handcard" + anchors.bottom: parent.bottom + anchors.bottomMargin: -6 + x: -6 Text { - id: generalName - x: 5 - y: 28 - font.family: "FZLiBian-S02" - font.pixelSize: 22 - opacity: 0.7 - horizontalAlignment: Text.AlignHCenter - lineHeight: 18 - lineHeightMode: Text.FixedHeight - color: "white" - width: 24 - wrapMode: Text.WordWrap - text: "" + text: root.handcards + font.family: "FZLiBian-S02" + font.pixelSize: 32 + //font.weight: 30 + color: "white" + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: 4 + style: Text.Outline } + } - HpBar { - id: hp - x: 8 - value: root.hp - maxValue: root.maxHp - anchors.bottom: parent.bottom - anchors.bottomMargin: 36 + MouseArea { + anchors.fill: parent + onClicked: { + if (parent.state != "candidate" || !parent.selectable) + return; + parent.selected = !parent.selected; } + } - Image { - id: generalImage - width: 138 - height: 222 - smooth: true - visible: false - fillMode: Image.PreserveAspectCrop - source: (general != "") ? SkinBank.GENERAL_DIR + general : "" + RoleComboBox { + id: role + value: root.role + anchors.top: parent.top + anchors.topMargin: -4 + anchors.right: parent.right + anchors.rightMargin: -4 + } + + Image { + visible: root.state === "candidate" && !selectable && !selected + source: SkinBank.PHOTO_DIR + "disable" + x: 31; y: -21 + } + + GlowText { + id: seatNum + visible: !progressBar.visible + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: -32 + property var seatChr: ["一", "二", "三", "四", "五", "六", "七", "八"] + font.family: "FZLiShu II-S06S" + font.pixelSize: 32 + text: seatChr[seatNumber - 1] + + glow.color: "brown" + glow.spread: 0.2 + glow.radius: 8 + glow.samples: 12 + } + + SequentialAnimation { + id: trembleAnimation + running: false + PropertyAnimation { + target: root + property: "x" + to: root.x - 20 + easing.type: Easing.InQuad + duration: 100 } - - Rectangle { - id: photoMask - x: 31 - y: 5 - width: 138 - height: 222 - radius: 8 - visible: false + PropertyAnimation { + target: root + property: "x" + to: root.x + easing.type: Easing.OutQuad + duration: 100 } + } + + function tremble() { + trembleAnimation.start() + } - OpacityMask { - anchors.fill: photoMask - source: generalImage - maskSource: photoMask + 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 = ""; + } } + } - Colorize { - anchors.fill: photoMask - source: generalImage - saturation: 0 - visible: root.isDead + 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: "" } + } - 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 - } + PixmapAnimation { + id: animSelectable + source: "selectable" + anchors.centerIn: parent + loop: true + visible: root.state === "candidate" && selectable + } - Image { - visible: equipAreaItem.length > 0 - source: SkinBank.PHOTO_DIR + "equipbg" - x: 31 - y: 121 - } + InvisibleCardArea { + id: handcardAreaItem + anchors.centerIn: parent + } - Image { - source: root.status != "normal" ? SkinBank.STATUS_DIR + root.status : "" - x: -6 - } + DelayedTrickArea { + id: delayedTrickAreaItem + rows: 1 + anchors.bottom: parent.bottom + anchors.bottomMargin: 8 + } - Image { - id: turnedOver - visible: !root.faceup - source: SkinBank.PHOTO_DIR + "faceturned" - x: 29; y: 5 - } + InvisibleCardArea { + id: defaultArea + anchors.centerIn: parent + } - EquipArea { - id: equipAreaItem - - x: 31 - y: 139 - } - - Image { - id: chain - visible: root.chained - source: SkinBank.PHOTO_DIR + "chain" - anchors.horizontalCenter: parent.horizontalCenter - y: 72 - } - - Image { - // id: saveme - visible: root.isDead || root.dying - source: SkinBank.DEATH_DIR + (root.isDead ? root.role : "saveme") - anchors.centerIn: photoMask - } - - Image { - id: netstat - source: SkinBank.STATE_DIR + root.netstate - x: photoMask.x - y: photoMask.y - } - - Image { - id: handcardNum - source: SkinBank.PHOTO_DIR + "handcard" - anchors.bottom: parent.bottom - anchors.bottomMargin: -6 - x: -6 - - Text { - text: root.handcards - font.family: "FZLiBian-S02" - font.pixelSize: 32 - //font.weight: 30 - color: "white" - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - anchors.bottomMargin: 4 - style: Text.Outline - } - } - - RoleComboBox { - id: role - value: root.role - anchors.top: parent.top - anchors.topMargin: -4 - anchors.right: parent.right - anchors.rightMargin: -4 - } - - GlowText { - id: seatNum - visible: !progressBar.visible - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - anchors.bottomMargin: -32 - property var seatChr: ["一", "二", "三", "四", "五", "六", "七", "八"] - font.family: "FZLiShu II-S06S" - font.pixelSize: 32 - text: seatChr[seatNumber - 1] - - glow.color: "brown" - glow.spread: 0.2 - glow.radius: 8 - glow.samples: 12 - } - - SequentialAnimation { - id: trembleAnimation - running: false - PropertyAnimation { - target: root - property: "x" - to: root.x - 20 - easing.type: Easing.InQuad - duration: 100 - } - PropertyAnimation { - target: root - property: "x" - to: root.x - easing.type: Easing.OutQuad - duration: 100 - } - } - - 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: "" - } - } - - PixmapAnimation { - id: animSelectable - source: "selectable" - anchors.centerIn: parent - loop: true - } - - InvisibleCardArea { - id: handcardAreaItem - anchors.centerIn: parent - } - - DelayedTrickArea { - id: delayedTrickAreaItem - rows: 1 - anchors.bottom: parent.bottom - anchors.bottomMargin: 8 - } - - InvisibleCardArea { - id: defaultArea - anchors.centerIn: parent - } - - onGeneralChanged: { - if (!roomScene.isStarted) return; - generalName.text = Backend.translate(general); - let data = JSON.parse(Backend.getGeneralData(general)); - kingdom = data.kingdom; - } + onGeneralChanged: { + if (!roomScene.isStarted) return; + generalName.text = Backend.translate(general); + let data = JSON.parse(Backend.callLuaFunction("GetGeneralData", [general])); + kingdom = data.kingdom; + } } diff --git a/qml/Pages/RoomElement/PhotoElement/DelayedTrickArea.qml b/qml/Pages/RoomElement/PhotoElement/DelayedTrickArea.qml index 87070c93..c9eff3d7 100644 --- a/qml/Pages/RoomElement/PhotoElement/DelayedTrickArea.qml +++ b/qml/Pages/RoomElement/PhotoElement/DelayedTrickArea.qml @@ -4,62 +4,62 @@ import ".." import "../../skin-bank.js" as SkinBank Item { - property alias rows: grid.rows - property alias columns: grid.columns + property alias rows: grid.rows + property alias columns: grid.columns - InvisibleCardArea { - id: area - checkExisting: true + InvisibleCardArea { + id: area + checkExisting: true + } + + ListModel { + id: cards + } + + Grid { + id: grid + anchors.fill: parent + rows: 100 + columns: 100 + + Repeater { + model: cards + + Image { + source: SkinBank.DELAYED_TRICK_DIR + name + } } + } - ListModel { - id: cards + function add(inputs) + { + area.add(inputs); + if (inputs instanceof Array) { + cards.append(...inputs); + } else { + cards.append(inputs); } + } - Grid { - id: grid - anchors.fill: parent - rows: 100 - columns: 100 - - Repeater { - model: cards - - Image { - source: SkinBank.DELAYED_TRICK_DIR + name - } + function remove(outputs) + { + let result = area.remove(outputs); + for (let i = 0; i < result.length; i++) { + let item = result[i]; + for (let j = 0; j < cards.count; j++) { + let icon = cards.get(j); + if (icon.cid === item.cid) { + cards.remove(j, 1); + break; } + } } - function add(inputs) - { - area.add(inputs); - if (inputs instanceof Array) { - cards.append(...inputs); - } else { - cards.append(inputs); - } - } + return result; + } - function remove(outputs) - { - let result = area.remove(outputs); - for (let i = 0; i < result.length; i++) { - let item = result[i]; - for (let j = 0; j < cards.count; j++) { - let icon = cards.get(j); - if (icon.cid === item.cid) { - cards.remove(j, 1); - break; - } - } - } - - return result; - } - - function updateCardPosition(animated) - { - area.updateCardPosition(animated); - } + function updateCardPosition(animated) + { + area.updateCardPosition(animated); + } } diff --git a/qml/Pages/RoomElement/PhotoElement/EquipArea.qml b/qml/Pages/RoomElement/PhotoElement/EquipArea.qml index c3be0ba6..77a9cfbe 100644 --- a/qml/Pages/RoomElement/PhotoElement/EquipArea.qml +++ b/qml/Pages/RoomElement/PhotoElement/EquipArea.qml @@ -11,110 +11,110 @@ import "../../skin-bank.js" as SkinBank */ Column { - height: 88 - width: 138 - property int itemHeight: Math.floor(height / 4) - property var items: [treasureItem, weaponItem, armorItem, defensiveHorseItem, offensiveHorseItem] - property var subtypes: ["treasure", "weapon", "armor", "defensive_horse", "offensive_horse"] - property int length: area.length + height: 88 + width: 138 + property int itemHeight: Math.floor(height / 4) + property var items: [treasureItem, weaponItem, armorItem, defensiveHorseItem, offensiveHorseItem] + property var subtypes: ["treasure", "weapon", "armor", "defensive_horse", "offensive_horse"] + property int length: area.length - InvisibleCardArea { - id: area - checkExisting: true - } + InvisibleCardArea { + id: area + checkExisting: true + } - EquipItem { - id: treasureItem + EquipItem { + id: treasureItem + width: parent.width + height: itemHeight + opacity: 0 + } + + EquipItem { + id: weaponItem + width: parent.width + height: itemHeight + opacity: 0 + } + + EquipItem { + id: armorItem + width: parent.width + height: itemHeight + opacity: 0 + } + + Row { + width: parent.width + height: itemHeight + + Item { + width: Math.ceil(parent.width / 2) + height: itemHeight + + EquipItem { + id: defensiveHorseItem width: parent.width height: itemHeight + icon: "horse" opacity: 0 + } } - EquipItem { - id: weaponItem + Item { + width: Math.floor(parent.width / 2) + height: itemHeight + + EquipItem { + id: offensiveHorseItem width: parent.width height: itemHeight + icon: "horse" opacity: 0 + } } + } - EquipItem { - id: armorItem - width: parent.width - height: itemHeight - opacity: 0 + function add(inputs) + { + area.add(inputs); + + let card, item; + if (inputs instanceof Array) { + for (let i = 0; i < inputs.length; i++) { + card = inputs[i]; + item = items[subtypes.indexOf(card.subtype)]; + item.setCard(card); + item.show(); + } + } else { + card = inputs; + item = items[subtypes.indexOf(card.subtype)]; + item.setCard(card); + item.show(); } + } - Row { - width: parent.width - height: itemHeight - - Item { - width: Math.ceil(parent.width / 2) - height: itemHeight - - EquipItem { - id: defensiveHorseItem - width: parent.width - height: itemHeight - icon: "horse" - opacity: 0 - } - } - - Item { - width: Math.floor(parent.width / 2) - height: itemHeight - - EquipItem { - id: offensiveHorseItem - width: parent.width - height: itemHeight - icon: "horse" - opacity: 0 - } + function remove(outputs) + { + let result = area.remove(outputs); + for (let i = 0; i < result.length; i++) { + let card = result[i]; + for (let j = 0; j < items.length; j++) { + let item = items[j]; + if (item.cid === card.cid) { + item.reset(); + item.hide(); } + } } - function add(inputs) - { - area.add(inputs); + return result; + } - let card, item; - if (inputs instanceof Array) { - for (let i = 0; i < inputs.length; i++) { - card = inputs[i]; - item = items[subtypes.indexOf(card.subtype)]; - item.setCard(card); - item.show(); - } - } else { - card = inputs; - item = items[subtypes.indexOf(card.subtype)]; - item.setCard(card); - item.show(); - } - } - - function remove(outputs) - { - let result = area.remove(outputs); - for (let i = 0; i < result.length; i++) { - let card = result[i]; - for (let j = 0; j < items.length; j++) { - let item = items[j]; - if (item.cid === card.cid) { - item.reset(); - item.hide(); - } - } - } - - return result; - } - - function updateCardPosition(animated) - { - area.updateCardPosition(animated); - } + function updateCardPosition(animated) + { + area.updateCardPosition(animated); + } } diff --git a/qml/Pages/RoomElement/PhotoElement/EquipItem.qml b/qml/Pages/RoomElement/PhotoElement/EquipItem.qml index e4bbb0cc..1ce32762 100644 --- a/qml/Pages/RoomElement/PhotoElement/EquipItem.qml +++ b/qml/Pages/RoomElement/PhotoElement/EquipItem.qml @@ -4,139 +4,139 @@ import "../../../util.js" as Utility import "../../skin-bank.js" as SkinBank Item { - property int cid: 0 - property string name: "" - property string suit: "" - property int number: 0 + property int cid: 0 + property string name: "" + property string suit: "" + property int number: 0 - property string icon: "" - property alias text: textItem.text + property string icon: "" + property alias text: textItem.text - id: root + id: root - Image { - id: iconItem - anchors.verticalCenter: parent.verticalCenter - x: 3 + Image { + id: iconItem + anchors.verticalCenter: parent.verticalCenter + x: 3 - source: icon ? SkinBank.EQUIP_ICON_DIR + icon : "" + source: icon ? SkinBank.EQUIP_ICON_DIR + icon : "" + } + + Image { + id: suitItem + anchors.right: parent.right + source: suit ? SkinBank.CARD_SUIT_DIR + suit : "" + width: implicitWidth / implicitHeight * height + height: 16 + } + + GlowText { + id: numberItem + visible: number > 0 && number < 14 + text: Utility.convertNumber(number) + color: "white" + font.family: "FZLiBian-S02" + font.pixelSize: 16 + glow.color: "black" + glow.spread: 0.75 + glow.radius: 2 + glow.samples: 4 + x: parent.width - 24 + y: 1 + } + + GlowText { + id: textItem + font.family: "FZLiBian-S02" + color: "white" + font.pixelSize: 18 + glow.color: "black" + glow.spread: 0.9 + glow.radius: 2 + glow.samples: 6 + anchors.left: iconItem.right + anchors.leftMargin: -8 + verticalAlignment: Text.AlignVCenter + } + + ParallelAnimation { + id: showAnime + + NumberAnimation { + target: root + property: "x" + duration: 200 + easing.type: Easing.InOutQuad + from: 10 + to: 0 } - Image { - id: suitItem - anchors.right: parent.right - source: suit ? SkinBank.CARD_SUIT_DIR + suit : "" - width: implicitWidth / implicitHeight * height - height: 16 + NumberAnimation { + target: root + property: "opacity" + duration: 200 + easing.type: Easing.InOutQuad + from: 0 + to: 1 + } + } + + ParallelAnimation { + id: hideAnime + + NumberAnimation { + target: root + property: "x" + duration: 200 + easing.type: Easing.InOutQuad + from: 0 + to: 10 } - GlowText { - id: numberItem - visible: number > 0 && number < 14 - text: Utility.convertNumber(number) - color: "white" - font.family: "FZLiBian-S02" - font.pixelSize: 16 - glow.color: "black" - glow.spread: 0.75 - glow.radius: 2 - glow.samples: 4 - x: parent.width - 24 - y: 1 + NumberAnimation { + target: root + property: "opacity" + duration: 200 + easing.type: Easing.InOutQuad + from: 1 + to: 0 } + } - GlowText { - id: textItem - font.family: "FZLiBian-S02" - color: "white" - font.pixelSize: 18 - glow.color: "black" - glow.spread: 0.9 - glow.radius: 2 - glow.samples: 6 - anchors.left: iconItem.right - anchors.leftMargin: -8 - verticalAlignment: Text.AlignVCenter + function reset() + { + cid = 0; + name = ""; + suit = ""; + number = 0; + text = ""; + } + + function setCard(card) + { + cid = card.cid; + name = card.name; + suit = card.suit; + number = card.number; + if (card.subtype === "defensive_horse") { + text = "+1"; + icon = "horse"; + } else if (card.subtype === "offensive_horse") { + text = "-1" + icon = "horse"; + } else { + text = Backend.translate(name); + icon = name; } + } - ParallelAnimation { - id: showAnime + function show() + { + showAnime.start(); + } - NumberAnimation { - target: root - property: "x" - duration: 200 - easing.type: Easing.InOutQuad - from: 10 - to: 0 - } - - NumberAnimation { - target: root - property: "opacity" - duration: 200 - easing.type: Easing.InOutQuad - from: 0 - to: 1 - } - } - - ParallelAnimation { - id: hideAnime - - NumberAnimation { - target: root - property: "x" - duration: 200 - easing.type: Easing.InOutQuad - from: 0 - to: 10 - } - - NumberAnimation { - target: root - property: "opacity" - duration: 200 - easing.type: Easing.InOutQuad - from: 1 - to: 0 - } - } - - function reset() - { - cid = 0; - name = ""; - suit = ""; - number = 0; - text = ""; - } - - function setCard(card) - { - cid = card.cid; - name = card.name; - suit = card.suit; - number = card.number; - if (card.subtype === "defensive_horse") { - text = "+1"; - icon = "horse"; - } else if (card.subtype === "offensive_horse") { - text = "-1" - icon = "horse"; - } else { - text = name; - icon = name; - } - } - - function show() - { - showAnime.start(); - } - - function hide() - { - hideAnime.start(); - } + function hide() + { + hideAnime.start(); + } } diff --git a/qml/Pages/RoomElement/PhotoElement/HpBar.qml b/qml/Pages/RoomElement/PhotoElement/HpBar.qml index 470a0e49..6e0a0d1c 100644 --- a/qml/Pages/RoomElement/PhotoElement/HpBar.qml +++ b/qml/Pages/RoomElement/PhotoElement/HpBar.qml @@ -2,71 +2,71 @@ import QtQuick 2.15 import ".." Column { - id: root - property int maxValue: 4 - property int value: 4 - property var colors: ["#F4180E", "#F4180E", "#E3B006", "#25EC27"] + id: root + property int maxValue: 4 + property int value: 4 + property var colors: ["#F4180E", "#F4180E", "#E3B006", "#25EC27"] - Repeater { - id: repeater - model: maxValue <= 4 ? maxValue : 0 - Magatama { - state: (maxValue - 1 - index) >= value ? 0 : (value >= 3 || value >= maxValue ? 3 : (value <= 0 ? 0 : value)) - } + Repeater { + id: repeater + model: maxValue <= 4 ? maxValue : 0 + Magatama { + state: (maxValue - 1 - index) >= value ? 0 : (value >= 3 || value >= maxValue ? 3 : (value <= 0 ? 0 : value)) + } + } + + Column { + visible: maxValue > 4 + spacing: -4 + + Magatama { + state: (value >= 3 || value >= maxValue) ? 3 : (value <= 0 ? 0 : value) } - Column { - visible: maxValue > 4 - spacing: -4 + GlowText { + id: hpItem + width: root.width + text: value + color: root.colors[(value >= 3 || value >= maxValue) ? 3 : (value <= 0 ? 0 : value)] + font.family: "FZLiBian-S02" + font.pixelSize: 22 + font.bold: true + horizontalAlignment: Text.AlignHCenter - Magatama { - state: (value >= 3 || value >= maxValue) ? 3 : (value <= 0 ? 0 : value) - } - - GlowText { - id: hpItem - width: root.width - text: value - color: root.colors[(value >= 3 || value >= maxValue) ? 3 : (value <= 0 ? 0 : value)] - font.family: "FZLiBian-S02" - font.pixelSize: 22 - font.bold: true - horizontalAlignment: Text.AlignHCenter - - glow.color: "#3E3F47" - glow.spread: 0.8 - glow.radius: 8 - glow.samples: 12 - } - - GlowText { - id: splitter - width: root.width - text: "/" - z: -10 - color: hpItem.color - font: hpItem.font - horizontalAlignment: hpItem.horizontalAlignment - - glow.color: hpItem.glow.color - glow.spread: hpItem.glow.spread - glow.radius: hpItem.glow.radius - glow.samples: hpItem.glow.samples - } - - GlowText { - id: maxHpItem - width: root.width - text: maxValue - color: hpItem.color - font: hpItem.font - horizontalAlignment: hpItem.horizontalAlignment - - glow.color: hpItem.glow.color - glow.spread: hpItem.glow.spread - glow.radius: hpItem.glow.radius - glow.samples: hpItem.glow.samples - } + glow.color: "#3E3F47" + glow.spread: 0.8 + glow.radius: 8 + glow.samples: 12 } + + GlowText { + id: splitter + width: root.width + text: "/" + z: -10 + color: hpItem.color + font: hpItem.font + horizontalAlignment: hpItem.horizontalAlignment + + glow.color: hpItem.glow.color + glow.spread: hpItem.glow.spread + glow.radius: hpItem.glow.radius + glow.samples: hpItem.glow.samples + } + + GlowText { + id: maxHpItem + width: root.width + text: maxValue + color: hpItem.color + font: hpItem.font + horizontalAlignment: hpItem.horizontalAlignment + + glow.color: hpItem.glow.color + glow.spread: hpItem.glow.spread + glow.radius: hpItem.glow.radius + glow.samples: hpItem.glow.samples + } + } } diff --git a/qml/Pages/RoomElement/PhotoElement/Magatama.qml b/qml/Pages/RoomElement/PhotoElement/Magatama.qml index f8e4628c..a2ab7041 100644 --- a/qml/Pages/RoomElement/PhotoElement/Magatama.qml +++ b/qml/Pages/RoomElement/PhotoElement/Magatama.qml @@ -2,57 +2,57 @@ import QtQuick 2.15 import "../../skin-bank.js" as SkinBank Image { - source: SkinBank.MAGATAMA_DIR + "0" - state: "3" + source: SkinBank.MAGATAMA_DIR + "0" + state: "3" - states: [ - State { - name: "3" - PropertyChanges { - target: main - source: SkinBank.MAGATAMA_DIR + "3" - opacity: 1 - scale: 1 - } - }, - State { - name: "2" - PropertyChanges { - target: main - source: SkinBank.MAGATAMA_DIR + "2" - opacity: 1 - scale: 1 - } - }, - State { - name: "1" - PropertyChanges { - target: main - source: SkinBank.MAGATAMA_DIR + "1" - opacity: 1 - scale: 1 - } - }, - State { - name: "0" - PropertyChanges { - target: main - source: SkinBank.MAGATAMA_DIR + "0" - opacity: 0 - scale: 4 - } - } - ] - - transitions: Transition { - PropertyAnimation { - properties: "opacity,scale" - } + states: [ + State { + name: "3" + PropertyChanges { + target: main + source: SkinBank.MAGATAMA_DIR + "3" + opacity: 1 + scale: 1 + } + }, + State { + name: "2" + PropertyChanges { + target: main + source: SkinBank.MAGATAMA_DIR + "2" + opacity: 1 + scale: 1 + } + }, + State { + name: "1" + PropertyChanges { + target: main + source: SkinBank.MAGATAMA_DIR + "1" + opacity: 1 + scale: 1 + } + }, + State { + name: "0" + PropertyChanges { + target: main + source: SkinBank.MAGATAMA_DIR + "0" + opacity: 0 + scale: 4 + } } + ] - Image { - id: main - anchors.centerIn: parent + transitions: Transition { + PropertyAnimation { + properties: "opacity,scale" } + } + + Image { + id: main + anchors.centerIn: parent + } } diff --git a/qml/Pages/RoomElement/PhotoElement/RoleComboBox.qml b/qml/Pages/RoomElement/PhotoElement/RoleComboBox.qml index b1d6e2e0..aae166f9 100644 --- a/qml/Pages/RoomElement/PhotoElement/RoleComboBox.qml +++ b/qml/Pages/RoomElement/PhotoElement/RoleComboBox.qml @@ -3,45 +3,45 @@ import QtQuick 2.15 import "../../skin-bank.js" as SkinBank Image { + property string value: "unknown" + property var options: ["unknown", "loyalist", "rebel", "renegade"] + + id: root + source: visible ? SkinBank.ROLE_DIR + value : "" + visible: value != "hidden" + + Image { property string value: "unknown" - property var options: ["unknown", "loyalist", "rebel", "renegade"] - id: root - source: visible ? SkinBank.ROLE_DIR + value : "" - visible: value != "hidden" + id: assumptionBox + source: SkinBank.ROLE_DIR + value + visible: root.value == "unknown" - Image { - property string value: "unknown" + MouseArea { + anchors.fill: parent + onClicked: optionPopupBox.visible = true; + } + } - id: assumptionBox - source: SkinBank.ROLE_DIR + value - visible: root.value == "unknown" + Column { + id: optionPopupBox + visible: false + spacing: 2 + + Repeater { + model: options + + Image { + source: SkinBank.ROLE_DIR + modelData MouseArea { - anchors.fill: parent - onClicked: optionPopupBox.visible = true; - } - } - - Column { - id: optionPopupBox - visible: false - spacing: 2 - - Repeater { - model: options - - Image { - source: SkinBank.ROLE_DIR + modelData - - MouseArea { - anchors.fill: parent - onClicked: { - optionPopupBox.visible = false; - assumptionBox.value = modelData; - } - } - } + anchors.fill: parent + onClicked: { + optionPopupBox.visible = false; + assumptionBox.value = modelData; + } } + } } + } } diff --git a/qml/Pages/RoomElement/PixmapAnimation.qml b/qml/Pages/RoomElement/PixmapAnimation.qml index f2ebc003..715eb4b3 100644 --- a/qml/Pages/RoomElement/PixmapAnimation.qml +++ b/qml/Pages/RoomElement/PixmapAnimation.qml @@ -3,87 +3,87 @@ import Qt.labs.folderlistmodel 2.15 import "../skin-bank.js" as SkinBank Item { - property string source: "" - property int currentFrame: 0 - property alias interval: timer.interval - property int loadedFrameCount: 0 - property bool autoStart: false - property bool loop: false + property string source: "" + property int currentFrame: 0 + property alias interval: timer.interval + property int loadedFrameCount: 0 + property bool autoStart: false + property bool loop: false - signal loaded() - signal started() - signal finished() + signal loaded() + signal started() + signal finished() - id: root - width: childrenRect.width - height: childrenRect.height + id: root + width: childrenRect.width + height: childrenRect.height - FolderListModel { - id: fileModel - folder: SkinBank.PIXANIM_DIR + source - nameFilters: ["*.png"] - showDirs: false - } + FolderListModel { + id: fileModel + folder: SkinBank.PIXANIM_DIR + source + nameFilters: ["*.png"] + showDirs: false + } - Repeater { - id: frames - model: fileModel + Repeater { + id: frames + model: fileModel - Image { - source: SkinBank.PIXANIM_DIR + root.source + "/" + index - visible: false - onStatusChanged: { - if (status == Image.Ready) { - loadedFrameCount++; - if (loadedFrameCount == fileModel.count) - root.loaded(); - } - } + Image { + source: SkinBank.PIXANIM_DIR + root.source + "/" + index + visible: false + onStatusChanged: { + if (status == Image.Ready) { + loadedFrameCount++; + if (loadedFrameCount == fileModel.count) + root.loaded(); } + } } + } - onLoaded: { - if (autoStart) - timer.start(); - } + onLoaded: { + if (autoStart) + timer.start(); + } - Timer { - id: timer - interval: 50 - repeat: true - onTriggered: { - if (currentFrame >= fileModel.count) { - frames.itemAt(fileModel.count - 1).visible = false; - if (loop) { - currentFrame = 0; - } else { - timer.stop(); - root.finished(); - return; - } - } - - if (currentFrame > 0) - frames.itemAt(currentFrame - 1).visible = false; - frames.itemAt(currentFrame).visible = true; - - currentFrame++; - } - } - - function start() - { - if (loadedFrameCount == fileModel.count) { - timer.start(); + Timer { + id: timer + interval: 50 + repeat: true + onTriggered: { + if (currentFrame >= fileModel.count) { + frames.itemAt(fileModel.count - 1).visible = false; + if (loop) { + currentFrame = 0; } else { - root.loaded.connect(function(){ - timer.start(); - }); + timer.stop(); + root.finished(); + return; } - } + } - function stop() - { - timer.stop(); + if (currentFrame > 0) + frames.itemAt(currentFrame - 1).visible = false; + frames.itemAt(currentFrame).visible = true; + + currentFrame++; } + } + + function start() + { + if (loadedFrameCount == fileModel.count) { + timer.start(); + } else { + root.loaded.connect(function(){ + timer.start(); + }); + } + } + + function stop() + { + timer.stop(); + } } diff --git a/qml/Pages/RoomElement/SkillArea.qml b/qml/Pages/RoomElement/SkillArea.qml index ee41fbc8..4554a08c 100644 --- a/qml/Pages/RoomElement/SkillArea.qml +++ b/qml/Pages/RoomElement/SkillArea.qml @@ -1,5 +1,5 @@ import QtQuick 2.15 Flickable { - id: root + id: root } diff --git a/qml/Pages/RoomElement/TablePile.qml b/qml/Pages/RoomElement/TablePile.qml index f49f6f6d..58cfd9e0 100644 --- a/qml/Pages/RoomElement/TablePile.qml +++ b/qml/Pages/RoomElement/TablePile.qml @@ -1,135 +1,135 @@ import QtQuick 2.15 Item { - property var discardedCards: [] - property alias cards: area.cards - property bool toVanish: false + property var discardedCards: [] + property alias cards: area.cards + property bool toVanish: false - id: root + id: root - CardArea { - id: area - } + CardArea { + id: area + } - InvisibleCardArea { - id: invisibleArea - } + InvisibleCardArea { + id: invisibleArea + } - Timer { - id: vanishTimer - interval: 1500 - repeat: true - running: true - triggeredOnStart: true - onTriggered: { - let i, card; - if (toVanish) { - for (i = 0; i < discardedCards.length; i++) { - card = discardedCards[i]; - card.origOpacity = 0; - card.goBack(true); - card.destroyOnStop() - } - - cards.splice(0, discardedCards.length); - updateCardPosition(true); - - discardedCards = new Array(cards.length); - for (i = 0; i < cards.length; i++) - discardedCards[i] = cards[i]; - toVanish = false - } else { - for (i = 0; i < discardedCards.length; i++) { - discardedCards[i].selectable = false - } - toVanish = true - } + Timer { + id: vanishTimer + interval: 1500 + repeat: true + running: true + triggeredOnStart: true + onTriggered: { + let i, card; + if (toVanish) { + for (i = 0; i < discardedCards.length; i++) { + card = discardedCards[i]; + card.origOpacity = 0; + card.goBack(true); + card.destroyOnStop() } - } - function add(inputs) - { - area.add(inputs); - // if (!inputs instanceof Array) - for (let i = 0; i < inputs.length; i++) { - inputs[i].footnoteVisible = true - inputs[i].selectable = true - } - } - - function remove(outputs) - { - let i, j; - - let result = area.remove(outputs); - let vanished = []; - if (result.length < outputs.length) { - for (i = 0; i < outputs.length; i++) { - let exists = false; - for (j = 0; j < result.length; j++) { - if (result[j].cid === outputs[i]) { - exists = true; - break; - } - } - if (!exists) - vanished.push(outputs[i]); - } - } - result = result.concat(invisibleArea.remove(vanished)); - - for (i = 0; i < result.length; i++) { - for (j = 0; j < discardedCards.length; j++) { - if (result[i].cid === discardedCards[j].cid) { - discardedCards.splice(j, 1); - break; - } - } - } + cards.splice(0, discardedCards.length); updateCardPosition(true); - return result; + + discardedCards = new Array(cards.length); + for (i = 0; i < cards.length; i++) + discardedCards[i] = cards[i]; + toVanish = false + } else { + for (i = 0; i < discardedCards.length; i++) { + discardedCards[i].selectable = false + } + toVanish = true + } + } + } + + function add(inputs) + { + area.add(inputs); + // if (!inputs instanceof Array) + for (let i = 0; i < inputs.length; i++) { + inputs[i].footnoteVisible = true + inputs[i].selectable = true + } + } + + function remove(outputs) + { + let i, j; + + let result = area.remove(outputs); + let vanished = []; + if (result.length < outputs.length) { + for (i = 0; i < outputs.length; i++) { + let exists = false; + for (j = 0; j < result.length; j++) { + if (result[j].cid === outputs[i]) { + exists = true; + break; + } + } + if (!exists) + vanished.push(outputs[i]); + } + } + result = result.concat(invisibleArea.remove(vanished)); + + for (i = 0; i < result.length; i++) { + for (j = 0; j < discardedCards.length; j++) { + if (result[i].cid === discardedCards[j].cid) { + discardedCards.splice(j, 1); + break; + } + } + } + updateCardPosition(true); + return result; + } + + function updateCardPosition(animated) + { + if (cards.length <= 0) + return; + + let i, card; + + let overflow = false; + for (i = 0; i < cards.length; i++) { + card = cards[i]; + card.origX = i * card.width; + if (card.origX + card.width >= root.width) { + overflow = true; + break; + } + card.origY = 0; } - function updateCardPosition(animated) - { - if (cards.length <= 0) - return; - - let i, card; - - let overflow = false; - for (i = 0; i < cards.length; i++) { - card = cards[i]; - card.origX = i * card.width; - if (card.origX + card.width >= root.width) { - overflow = true; - break; - } - card.origY = 0; - } - - if (overflow) { - //@to-do: Adjust cards in multiple lines if there are too many cards - 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; - card.origY = 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.origX += parentPos.x + offsetX; - card.origY += parentPos.y; - } - - if (animated) { - for (i = 0; i < cards.length; i++) - cards[i].goBack(true) - } + if (overflow) { + //@to-do: Adjust cards in multiple lines if there are too many cards + 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; + card.origY = 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.origX += parentPos.x + offsetX; + card.origY += parentPos.y; + } + + if (animated) { + for (i = 0; i < cards.length; i++) + cards[i].goBack(true) + } + } } diff --git a/qml/Pages/RoomLogic.js b/qml/Pages/RoomLogic.js index de90f53e..bc264dc0 100644 --- a/qml/Pages/RoomLogic.js +++ b/qml/Pages/RoomLogic.js @@ -1,377 +1,465 @@ var Card = { - Unknown : 0, - PlayerHand : 1, - PlayerEquip : 2, - PlayerJudge : 3, - PlayerSpecial : 4, - Processing : 5, - DrawPile : 6, - DiscardPile : 7, - Void : 8 + Unknown : 0, + PlayerHand : 1, + PlayerEquip : 2, + PlayerJudge : 3, + PlayerSpecial : 4, + Processing : 5, + DrawPile : 6, + DiscardPile : 7, + Void : 8 } function arrangePhotos() { - /* Layout of photos: - * +---------------+ - * | 6 5 4 3 2 | - * | 7 1 | - * | dashboard | - * +---------------+ - */ + /* Layout of photos: + * +---------------+ + * | 6 5 4 3 2 | + * | 7 1 | + * | dashboard | + * +---------------+ + */ - 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; + 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 - 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 }, - { x: verticalPadding + (photoWidth + verticalSpacing) * 3, y: roomAreaPadding }, - { x: verticalPadding + (photoWidth + verticalSpacing) * 2, y: roomAreaPadding }, - { x: verticalPadding + photoWidth + verticalSpacing, y: roomAreaPadding + horizontalSpacing }, - { x: verticalPadding, y: roomAreaPadding + horizontalSpacing * 2 }, - ]; + // Position 1-7 + 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 }, + { x: verticalPadding + (photoWidth + verticalSpacing) * 3, y: roomAreaPadding }, + { x: verticalPadding + (photoWidth + verticalSpacing) * 2, y: roomAreaPadding }, + { x: verticalPadding + photoWidth + verticalSpacing, y: roomAreaPadding + horizontalSpacing }, + { x: verticalPadding, y: roomAreaPadding + horizontalSpacing * 2 }, + ]; - const regularSeatIndex = [ - [4], - [3, 5], - [1, 4, 7], - [1, 3, 5, 7], - [1, 3, 4, 5, 7], - [1, 2, 3, 5, 6, 7], - [1, 2, 3, 4, 5, 6, 7], - ]; - let seatIndex = regularSeatIndex[playerNum - 2]; + const regularSeatIndex = [ + [4], + [3, 5], + [1, 4, 7], + [1, 3, 5, 7], + [1, 3, 4, 5, 7], + [1, 2, 3, 5, 6, 7], + [1, 2, 3, 4, 5, 6, 7], + ]; + let seatIndex = regularSeatIndex[playerNum - 2]; - let item, region, i; + let item, region, i; - for (i = 0; i < playerNum - 1; i++) { - item = photos.itemAt(i); - if (!item) - continue; + for (i = 0; i < playerNum - 1; i++) { + item = photos.itemAt(i); + if (!item) + continue; - region = regions[seatIndex[photoModel.get(i).index] - 1]; - item.x = region.x; - item.y = region.y; - } + region = regions[seatIndex[photoModel.get(i).index] - 1]; + item.x = region.x; + item.y = region.y; + } } function doOkButton() { - replyToServer("1"); + if (roomScene.state == "playing") { + replyToServer(JSON.stringify( + { + card: dashboard.getSelectedCard(), + targets: selected_targets + } + )); + return; + } + replyToServer("1"); } function doCancelButton() { - replyToServer(""); + replyToServer(""); } function replyToServer(jsonData) { - roomScene.state = "notactive"; - ClientInstance.replyToServer("", jsonData); + roomScene.state = "notactive"; + ClientInstance.replyToServer("", jsonData); } function getPhotoModel(id) { - for (let i = 0; i < photoModel.count; i++) { - let item = photoModel.get(i); - if (item.id === id) { - return item; - } + for (let i = 0; i < photoModel.count; i++) { + let item = photoModel.get(i); + if (item.id === id) { + return item; } - return undefined; + } + return undefined; } function getPhoto(id) { - for (let i = 0; i < photoModel.count; i++) { - let item = photoModel.get(i); - if (item.id === id) { - return photos.itemAt(i); - } + for (let i = 0; i < photoModel.count; i++) { + let item = photoModel.get(i); + if (item.id === id) { + return photos.itemAt(i); } - return undefined; + } + return undefined; } function getPhotoOrDashboard(id) { - let photo = getPhoto(id); - if (!photo) { - if (id === Self.id) - return dashboard; - } - return photo; + let photo = getPhoto(id); + if (!photo) { + if (id === Self.id) + return dashboard; + } + return photo; } function getAreaItem(area, id) { - if (area === Card.DrawPile) { - return drawPile; - } else if (area === Card.DiscardPile || area === Card.Processing) { - return tablePile; - } else if (area === Card.AG) { - return popupBox.item; - } - - let photo = getPhotoOrDashboard(id); - if (!photo) { - return null; - } - - if (area === Card.PlayerHand) { - return photo.handcardArea; - } else if (area === Card.PlayerEquip) - return photo.equipArea; - else if (area === Card.PlayerJudge) - return photo.delayedTrickArea; - else if (area === Card.PlayerSpecial) - return photo.specialArea; + if (area === Card.DrawPile) { + return drawPile; + } else if (area === Card.DiscardPile || area === Card.Processing) { + return tablePile; + } else if (area === Card.AG) { + return popupBox.item; + } + let photo = getPhotoOrDashboard(id); + if (!photo) { return null; + } + + if (area === Card.PlayerHand) { + return photo.handcardArea; + } else if (area === Card.PlayerEquip) + return photo.equipArea; + else if (area === Card.PlayerJudge) + return photo.delayedTrickArea; + else if (area === Card.PlayerSpecial) + return photo.specialArea; + + return null; } function moveCards(moves) { - for (let i = 0; i < moves.length; i++) { - let move = moves[i]; - let from = getAreaItem(move.fromArea, move.from); - let to = getAreaItem(move.toArea, move.to); - if (!from || !to || from === to) - continue; - let items = from.remove(move.ids); - if (items.length > 0) - to.add(items); - to.updateCardPosition(true); - } + for (let i = 0; i < moves.length; i++) { + let move = moves[i]; + let from = getAreaItem(move.fromArea, move.from); + let to = getAreaItem(move.toArea, move.to); + if (!from || !to || from === to) + continue; + let items = from.remove(move.ids); + if (items.length > 0) + to.add(items); + to.updateCardPosition(true); + } } function setEmotion(id, emotion) { - let component = Qt.createComponent("RoomElement/PixmapAnimation.qml"); - if (component.status !== Component.Ready) - return; + let component = Qt.createComponent("RoomElement/PixmapAnimation.qml"); + if (component.status !== Component.Ready) + return; - let photo = getPhoto(id); - if (!photo) { - if (id === dashboardModel.id) { - photo = dashboard.self; - } else { - return null; - } + let photo = getPhoto(id); + if (!photo) { + if (id === dashboardModel.id) { + photo = dashboard.self; + } else { + return null; } + } - let animation = component.createObject(photo, {source: emotion, anchors: {centerIn: photo}}); - animation.finished.connect(() => animation.destroy()); - animation.start(); + let animation = component.createObject(photo, {source: emotion, anchors: {centerIn: photo}}); + animation.finished.connect(() => animation.destroy()); + animation.start(); } function changeHp(id, delta, losthp) { - let photo = getPhoto(id); - if (!photo) { - if (id === dashboardModel.id) { - photo = dashboard.self; - } else { - return null; - } + let photo = getPhoto(id); + if (!photo) { + if (id === dashboardModel.id) { + photo = dashboard.self; + } else { + return null; } - if (delta < 0) { - if (!losthp) { - setEmotion(id, "damage") - photo.tremble() - } + } + if (delta < 0) { + if (!losthp) { + setEmotion(id, "damage") + photo.tremble() } + } } function doIndicate(from, tos) { - let component = Qt.createComponent("RoomElement/IndicatorLine.qml"); - if (component.status !== Component.Ready) - return; + let component = Qt.createComponent("RoomElement/IndicatorLine.qml"); + if (component.status !== Component.Ready) + return; - let fromItem = getPhotoOrDashboard(from); - let fromPos = mapFromItem(fromItem, fromItem.width / 2, fromItem.height / 2); + let fromItem = getPhotoOrDashboard(from); + let fromPos = mapFromItem(fromItem, fromItem.width / 2, fromItem.height / 2); - let end = []; - for (let i = 0; i < tos.length; i++) { - if (from === tos[i]) - continue; - let toItem = getPhotoOrDashboard(tos[i]); - let toPos = mapFromItem(toItem, toItem.width / 2, toItem.height / 2); - end.push(toPos); - } + let end = []; + for (let i = 0; i < tos.length; i++) { + if (from === tos[i]) + continue; + let toItem = getPhotoOrDashboard(tos[i]); + let toPos = mapFromItem(toItem, toItem.width / 2, toItem.height / 2); + end.push(toPos); + } - let color = "#96943D"; - let line = component.createObject(roomScene, {start: fromPos, end: end, color: color}); - line.finished.connect(() => line.destroy()); - line.running = true; + let color = "#96943D"; + let line = component.createObject(roomScene, {start: fromPos, end: end, color: color}); + line.finished.connect(() => line.destroy()); + line.running = true; } callbacks["AddPlayer"] = function(jsonData) { - // jsonData: int id, string screenName, string avatar - for (let i = 0; i < photoModel.count; i++) { - let item = photoModel.get(i); - if (item.id === -1) { - let data = JSON.parse(jsonData); - let uid = data[0]; - let name = data[1]; - let avatar = data[2]; - item.id = uid; - item.screenName = name; - item.general = avatar; - return; - } + // jsonData: int id, string screenName, string avatar + for (let i = 0; i < photoModel.count; i++) { + let item = photoModel.get(i); + if (item.id === -1) { + let data = JSON.parse(jsonData); + let uid = data[0]; + let name = data[1]; + let avatar = data[2]; + item.id = uid; + item.screenName = name; + item.general = avatar; + return; } + } +} + +function enableTargets(card) { // card: int | { skill: string, subcards: int[] } + let i = 0; + let candidate = (!isNaN(card) && card !== -1) || typeof(card) === "string"; + let all_photos = [dashboard.self]; + for (i = 0; i < playerNum - 1; i++) { + all_photos.push(photos.itemAt(i)) + } + selected_targets = []; + for (i = 0; i < playerNum; i++) { + all_photos[i].selected = false; + } + + if (candidate) { + let data = { + ok_enabled: false, + enabled_targets: [] + } + + all_photos.forEach(photo => { + photo.state = "candidate"; + let id = photo.playerid; + let ret = JSON.parse(Backend.callLuaFunction( + "CanUseCardToTarget", + [card, id, selected_targets] + )); + photo.selectable = ret; + }) + + okButton.enabled = JSON.parse(Backend.callLuaFunction( + "CardFeasible", [card, selected_targets] + )); + } else { + all_photos.forEach(photo => { + photo.state = "normal"; + photo.selected = false; + }); + + okButton.enabled = false; + } +} + +function updateSelectedTargets(playerid, selected) { + let i = 0; + let card = dashboard.getSelectedCard(); + let all_photos = [dashboard.self] + for (i = 0; i < playerNum - 1; i++) { + all_photos.push(photos.itemAt(i)) + } + + if (selected) { + selected_targets.push(playerid); + } else { + selected_targets.splice(selected_targets.indexOf(playerid), 1); + } + + all_photos.forEach(photo => { + if (photo.selected) return; + let id = photo.playerid; + let ret = JSON.parse(Backend.callLuaFunction( + "CanUseCardToTarget", + [card, id, selected_targets] + )); + photo.selectable = ret; + }) + + okButton.enabled = JSON.parse(Backend.callLuaFunction( + "CardFeasible", [card, selected_targets] + )); } callbacks["RemovePlayer"] = function(jsonData) { - // jsonData: int uid - let uid = JSON.parse(jsonData)[0]; - let model = getPhotoModel(uid); - if (typeof(model) !== "undefined") { - model.id = -1; - model.screenName = ""; - model.general = ""; - } + // jsonData: int uid + let uid = JSON.parse(jsonData)[0]; + let model = getPhotoModel(uid); + if (typeof(model) !== "undefined") { + model.id = -1; + model.screenName = ""; + model.general = ""; + } } callbacks["RoomOwner"] = function(jsonData) { - // jsonData: int uid of the owner - let uid = JSON.parse(jsonData)[0]; + // jsonData: int uid of the owner + let uid = JSON.parse(jsonData)[0]; - if (dashboardModel.id === uid) { - dashboardModel.isOwner = true; - roomScene.dashboardModelChanged(); - return; - } + if (dashboardModel.id === uid) { + dashboardModel.isOwner = true; + roomScene.dashboardModelChanged(); + return; + } - let model = getPhotoModel(uid); - if (typeof(model) !== "undefined") { - model.isOwner = true; - } + let model = getPhotoModel(uid); + if (typeof(model) !== "undefined") { + model.isOwner = true; + } } callbacks["PropertyUpdate"] = function(jsonData) { - // jsonData: int id, string property_name, value - let data = JSON.parse(jsonData); - let uid = data[0]; - let property_name = data[1]; - let value = data[2]; + // jsonData: int id, string property_name, value + let data = JSON.parse(jsonData); + let uid = data[0]; + let property_name = data[1]; + let value = data[2]; - if (Self.id === uid) { - dashboardModel[property_name] = value; - roomScene.dashboardModelChanged(); - return; - } + if (Self.id === uid) { + dashboardModel[property_name] = value; + roomScene.dashboardModelChanged(); + return; + } - let model = getPhotoModel(uid); - if (typeof(model) !== "undefined") { - model[property_name] = value; - } + let model = getPhotoModel(uid); + if (typeof(model) !== "undefined") { + model[property_name] = value; + } } callbacks["ArrangeSeats"] = function(jsonData) { - // jsonData: seat order - let order = JSON.parse(jsonData); - roomScene.isStarted = true; + // 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; - } + for (let i = 0; i < photoModel.count; i++) { + let item = photoModel.get(i); + item.seatNumber = order.indexOf(item.id) + 1; + } - dashboardModel.seatNumber = order.indexOf(Self.id) + 1; - roomScene.dashboardModelChanged(); - - // make Self to the first of list, then reorder photomodel - let selfIndex = order.indexOf(Self.id); - let after = order.splice(selfIndex); - after.push(...order); - let photoOrder = after.slice(1); + dashboardModel.seatNumber = order.indexOf(Self.id) + 1; + roomScene.dashboardModelChanged(); + + // make Self to the first of list, then reorder photomodel + let selfIndex = order.indexOf(Self.id); + let after = order.splice(selfIndex); + after.push(...order); + let photoOrder = after.slice(1); - for (let i = 0; i < photoModel.count; i++) { - let item = photoModel.get(i); - item.index = photoOrder.indexOf(item.id); - } - - arrangePhotos(); + for (let i = 0; i < photoModel.count; i++) { + let item = photoModel.get(i); + item.index = photoOrder.indexOf(item.id); + } + + arrangePhotos(); } function cancelAllFocus() { - let item; - for (let i = 0; i < playerNum - 1; i++) { - item = photos.itemAt(i); - item.progressBar.visible = false; - item.progressTip = ""; - } + 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..."; - } + // 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["PlayerRunned"] = function(jsonData) { - // jsonData: int runner, int robot - let data = JSON.parse(jsonData); - let runner = data[0]; - let robot = data[1]; + // jsonData: int runner, int robot + let data = JSON.parse(jsonData); + let runner = data[0]; + let robot = data[1]; - let model = getPhotoModel(runner); - if (typeof(model) !== "undefined") { - model.id = robot; - } + let model = getPhotoModel(runner); + if (typeof(model) !== "undefined") { + model.id = robot; + } } callbacks["AskForGeneral"] = function(jsonData) { - // jsonData: string[] Generals - // TODO: choose multiple generals - let data = JSON.parse(jsonData); - roomScene.promptText = "Please choose 1 general"; - roomScene.state = "replying"; - roomScene.popupBox.source = "RoomElement/ChooseGeneralBox.qml"; - let box = roomScene.popupBox.item; - box.choiceNum = 1; - box.accepted.connect(() => { - replyToServer(JSON.stringify([box.choices[0]])); - }); - for (let i = 0; i < data.length; i++) - box.generalList.append({ "name": data[i] }); - box.updatePosition(); + // jsonData: string[] Generals + // TODO: choose multiple generals + let data = JSON.parse(jsonData); + roomScene.promptText = "Please choose 1 general"; + roomScene.state = "replying"; + roomScene.popupBox.source = "RoomElement/ChooseGeneralBox.qml"; + let box = roomScene.popupBox.item; + box.choiceNum = 1; + box.accepted.connect(() => { + replyToServer(JSON.stringify([box.choices[0]])); + }); + for (let i = 0; i < data.length; i++) + box.generalList.append({ "name": data[i] }); + box.updatePosition(); } callbacks["AskForSkillInvoke"] = function(jsonData) { - // jsonData: string name - roomScene.promptText = "Do you want to invoke '" + jsonData + "' ?"; - roomScene.state = "responding"; + // jsonData: string name + roomScene.promptText = "Do you want to invoke '" + jsonData + "' ?"; + roomScene.state = "responding"; } callbacks["AskForChoice"] = function(jsonData) { - // jsonData: [ string[] choices, string skill ] - // TODO: multiple choices, e.g. benxi_ol - let data = JSON.parse(jsonData); - let choices = data[0]; - let skill_name = data[1]; - roomScene.promptText = skill_name + ": Please make choice"; - roomScene.state = "replying"; - roomScene.popupBox.source = "RoomElement/ChoiceBox.qml"; - let box = roomScene.popupBox.item; - box.options = choices; - box.skill_name = skill_name; - box.accepted.connect(() => { - replyToServer(choices[box.result]); - }); + // jsonData: [ string[] choices, string skill ] + // TODO: multiple choices, e.g. benxi_ol + let data = JSON.parse(jsonData); + let choices = data[0]; + let skill_name = data[1]; + roomScene.promptText = skill_name + ": Please make choice"; + roomScene.state = "replying"; + roomScene.popupBox.source = "RoomElement/ChoiceBox.qml"; + let box = roomScene.popupBox.item; + box.options = choices; + box.skill_name = skill_name; + box.accepted.connect(() => { + replyToServer(choices[box.result]); + }); } callbacks["MoveCards"] = function(jsonData) { - // jsonData: merged moves - let moves = JSON.parse(jsonData); - moveCards(moves); + // jsonData: merged moves + let moves = JSON.parse(jsonData); + moveCards(moves); +} + +callbacks["PlayCard"] = function(jsonData) { + // jsonData: int playerId + let playerId = parseInt(jsonData); + if (playerId == Self.id) { + roomScene.promptText = "Please use a card"; + roomScene.state = "playing"; + } } diff --git a/qml/Toast.qml b/qml/Toast.qml index 2cf878a9..62d67e56 100644 --- a/qml/Toast.qml +++ b/qml/Toast.qml @@ -1,56 +1,56 @@ import QtQuick 2.15 Rectangle { - function show(text, duration) { - message.text = text; - time = Math.max(duration, 2 * fadeTime); - animation.start(); + function show(text, duration) { + message.text = text; + time = Math.max(duration, 2 * fadeTime); + animation.start(); + } + + id: root + + readonly property real defaultTime: 3000 + property real time: defaultTime + readonly property real fadeTime: 300 + + anchors.horizontalCenter: parent != null ? parent.horizontalCenter : undefined + height: message.height + 20 + width: message.width + 40 + radius: 16 + + opacity: 0 + color: "#F2808A87" + + Text { + id: message + color: "white" + horizontalAlignment: Text.AlignHCenter + anchors.centerIn: parent + } + + SequentialAnimation on opacity { + id: animation + running: false + + + NumberAnimation { + to: .9 + duration: fadeTime } - id: root - - readonly property real defaultTime: 3000 - property real time: defaultTime - readonly property real fadeTime: 300 - - anchors.horizontalCenter: parent != null ? parent.horizontalCenter : undefined - height: message.height + 20 - width: message.width + 40 - radius: 16 - - opacity: 0 - color: "#F2808A87" - - Text { - id: message - color: "white" - horizontalAlignment: Text.AlignHCenter - anchors.centerIn: parent + PauseAnimation { + duration: time - 2 * fadeTime } - SequentialAnimation on opacity { - id: animation - running: false - - - NumberAnimation { - to: .9 - duration: fadeTime - } - - PauseAnimation { - duration: time - 2 * fadeTime - } - - NumberAnimation { - to: 0 - duration: fadeTime - } - - onRunningChanged: { - if (!running) { - toast.model.remove(index); - } - } + NumberAnimation { + to: 0 + duration: fadeTime } + + onRunningChanged: { + if (!running) { + toast.model.remove(index); + } + } + } } diff --git a/qml/ToastManager.qml b/qml/ToastManager.qml index efb374bd..71cdb21d 100644 --- a/qml/ToastManager.qml +++ b/qml/ToastManager.qml @@ -3,35 +3,35 @@ import QtQuick 2.15 // copy from https://gist.github.com/jonmcclung/bae669101d17b103e94790341301c129 // and modified some code ListView { - function show(text, duration) { - if (duration === undefined) { - duration = 3000; - } - model.insert(0, {text: text, duration: duration}); + function show(text, duration) { + if (duration === undefined) { + duration = 3000; } + model.insert(0, {text: text, duration: duration}); + } - id: root + id: root - z: Infinity - spacing: 5 - anchors.fill: parent - anchors.bottomMargin: 10 - verticalLayoutDirection: ListView.BottomToTop + z: Infinity + spacing: 5 + anchors.fill: parent + anchors.bottomMargin: 10 + verticalLayoutDirection: ListView.BottomToTop - interactive: false + interactive: false - displaced: Transition { - NumberAnimation { - properties: "y" - easing.type: Easing.InOutQuad - } + displaced: Transition { + NumberAnimation { + properties: "y" + easing.type: Easing.InOutQuad } - - delegate: Toast { - Component.onCompleted: { - show(text, duration); - } + } + + delegate: Toast { + Component.onCompleted: { + show(text, duration); } + } - model: ListModel {id: model} + model: ListModel {id: model} } diff --git a/qml/main.qml b/qml/main.qml index ffb8b920..6667664a 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -5,116 +5,116 @@ import "Logic.js" as Logic import "Pages" Window { - id: mainWindow - visible: true - width: 720 - height: 480 - property var callbacks: Logic.callbacks + id: mainWindow + visible: true + width: 720 + height: 480 + property var callbacks: Logic.callbacks - StackView { - id: mainStack - visible: !mainWindow.busy - initialItem: init - anchors.fill: parent + StackView { + id: mainStack + visible: !mainWindow.busy + initialItem: init + anchors.fill: parent + } + + Component { id: init; Init {} } + Component { id: lobby; Lobby {} } + Component { id: generalsOverview; GeneralsOverview {} } + Component { id: cardsOverview; CardsOverview {} } + Component { id: room; Room {} } + + property bool busy: false + BusyIndicator { + running: true + anchors.centerIn: parent + visible: mainWindow.busy === true + } + + Config { + id: config + } + + // global popup. it is modal and just lower than toast + Rectangle { + id: globalPopupDim + anchors.fill: parent + color: "black" + opacity: 0 + visible: !mainWindow.busy + + property bool stateVisible: false + states: [ + State { + when: globalPopupDim.stateVisible + PropertyChanges { target: globalPopupDim; opacity: 0.5 } + }, + State { + when: !globalPopupDim.stateVisible + PropertyChanges { target: globalPopupDim; opacity: 0.0 } + } + ] + + transitions: Transition { + NumberAnimation { properties: "opacity"; easing.type: Easing.InOutQuad } + } + } + + Popup { + id: globalPopup + property string source: "" + modal: true + dim: false // cannot animate the dim + focus: true + opacity: mainWindow.busy ? 0 : 1 + closePolicy: Popup.CloseOnEscape + anchors.centerIn: parent + + onAboutToShow: { + globalPopupDim.stateVisible = true } - Component { id: init; Init {} } - Component { id: lobby; Lobby {} } - Component { id: generalsOverview; GeneralsOverview {} } - Component { id: cardsOverview; CardsOverview {} } - Component { id: room; Room {} } - - property bool busy: false - BusyIndicator { - running: true - anchors.centerIn: parent - visible: mainWindow.busy === true + enter: Transition { + NumberAnimation { properties: "opacity"; from: 0; to: 1 } + NumberAnimation { properties: "scale"; from: 0.4; to: 1 } } - Config { - id: config + onAboutToHide: { + globalPopupDim.stateVisible = false } - // global popup. it is modal and just lower than toast - Rectangle { - id: globalPopupDim - anchors.fill: parent - color: "black" - opacity: 0 - visible: !mainWindow.busy - - property bool stateVisible: false - states: [ - State { - when: globalPopupDim.stateVisible - PropertyChanges { target: globalPopupDim; opacity: 0.5 } - }, - State { - when: !globalPopupDim.stateVisible - PropertyChanges { target: globalPopupDim; opacity: 0.0 } - } - ] - - transitions: Transition { - NumberAnimation { properties: "opacity"; easing.type: Easing.InOutQuad } - } + exit: Transition { + NumberAnimation { properties: "opacity"; from: 1; to: 0 } + NumberAnimation { properties: "scale"; from: 1; to: 0.4 } } - Popup { - id: globalPopup - property string source: "" - modal: true - dim: false // cannot animate the dim - focus: true - opacity: mainWindow.busy ? 0 : 1 - closePolicy: Popup.CloseOnEscape - anchors.centerIn: parent - - onAboutToShow: { - globalPopupDim.stateVisible = true - } - - enter: Transition { - NumberAnimation { properties: "opacity"; from: 0; to: 1 } - NumberAnimation { properties: "scale"; from: 0.4; to: 1 } - } - - onAboutToHide: { - globalPopupDim.stateVisible = false - } - - exit: Transition { - NumberAnimation { properties: "opacity"; from: 1; to: 0 } - NumberAnimation { properties: "scale"; from: 1; to: 0.4 } - } - - Loader { - visible: !mainWindow.busy - source: globalPopup.source === "" ? "" : "GlobalPopups/" + globalPopup.source - onSourceChanged: { - if (item === null) - return; - item.finished.connect(() => { - globalPopup.close(); - globalPopup.source = ""; - }); - } - } + Loader { + visible: !mainWindow.busy + source: globalPopup.source === "" ? "" : "GlobalPopups/" + globalPopup.source + onSourceChanged: { + if (item === null) + return; + item.finished.connect(() => { + globalPopup.close(); + globalPopup.source = ""; + }); + } } + } - ToastManager { - id: toast - } + ToastManager { + id: toast + } - Connections { - target: Backend - function onNotifyUI(command, jsonData) { - let cb = callbacks[command] - if (typeof(cb) === "function") { - cb(jsonData); - } else { - callbacks["ErrorMsg"]("Unknown command " + command + "!"); - } - } + Connections { + target: Backend + function onNotifyUI(command, jsonData) { + let cb = callbacks[command] + if (typeof(cb) === "function") { + cb(jsonData); + } else { + callbacks["ErrorMsg"]("Unknown command " + command + "!"); + } } + } } diff --git a/qml/util.js b/qml/util.js index 796be1c0..5f18888c 100644 --- a/qml/util.js +++ b/qml/util.js @@ -1,21 +1,21 @@ .pragma library function convertNumber(number) { - if (number === 1) - return "A"; - if (number >= 2 && number <= 10) - return number; - if (number >= 11 && number <= 13) { - const strs = ["J", "Q", "K"]; - return strs[number - 11]; - } - return ""; + if (number === 1) + return "A"; + if (number >= 2 && number <= 10) + return number; + if (number >= 11 && number <= 13) { + const strs = ["J", "Q", "K"]; + return strs[number - 11]; + } + return ""; } Array.prototype.contains = function(element) { - return this.indexOf(element) != -1; + return this.indexOf(element) != -1; } Array.prototype.prepend = function() { - this.splice(0, 0, ...arguments); + this.splice(0, 0, ...arguments); } diff --git a/server/init.sql b/server/init.sql index f3e09c9c..69afb583 100644 --- a/server/init.sql +++ b/server/init.sql @@ -1,12 +1,12 @@ CREATE TABLE userinfo ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name VARCHAR(255), - password CHAR(64), - avatar VARCHAR(64), - lastLoginIp VARCHAR(64), - banned BOOLEAN + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(255), + password CHAR(64), + avatar VARCHAR(64), + lastLoginIp VARCHAR(64), + banned BOOLEAN ); CREATE TABLE banip ( - ip VARCHAR(64) + ip VARCHAR(64) ); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ed26a31b..52ec07c9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,39 +1,39 @@ set(freekill_SRCS - "main.cpp" - "core/player.cpp" - "core/util.cpp" - "network/server_socket.cpp" - "network/client_socket.cpp" - "network/router.cpp" - "server/server.cpp" - "server/serverplayer.cpp" - "server/room.cpp" - "client/client.cpp" - "client/clientplayer.cpp" - "ui/qmlbackend.cpp" - "swig/freekill-wrap.cxx" + "main.cpp" + "core/player.cpp" + "core/util.cpp" + "network/server_socket.cpp" + "network/client_socket.cpp" + "network/router.cpp" + "server/server.cpp" + "server/serverplayer.cpp" + "server/room.cpp" + "client/client.cpp" + "client/clientplayer.cpp" + "ui/qmlbackend.cpp" + "swig/freekill-wrap.cxx" ) set(freekill_HEADERS - "core/util.h" - "core/player.h" - "network/server_socket.h" - "network/client_socket.h" - "network/router.h" - "server/server.h" - "server/serverplayer.h" - "server/room.h" - "client/client.h" - "client/clientplayer.h" - "ui/qmlbackend.h" + "core/util.h" + "core/player.h" + "network/server_socket.h" + "network/client_socket.h" + "network/router.h" + "server/server.h" + "server/serverplayer.h" + "server/room.h" + "client/client.h" + "client/clientplayer.h" + "ui/qmlbackend.h" ) if (WIN32) - set(LUA_LIB ${PROJECT_SOURCE_DIR}/lib/win/lua54.dll) - set(SQLITE3_LIB ${PROJECT_SOURCE_DIR}/lib/win/sqlite3.dll) + set(LUA_LIB ${PROJECT_SOURCE_DIR}/lib/win/lua54.dll) + set(SQLITE3_LIB ${PROJECT_SOURCE_DIR}/lib/win/sqlite3.dll) else () - set(LUA_LIB lua5.4) - set(SQLITE3_LIB sqlite3) + set(LUA_LIB lua5.4) + set(SQLITE3_LIB sqlite3) endif () source_group("Include" FILES ${freekill_HEADERS}) @@ -42,10 +42,10 @@ target_precompile_headers(FreeKill PRIVATE "pch.h") target_link_libraries(FreeKill ${LUA_LIB} ${SQLITE3_LIB} Qt5::Qml Qt5::Gui Qt5::Network Qt5::Multimedia) file(GLOB SWIG_FILES "${PROJECT_SOURCE_DIR}/src/swig/*.i") add_custom_command( - OUTPUT ${PROJECT_SOURCE_DIR}/src/swig/freekill-wrap.cxx - DEPENDS ${SWIG_FILES} - COMMENT "Generating freekill-wrap.cxx" - COMMAND swig -c++ -lua -Wall -o - ${PROJECT_SOURCE_DIR}/src/swig/freekill-wrap.cxx - ${PROJECT_SOURCE_DIR}/src/swig/freekill.i + OUTPUT ${PROJECT_SOURCE_DIR}/src/swig/freekill-wrap.cxx + DEPENDS ${SWIG_FILES} + COMMENT "Generating freekill-wrap.cxx" + COMMAND swig -c++ -lua -Wall -o + ${PROJECT_SOURCE_DIR}/src/swig/freekill-wrap.cxx + ${PROJECT_SOURCE_DIR}/src/swig/freekill.i ) diff --git a/src/client/client.cpp b/src/client/client.cpp index bd9a702f..9194324b 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -7,67 +7,67 @@ Client *ClientInstance; ClientPlayer *Self; Client::Client(QObject* parent) - : QObject(parent), callback(0) + : QObject(parent), callback(0) { - ClientInstance = this; - Self = new ClientPlayer(0, this); - QQmlApplicationEngine *engine = Backend->getEngine(); - engine->rootContext()->setContextProperty("ClientInstance", ClientInstance); - engine->rootContext()->setContextProperty("Self", Self); + ClientInstance = this; + Self = new ClientPlayer(0, this); + QQmlApplicationEngine *engine = Backend->getEngine(); + engine->rootContext()->setContextProperty("ClientInstance", ClientInstance); + engine->rootContext()->setContextProperty("Self", Self); - ClientSocket *socket = new ClientSocket; - connect(socket, &ClientSocket::error_message, this, &Client::error_message); - router = new Router(this, socket, Router::TYPE_CLIENT); + ClientSocket *socket = new ClientSocket; + connect(socket, &ClientSocket::error_message, this, &Client::error_message); + router = new Router(this, socket, Router::TYPE_CLIENT); - L = CreateLuaState(); - DoLuaScript(L, "lua/freekill.lua"); - DoLuaScript(L, "lua/client/client.lua"); + L = CreateLuaState(); + DoLuaScript(L, "lua/freekill.lua"); + DoLuaScript(L, "lua/client/client.lua"); } Client::~Client() { - ClientInstance = nullptr; - lua_close(L); - router->getSocket()->disconnectFromHost(); - router->getSocket()->deleteLater(); + ClientInstance = nullptr; + lua_close(L); + router->getSocket()->disconnectFromHost(); + router->getSocket()->deleteLater(); } void Client::connectToHost(const QHostAddress& server, ushort port) { - router->getSocket()->connectToHost(server, port); + router->getSocket()->connectToHost(server, port); } void Client::replyToServer(const QString& command, const QString& jsonData) { - int type = Router::TYPE_REPLY | Router::SRC_CLIENT | Router::DEST_SERVER; - router->reply(type, command, jsonData); + int type = Router::TYPE_REPLY | Router::SRC_CLIENT | Router::DEST_SERVER; + router->reply(type, command, jsonData); } void Client::notifyServer(const QString& command, const QString& jsonData) { - int type = Router::TYPE_NOTIFICATION | Router::SRC_CLIENT | Router::DEST_SERVER; - router->notify(type, command, jsonData); + int type = Router::TYPE_NOTIFICATION | Router::SRC_CLIENT | Router::DEST_SERVER; + router->notify(type, command, jsonData); } ClientPlayer *Client::addPlayer(int id, const QString &name, const QString &avatar) { - ClientPlayer *player = new ClientPlayer(id); - player->setScreenName(name); - player->setAvatar(avatar); + ClientPlayer *player = new ClientPlayer(id); + player->setScreenName(name); + player->setAvatar(avatar); - players[id] = player; - return player; + players[id] = player; + return player; } void Client::removePlayer(int id) { - ClientPlayer *p = players[id]; - p->deleteLater(); - players[id] = nullptr; + ClientPlayer *p = players[id]; + p->deleteLater(); + players[id] = nullptr; } void Client::clearPlayers() { - players.clear(); + players.clear(); } lua_State *Client::getLuaState() { - return L; + return L; } diff --git a/src/client/client.h b/src/client/client.h index 307851dc..557a2398 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -6,33 +6,33 @@ #include "qmlbackend.h" class Client : public QObject { - Q_OBJECT + Q_OBJECT public: - Client(QObject *parent = nullptr); - ~Client(); + Client(QObject *parent = nullptr); + ~Client(); - void connectToHost(const QHostAddress &server, ushort port); + void connectToHost(const QHostAddress &server, ushort port); - Q_INVOKABLE void replyToServer(const QString &command, const QString &jsonData); - Q_INVOKABLE void notifyServer(const QString &command, const QString &jsonData); + Q_INVOKABLE void replyToServer(const QString &command, const QString &jsonData); + Q_INVOKABLE void notifyServer(const QString &command, const QString &jsonData); - Q_INVOKABLE void callLua(const QString &command, const QString &jsonData); - LuaFunction callback; + Q_INVOKABLE void callLua(const QString &command, const QString &jsonData); + LuaFunction callback; - ClientPlayer *addPlayer(int id, const QString &name, const QString &avatar); - void removePlayer(int id); - Q_INVOKABLE void clearPlayers(); + ClientPlayer *addPlayer(int id, const QString &name, const QString &avatar); + void removePlayer(int id); + Q_INVOKABLE void clearPlayers(); - lua_State *getLuaState(); + lua_State *getLuaState(); signals: - void error_message(const QString &msg); + void error_message(const QString &msg); private: - Router *router; - QMap players; + Router *router; + QMap players; - lua_State *L; + lua_State *L; }; extern Client *ClientInstance; diff --git a/src/client/clientplayer.cpp b/src/client/clientplayer.cpp index 07334f7f..f0142a33 100644 --- a/src/client/clientplayer.cpp +++ b/src/client/clientplayer.cpp @@ -1,9 +1,9 @@ #include "clientplayer.h" ClientPlayer::ClientPlayer(int id, QObject* parent) - : Player(parent) + : Player(parent) { - setId(id); + setId(id); } ClientPlayer::~ClientPlayer() diff --git a/src/client/clientplayer.h b/src/client/clientplayer.h index c0f8a663..a55a4ed7 100644 --- a/src/client/clientplayer.h +++ b/src/client/clientplayer.h @@ -4,23 +4,23 @@ #include "player.h" class ClientPlayer : public Player { - Q_OBJECT + Q_OBJECT - Q_PROPERTY(int id READ getId CONSTANT) - Q_PROPERTY(QString screenName - READ getScreenName - WRITE setScreenName - NOTIFY screenNameChanged - ) - Q_PROPERTY(QString avatar - READ getAvatar - WRITE setAvatar - NOTIFY avatarChanged - ) + Q_PROPERTY(int id READ getId CONSTANT) + Q_PROPERTY(QString screenName + READ getScreenName + WRITE setScreenName + NOTIFY screenNameChanged + ) + Q_PROPERTY(QString avatar + READ getAvatar + WRITE setAvatar + NOTIFY avatarChanged + ) public: - ClientPlayer(int id, QObject *parent = nullptr); - ~ClientPlayer(); + ClientPlayer(int id, QObject *parent = nullptr); + ~ClientPlayer(); private: }; diff --git a/src/core/player.cpp b/src/core/player.cpp index e77c5aa5..f4d46200 100644 --- a/src/core/player.cpp +++ b/src/core/player.cpp @@ -1,10 +1,10 @@ #include "player.h" Player::Player(QObject* parent) - : QObject(parent) - , id(0) - , state(Player::Invalid) - , ready(false) + : QObject(parent) + , id(0) + , state(Player::Invalid) + , ready(false) { } @@ -14,85 +14,85 @@ Player::~Player() int Player::getId() const { - return id; + return id; } void Player::setId(int id) { - this->id = id; + this->id = id; } QString Player::getScreenName() const { - return screenName; + return screenName; } void Player::setScreenName(const QString& name) { - this->screenName = name; - emit screenNameChanged(); + this->screenName = name; + emit screenNameChanged(); } QString Player::getAvatar() const { - return avatar; + return avatar; } void Player::setAvatar(const QString& avatar) { - this->avatar = avatar; - emit avatarChanged(); + this->avatar = avatar; + emit avatarChanged(); } Player::State Player::getState() const { - return state; + return state; } QString Player::getStateString() const { - switch (state) { - case Online: - return QStringLiteral("online"); - case Trust: - return QStringLiteral("trust"); - case Robot: - return QStringLiteral("robot"); - case Offline: - return QStringLiteral("offline"); - default: - return QStringLiteral("invalid"); - } + switch (state) { + case Online: + return QStringLiteral("online"); + case Trust: + return QStringLiteral("trust"); + case Robot: + return QStringLiteral("robot"); + case Offline: + return QStringLiteral("offline"); + default: + return QStringLiteral("invalid"); + } } void Player::setState(Player::State state) { - this->state = state; - emit stateChanged(); + this->state = state; + emit stateChanged(); } void Player::setStateString(const QString &state) { - if (state == QStringLiteral("online")) - setState(Online); - else if (state == QStringLiteral("trust")) - setState(Trust); - else if (state == QStringLiteral("robot")) - setState(Robot); - else if (state == QStringLiteral("offline")) - setState(Offline); - else - setState(Invalid); + if (state == QStringLiteral("online")) + setState(Online); + else if (state == QStringLiteral("trust")) + setState(Trust); + else if (state == QStringLiteral("robot")) + setState(Robot); + else if (state == QStringLiteral("offline")) + setState(Offline); + else + setState(Invalid); } bool Player::isReady() const { - return ready; + return ready; } void Player::setReady(bool ready) { - this->ready = ready; - emit readyChanged(); + this->ready = ready; + emit readyChanged(); } diff --git a/src/core/player.h b/src/core/player.h index b0cb0657..fa25a949 100644 --- a/src/core/player.h +++ b/src/core/player.h @@ -4,49 +4,49 @@ // Common part of ServerPlayer and ClientPlayer // dont initialize it directly class Player : public QObject { - Q_OBJECT + Q_OBJECT public: - enum State{ - Invalid, - Online, - Trust, // Trust or run - Robot, // only for real robot - Offline - }; + enum State{ + Invalid, + Online, + Trust, // Trust or run + Robot, // only for real robot + Offline + }; - explicit Player(QObject *parent = nullptr); - ~Player(); + explicit Player(QObject *parent = nullptr); + ~Player(); - int getId() const; - void setId(int id); + int getId() const; + void setId(int id); - QString getScreenName() const; - void setScreenName(const QString &name); + QString getScreenName() const; + void setScreenName(const QString &name); - QString getAvatar() const; - void setAvatar(const QString &avatar); + QString getAvatar() const; + void setAvatar(const QString &avatar); - State getState() const; - QString getStateString() const; - void setState(State state); - void setStateString(const QString &state); + State getState() const; + QString getStateString() const; + void setState(State state); + void setStateString(const QString &state); - bool isReady() const; - void setReady(bool ready); + bool isReady() const; + void setReady(bool ready); signals: - void screenNameChanged(); - void avatarChanged(); - void stateChanged(); - void readyChanged(); + void screenNameChanged(); + void avatarChanged(); + void stateChanged(); + void readyChanged(); private: - int id; - QString screenName; // screenName should not be same. - QString avatar; - State state; - bool ready; + int id; + QString screenName; // screenName should not be same. + QString avatar; + State state; + bool ready; }; #endif // _PLAYER_H diff --git a/src/core/util.cpp b/src/core/util.cpp index 9a63e2fe..9df01962 100644 --- a/src/core/util.cpp +++ b/src/core/util.cpp @@ -1,123 +1,123 @@ #include "util.h" extern "C" { - int luaopen_fk(lua_State *); + int luaopen_fk(lua_State *); } lua_State *CreateLuaState() { - lua_State *L = luaL_newstate(); - luaL_openlibs(L); - luaopen_fk(L); + lua_State *L = luaL_newstate(); + luaL_openlibs(L); + luaopen_fk(L); - return L; + return L; } bool DoLuaScript(lua_State *L, const char *script) { - lua_getglobal(L, "debug"); - lua_getfield(L, -1, "traceback"); - lua_replace(L, -2); + lua_getglobal(L, "debug"); + lua_getfield(L, -1, "traceback"); + lua_replace(L, -2); - luaL_loadfile(L, script); - int error = lua_pcall(L, 0, LUA_MULTRET, -2); + luaL_loadfile(L, script); + int error = lua_pcall(L, 0, LUA_MULTRET, -2); - if (error) { - const char *error_msg = lua_tostring(L, -1); - qDebug() << error_msg; - lua_pop(L, 2); - return false; - } - lua_pop(L, 1); - return true; + if (error) { + const char *error_msg = lua_tostring(L, -1); + qDebug() << error_msg; + lua_pop(L, 2); + return false; + } + lua_pop(L, 1); + return true; } // For Lua debugging void Dumpstack(lua_State *L) { - int top = lua_gettop(L); - for (int i = 1; i <= top; i++) { - printf("%d\t%s\t", i, luaL_typename(L, i)); - switch (lua_type(L, i)) { - case LUA_TNUMBER: - printf("%g\n",lua_tonumber(L, i)); - break; - case LUA_TSTRING: - printf("%s\n",lua_tostring(L, i)); - break; - case LUA_TBOOLEAN: - printf("%s\n", (lua_toboolean(L, i) ? "true" : "false")); - break; - case LUA_TNIL: - printf("%s\n", "nil"); - break; - default: - printf("%p\n",lua_topointer(L, i)); - break; - } + int top = lua_gettop(L); + for (int i = 1; i <= top; i++) { + printf("%d\t%s\t", i, luaL_typename(L, i)); + switch (lua_type(L, i)) { + case LUA_TNUMBER: + printf("%g\n",lua_tonumber(L, i)); + break; + case LUA_TSTRING: + printf("%s\n",lua_tostring(L, i)); + break; + case LUA_TBOOLEAN: + printf("%s\n", (lua_toboolean(L, i) ? "true" : "false")); + break; + case LUA_TNIL: + printf("%s\n", "nil"); + break; + default: + printf("%p\n",lua_topointer(L, i)); + break; } + } } sqlite3 *OpenDatabase(const QString &filename) { - sqlite3 *ret; - int rc; - if (!QFile::exists(filename)) { - QFile file("./server/init.sql"); - if (!file.open(QIODevice::ReadOnly)) { - qDebug() << "cannot open init.sql. Quit now."; - qApp->exit(1); - } - - QTextStream in(&file); - char *err_msg; - sqlite3_open(filename.toLatin1().data(), &ret); - rc = sqlite3_exec(ret, in.readAll().toLatin1().data(), nullptr, nullptr, &err_msg); - - if (rc != SQLITE_OK ) { - qDebug() << "sqlite error:" << err_msg; - sqlite3_free(err_msg); - sqlite3_close(ret); - qApp->exit(1); - } - } else { - rc = sqlite3_open(filename.toLatin1().data(), &ret); - if (rc != SQLITE_OK) { - qDebug() << "Cannot open database:" << sqlite3_errmsg(ret); - sqlite3_close(ret); - qApp->exit(1); - } + sqlite3 *ret; + int rc; + if (!QFile::exists(filename)) { + QFile file("./server/init.sql"); + if (!file.open(QIODevice::ReadOnly)) { + qDebug() << "cannot open init.sql. Quit now."; + qApp->exit(1); } - return ret; + + QTextStream in(&file); + char *err_msg; + sqlite3_open(filename.toLatin1().data(), &ret); + rc = sqlite3_exec(ret, in.readAll().toLatin1().data(), nullptr, nullptr, &err_msg); + + if (rc != SQLITE_OK ) { + qDebug() << "sqlite error:" << err_msg; + sqlite3_free(err_msg); + sqlite3_close(ret); + qApp->exit(1); + } + } else { + rc = sqlite3_open(filename.toLatin1().data(), &ret); + if (rc != SQLITE_OK) { + qDebug() << "Cannot open database:" << sqlite3_errmsg(ret); + sqlite3_close(ret); + qApp->exit(1); + } + } + return ret; } // callback for handling SELECT expression static int callback(void *jsonDoc, int argc, char **argv, char **cols) { - QJsonObject obj; - for (int i = 0; i < argc; i++) { - QJsonArray arr = obj[QString(cols[i])].toArray(); - arr << QString(argv[i] ? argv[i] : "#null"); - obj[QString(cols[i])] = arr; - } - ((QJsonObject *)jsonDoc)->swap(obj); - return 0; + QJsonObject obj; + for (int i = 0; i < argc; i++) { + QJsonArray arr = obj[QString(cols[i])].toArray(); + arr << QString(argv[i] ? argv[i] : "#null"); + obj[QString(cols[i])] = arr; + } + ((QJsonObject *)jsonDoc)->swap(obj); + return 0; } QJsonObject SelectFromDatabase(sqlite3 *db, const QString &sql) { - QJsonObject obj; - sqlite3_exec(db, sql.toUtf8().data(), callback, (void *)&obj, nullptr); - return obj; + QJsonObject obj; + sqlite3_exec(db, sql.toUtf8().data(), callback, (void *)&obj, nullptr); + return obj; } QString SelectFromDb(sqlite3 *db, const QString &sql) { - QJsonObject obj = SelectFromDatabase(db, sql); - return QJsonDocument(obj).toJson(); + QJsonObject obj = SelectFromDatabase(db, sql); + return QJsonDocument(obj).toJson(); } void ExecSQL(sqlite3 *db, const QString &sql) { - sqlite3_exec(db, sql.toUtf8().data(), nullptr, nullptr, nullptr); + sqlite3_exec(db, sql.toUtf8().data(), nullptr, nullptr, nullptr); } void CloseDatabase(sqlite3 *db) { - sqlite3_close(db); + sqlite3_close(db); } diff --git a/src/main.cpp b/src/main.cpp index db24b097..89062f1c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,59 +3,61 @@ int main(int argc, char *argv[]) { - QCoreApplication *app; - QCoreApplication::setApplicationName("FreeKill"); - QCoreApplication::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"}); - QStringList cliOptions; - for (int i = 0; i < argc; i++) - cliOptions << argv[i]; + QCommandLineParser parser; + parser.setApplicationDescription("FreeKill server"); + parser.addHelpOption(); + parser.addVersionOption(); + parser.addOption({{"s", "server"}, "start server at ", "port"}); + QStringList cliOptions; + for (int i = 0; i < argc; i++) + cliOptions << argv[i]; - parser.parse(cliOptions); + parser.parse(cliOptions); - bool startServer = parser.isSet("server"); - ushort serverPort = 9527; + 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); - } - return app->exec(); + 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); } + return app->exec(); + } - app = new QGuiApplication(argc, argv); + app = new QGuiApplication(argc, argv); - QQmlApplicationEngine *engine = new QQmlApplicationEngine; - - QmlBackend backend; - backend.setEngine(engine); - - engine->rootContext()->setContextProperty("Backend", &backend); - engine->rootContext()->setContextProperty("AppPath", QUrl::fromLocalFile(QDir::currentPath())); + QQmlApplicationEngine *engine = new QQmlApplicationEngine; + + QmlBackend backend; + backend.setEngine(engine); + + engine->rootContext()->setContextProperty("Backend", &backend); + engine->rootContext()->setContextProperty("AppPath", QUrl::fromLocalFile(QDir::currentPath())); #ifdef QT_DEBUG - bool debugging = true; + bool debugging = true; #else - bool debugging = false; + bool debugging = false; #endif - engine->rootContext()->setContextProperty("Debugging", debugging); - engine->load("qml/main.qml"); + engine->rootContext()->setContextProperty("Debugging", debugging); + engine->load("qml/main.qml"); + if (engine->rootObjects().isEmpty()) + return -1; - int ret = app->exec(); + int ret = app->exec(); - // delete the engine first - // to avoid "TypeError: Cannot read property 'xxx' of null" - delete engine; + // delete the engine first + // to avoid "TypeError: Cannot read property 'xxx' of null" + delete engine; - return ret; + return ret; } diff --git a/src/network/client_socket.cpp b/src/network/client_socket.cpp index 64cfb53a..e0ea33b6 100644 --- a/src/network/client_socket.cpp +++ b/src/network/client_socket.cpp @@ -2,94 +2,94 @@ ClientSocket::ClientSocket() : socket(new QTcpSocket(this)) { - init(); + init(); } ClientSocket::ClientSocket(QTcpSocket* socket) { - socket->setParent(this); - this->socket = socket; - timerSignup.setSingleShot(true); - connect(&timerSignup, &QTimer::timeout, this, &ClientSocket::disconnectFromHost); - init(); + socket->setParent(this); + this->socket = socket; + timerSignup.setSingleShot(true); + connect(&timerSignup, &QTimer::timeout, this, &ClientSocket::disconnectFromHost); + init(); } void ClientSocket::init() { - connect(socket, &QTcpSocket::connected, - this, &ClientSocket::connected); - connect(socket, &QTcpSocket::disconnected, - this, &ClientSocket::disconnected); - connect(socket, &QTcpSocket::readyRead, - this, &ClientSocket::getMessage); - connect(socket, &QTcpSocket::errorOccurred, - this, &ClientSocket::raiseError); + connect(socket, &QTcpSocket::connected, + this, &ClientSocket::connected); + connect(socket, &QTcpSocket::disconnected, + this, &ClientSocket::disconnected); + connect(socket, &QTcpSocket::readyRead, + this, &ClientSocket::getMessage); + connect(socket, &QTcpSocket::errorOccurred, + this, &ClientSocket::raiseError); } void ClientSocket::connectToHost(const QHostAddress &address, ushort port) { - socket->connectToHost(address, port); + socket->connectToHost(address, port); } void ClientSocket::getMessage() { - while (socket->canReadLine()) { - char msg[16000]; // buffer - socket->readLine(msg, sizeof(msg)); - emit message_got(msg); - } + while (socket->canReadLine()) { + char msg[16000]; // buffer + socket->readLine(msg, sizeof(msg)); + emit message_got(msg); + } } void ClientSocket::disconnectFromHost() { - socket->disconnectFromHost(); + socket->disconnectFromHost(); } void ClientSocket::send(const QByteArray &msg) { - socket->write(msg); - if (!msg.endsWith("\n")) - socket->write("\n"); - socket->flush(); + socket->write(msg); + if (!msg.endsWith("\n")) + socket->write("\n"); + socket->flush(); } bool ClientSocket::isConnected() const { - return socket->state() == QTcpSocket::ConnectedState; + return socket->state() == QTcpSocket::ConnectedState; } QString ClientSocket::peerName() const { - QString peer_name = socket->peerName(); - if (peer_name.isEmpty()) - peer_name = QString("%1:%2").arg(socket->peerAddress().toString()).arg(socket->peerPort()); + QString peer_name = socket->peerName(); + if (peer_name.isEmpty()) + peer_name = QString("%1:%2").arg(socket->peerAddress().toString()).arg(socket->peerPort()); - return peer_name; + return peer_name; } QString ClientSocket::peerAddress() const { - return socket->peerAddress().toString(); + return socket->peerAddress().toString(); } void ClientSocket::raiseError(QAbstractSocket::SocketError socket_error) { - // translate error message - QString reason; - switch (socket_error) { - case QAbstractSocket::ConnectionRefusedError: - reason = tr("Connection was refused or timeout"); break; - case QAbstractSocket::RemoteHostClosedError: - reason = tr("Remote host close this connection"); break; - case QAbstractSocket::HostNotFoundError: - reason = tr("Host not found"); break; - case QAbstractSocket::SocketAccessError: - reason = tr("Socket access error"); break; - case QAbstractSocket::NetworkError: - return; // this error is ignored ... - default: reason = tr("Unknow error"); break; - } + // translate error message + QString reason; + switch (socket_error) { + case QAbstractSocket::ConnectionRefusedError: + reason = tr("Connection was refused or timeout"); break; + case QAbstractSocket::RemoteHostClosedError: + reason = tr("Remote host close this connection"); break; + case QAbstractSocket::HostNotFoundError: + reason = tr("Host not found"); break; + case QAbstractSocket::SocketAccessError: + reason = tr("Socket access error"); break; + case QAbstractSocket::NetworkError: + return; // this error is ignored ... + default: reason = tr("Unknow error"); break; + } - emit error_message(tr("Connection failed, error code = %1\n reason: %2") - .arg(socket_error).arg(reason)); + emit error_message(tr("Connection failed, error code = %1\n reason: %2") + .arg(socket_error).arg(reason)); } diff --git a/src/network/client_socket.h b/src/network/client_socket.h index 3db14196..279bd85d 100644 --- a/src/network/client_socket.h +++ b/src/network/client_socket.h @@ -2,34 +2,34 @@ #define _CLIENT_SOCKET_H class ClientSocket : public QObject { - Q_OBJECT + Q_OBJECT public: - ClientSocket(); - // For server use - ClientSocket(QTcpSocket *socket); + ClientSocket(); + // For server use + ClientSocket(QTcpSocket *socket); - void connectToHost(const QHostAddress &address = QHostAddress::LocalHost, ushort port = 9527u); - void disconnectFromHost(); - void send(const QByteArray& msg); - bool isConnected() const; - QString peerName() const; - QString peerAddress() const; - QTimer timerSignup; + void connectToHost(const QHostAddress &address = QHostAddress::LocalHost, ushort port = 9527u); + void disconnectFromHost(); + void send(const QByteArray& msg); + bool isConnected() const; + QString peerName() const; + QString peerAddress() const; + QTimer timerSignup; signals: - void message_got(const QByteArray& msg); - void error_message(const QString &msg); - void disconnected(); - void connected(); + void message_got(const QByteArray& msg); + void error_message(const QString &msg); + void disconnected(); + void connected(); private slots: - void getMessage(); - void raiseError(QAbstractSocket::SocketError error); + void getMessage(); + void raiseError(QAbstractSocket::SocketError error); private: - QTcpSocket *socket; - void init(); + QTcpSocket *socket; + void init(); }; #endif // _CLIENT_SOCKET_H diff --git a/src/network/router.cpp b/src/network/router.cpp index ad3aeb3b..c7eb0696 100644 --- a/src/network/router.cpp +++ b/src/network/router.cpp @@ -5,200 +5,200 @@ #include "serverplayer.h" Router::Router(QObject *parent, ClientSocket *socket, RouterType type) - : QObject(parent) + : QObject(parent) { - this->type = type; - this->socket = nullptr; - setSocket(socket); - expectedReplyId = -1; - replyTimeout = 0; - extraReplyReadySemaphore = nullptr; + this->type = type; + this->socket = nullptr; + setSocket(socket); + expectedReplyId = -1; + replyTimeout = 0; + extraReplyReadySemaphore = nullptr; } Router::~Router() { - abortRequest(); + abortRequest(); } ClientSocket* Router::getSocket() const { - return socket; + return socket; } void Router::setSocket(ClientSocket *socket) { - if (this->socket != nullptr) { - this->socket->disconnect(this); - disconnect(this->socket); - this->socket->deleteLater(); - } + if (this->socket != nullptr) { + this->socket->disconnect(this); + disconnect(this->socket); + this->socket->deleteLater(); + } - this->socket = nullptr; - if (socket != nullptr) { - connect(this, &Router::messageReady, socket, &ClientSocket::send); - connect(socket, &ClientSocket::message_got, this, &Router::handlePacket); - connect(socket, &ClientSocket::disconnected, this, &Router::abortRequest); - socket->setParent(this); - this->socket = socket; - } + this->socket = nullptr; + if (socket != nullptr) { + connect(this, &Router::messageReady, socket, &ClientSocket::send); + connect(socket, &ClientSocket::message_got, this, &Router::handlePacket); + connect(socket, &ClientSocket::disconnected, this, &Router::abortRequest); + socket->setParent(this); + this->socket = socket; + } } void Router::setReplyReadySemaphore(QSemaphore *semaphore) { - extraReplyReadySemaphore = semaphore; + extraReplyReadySemaphore = semaphore; } void Router::request(int type, const QString& command, - const QString& jsonData, int timeout) + const QString& jsonData, int timeout) { - // In case a request is called without a following waitForReply call - if (replyReadySemaphore.available() > 0) - replyReadySemaphore.acquire(replyReadySemaphore.available()); + // In case a request is called without a following waitForReply call + if (replyReadySemaphore.available() > 0) + replyReadySemaphore.acquire(replyReadySemaphore.available()); - static int requestId = 0; - requestId++; + static int requestId = 0; + requestId++; - replyMutex.lock(); - expectedReplyId = requestId; - replyTimeout = timeout; - requestStartTime = QDateTime::currentDateTime(); - m_reply = QString(); - replyMutex.unlock(); + replyMutex.lock(); + expectedReplyId = requestId; + replyTimeout = timeout; + requestStartTime = QDateTime::currentDateTime(); + m_reply = QString(); + replyMutex.unlock(); - QJsonArray body; - body << requestId; - body << type; - body << command; - body << jsonData; - body << timeout; + QJsonArray body; + body << requestId; + body << type; + body << command; + body << jsonData; + body << timeout; - emit messageReady(QJsonDocument(body).toJson(QJsonDocument::Compact)); + emit messageReady(QJsonDocument(body).toJson(QJsonDocument::Compact)); } void Router::reply(int type, const QString& command, const QString& jsonData) { - QJsonArray body; - body << this->requestId; - body << type; - body << command; - body << jsonData; + QJsonArray body; + body << this->requestId; + body << type; + body << command; + body << jsonData; - emit messageReady(QJsonDocument(body).toJson(QJsonDocument::Compact)); + emit messageReady(QJsonDocument(body).toJson(QJsonDocument::Compact)); } void Router::notify(int type, const QString& command, const QString& jsonData) { - QJsonArray body; - body << -2; // requestId = -2 mean this is for notification - body << type; - body << command; - body << jsonData; + QJsonArray body; + body << -2; // requestId = -2 mean this is for notification + body << type; + body << command; + body << jsonData; - emit messageReady(QJsonDocument(body).toJson(QJsonDocument::Compact)); + emit messageReady(QJsonDocument(body).toJson(QJsonDocument::Compact)); } int Router::getTimeout() const { - return requestTimeout; + return requestTimeout; } // cancel last request from the sender void Router::cancelRequest() { - replyMutex.lock(); - expectedReplyId = -1; - replyTimeout = 0; - extraReplyReadySemaphore = nullptr; - replyMutex.unlock(); + replyMutex.lock(); + expectedReplyId = -1; + replyTimeout = 0; + extraReplyReadySemaphore = nullptr; + replyMutex.unlock(); - if (replyReadySemaphore.available() > 0) - replyReadySemaphore.acquire(replyReadySemaphore.available()); + if (replyReadySemaphore.available() > 0) + replyReadySemaphore.acquire(replyReadySemaphore.available()); } QString Router::waitForReply() { - replyReadySemaphore.acquire(); - return m_reply; + replyReadySemaphore.acquire(); + return m_reply; } QString Router::waitForReply(int timeout) { - replyReadySemaphore.tryAcquire(1, timeout * 1000); - return m_reply; + replyReadySemaphore.tryAcquire(1, timeout * 1000); + return m_reply; } void Router::abortRequest() { - replyMutex.lock(); - if (expectedReplyId != -1) { - replyReadySemaphore.release(); - if (extraReplyReadySemaphore) - extraReplyReadySemaphore->release(); - expectedReplyId = -1; - extraReplyReadySemaphore = nullptr; - } - replyMutex.unlock(); + replyMutex.lock(); + if (expectedReplyId != -1) { + replyReadySemaphore.release(); + if (extraReplyReadySemaphore) + extraReplyReadySemaphore->release(); + expectedReplyId = -1; + extraReplyReadySemaphore = nullptr; + } + replyMutex.unlock(); } void Router::handlePacket(const QByteArray& rawPacket) { - QJsonDocument packet = QJsonDocument::fromJson(rawPacket); - if (packet.isNull() || !packet.isArray()) - return; + QJsonDocument packet = QJsonDocument::fromJson(rawPacket); + if (packet.isNull() || !packet.isArray()) + return; - int requestId = packet[0].toInt(); - int type = packet[1].toInt(); - QString command = packet[2].toString(); - QString jsonData = packet[3].toString(); + int requestId = packet[0].toInt(); + int type = packet[1].toInt(); + QString command = packet[2].toString(); + QString jsonData = packet[3].toString(); - if (type & TYPE_NOTIFICATION) { - if (type & DEST_CLIENT) { - ClientInstance->callLua(command, jsonData); - } else { - ServerPlayer *player = qobject_cast(parent()); - // Add the uid of sender to jsonData - QJsonArray arr = QJsonDocument::fromJson(jsonData.toUtf8()).array(); - arr.prepend(player->getId()); + if (type & TYPE_NOTIFICATION) { + if (type & DEST_CLIENT) { + ClientInstance->callLua(command, jsonData); + } else { + ServerPlayer *player = qobject_cast(parent()); + // Add the uid of sender to jsonData + QJsonArray arr = QJsonDocument::fromJson(jsonData.toUtf8()).array(); + arr.prepend(player->getId()); - Room *room = player->getRoom(); - room->lockLua(__FUNCTION__); - room->callLua(command, QJsonDocument(arr).toJson()); - room->unlockLua(__FUNCTION__); - } + Room *room = player->getRoom(); + room->lockLua(__FUNCTION__); + room->callLua(command, QJsonDocument(arr).toJson()); + room->unlockLua(__FUNCTION__); } - else if (type & TYPE_REQUEST) { - this->requestId = requestId; - this->requestTimeout = packet[4].toInt(); + } + else if (type & TYPE_REQUEST) { + this->requestId = requestId; + this->requestTimeout = packet[4].toInt(); - if (type & DEST_CLIENT) { - qobject_cast(parent())->callLua(command, jsonData); - } else { - // requesting server is not allowed - Q_ASSERT(false); - } + if (type & DEST_CLIENT) { + qobject_cast(parent())->callLua(command, jsonData); + } else { + // requesting server is not allowed + Q_ASSERT(false); } - else if (type & TYPE_REPLY) { - QMutexLocker locker(&replyMutex); + } + else if (type & TYPE_REPLY) { + QMutexLocker locker(&replyMutex); - if (requestId != this->expectedReplyId) - return; + if (requestId != this->expectedReplyId) + return; - this->expectedReplyId = -1; + this->expectedReplyId = -1; - if (replyTimeout >= 0 && replyTimeout < - requestStartTime.secsTo(QDateTime::currentDateTime())) - return; + if (replyTimeout >= 0 && replyTimeout < + requestStartTime.secsTo(QDateTime::currentDateTime())) + return; - m_reply = jsonData; - // TODO: callback? + m_reply = jsonData; + // TODO: callback? - replyReadySemaphore.release(); - if (extraReplyReadySemaphore) { - extraReplyReadySemaphore->release(); - extraReplyReadySemaphore = nullptr; - } - locker.unlock(); - emit replyReady(); + replyReadySemaphore.release(); + if (extraReplyReadySemaphore) { + extraReplyReadySemaphore->release(); + extraReplyReadySemaphore = nullptr; } + locker.unlock(); + emit replyReady(); + } } diff --git a/src/network/router.h b/src/network/router.h index f76db1c1..406d81cc 100644 --- a/src/network/router.h +++ b/src/network/router.h @@ -4,75 +4,75 @@ class ClientSocket; class Router : public QObject { - Q_OBJECT + Q_OBJECT public: - enum PacketType { - TYPE_REQUEST = 0x100, - TYPE_REPLY = 0x200, - TYPE_NOTIFICATION = 0x400, - SRC_CLIENT = 0x010, - SRC_SERVER = 0x020, - SRC_LOBBY = 0x040, - DEST_CLIENT = 0x001, - DEST_SERVER = 0x002, - DEST_LOBBY = 0x004 - }; + enum PacketType { + TYPE_REQUEST = 0x100, + TYPE_REPLY = 0x200, + TYPE_NOTIFICATION = 0x400, + SRC_CLIENT = 0x010, + SRC_SERVER = 0x020, + SRC_LOBBY = 0x040, + DEST_CLIENT = 0x001, + DEST_SERVER = 0x002, + DEST_LOBBY = 0x004 + }; - enum RouterType { - TYPE_SERVER, - TYPE_CLIENT - }; - Router(QObject *parent, ClientSocket *socket, RouterType type); - ~Router(); + enum RouterType { + TYPE_SERVER, + TYPE_CLIENT + }; + Router(QObject *parent, ClientSocket *socket, RouterType type); + ~Router(); - ClientSocket *getSocket() const; - void setSocket(ClientSocket *socket); + ClientSocket *getSocket() const; + void setSocket(ClientSocket *socket); - void setReplyReadySemaphore(QSemaphore *semaphore); + void setReplyReadySemaphore(QSemaphore *semaphore); - void request(int type, const QString &command, - const QString &jsonData, int timeout); - void reply(int type, const QString &command, const QString &jsonData); - void notify(int type, const QString &command, const QString &jsonData); + void request(int type, const QString &command, + const QString &jsonData, int timeout); + void reply(int type, const QString &command, const QString &jsonData); + void notify(int type, const QString &command, const QString &jsonData); - int getTimeout() const; + int getTimeout() const; - void cancelRequest(); - void abortRequest(); + void cancelRequest(); + void abortRequest(); - QString waitForReply(); - QString waitForReply(int timeout); + QString waitForReply(); + QString waitForReply(int timeout); signals: - void messageReady(const QByteArray &message); - void unknownPacket(const QByteArray &packet); - void replyReady(); + void messageReady(const QByteArray &message); + void unknownPacket(const QByteArray &packet); + void replyReady(); protected: - void handlePacket(const QByteArray &rawPacket); + void handlePacket(const QByteArray &rawPacket); private: - ClientSocket *socket; - RouterType type; + ClientSocket *socket; + RouterType type; - // For sender - int requestId; - int requestTimeout; + // For sender + int requestId; + int requestTimeout; - // For receiver - QDateTime requestStartTime; - QMutex replyMutex; - int expectedReplyId; - int replyTimeout; - QString m_reply; // should be json string - QSemaphore replyReadySemaphore; - QSemaphore *extraReplyReadySemaphore; + // For receiver + QDateTime requestStartTime; + QMutex replyMutex; + int expectedReplyId; + int replyTimeout; + QString m_reply; // should be json string + QSemaphore replyReadySemaphore; + QSemaphore *extraReplyReadySemaphore; - // Two Lua global table for callbacks and interactions - // stored in the lua_State of the sender - // LuaTable interactions; - // LuaTable callbacks; + // Two Lua global table for callbacks and interactions + // stored in the lua_State of the sender + // LuaTable interactions; + // LuaTable callbacks; }; #endif // _ROUTER_H diff --git a/src/network/server_socket.cpp b/src/network/server_socket.cpp index cf401a02..cd346a3b 100644 --- a/src/network/server_socket.cpp +++ b/src/network/server_socket.cpp @@ -3,23 +3,23 @@ ServerSocket::ServerSocket() { - server = new QTcpServer(this); - connect(server, &QTcpServer::newConnection, - this, &ServerSocket::processNewConnection); + server = new QTcpServer(this); + connect(server, &QTcpServer::newConnection, + this, &ServerSocket::processNewConnection); } bool ServerSocket::listen(const QHostAddress &address, ushort port) { - return server->listen(address, port); + return server->listen(address, port); } void ServerSocket::processNewConnection() { - QTcpSocket *socket = server->nextPendingConnection(); - ClientSocket *connection = new ClientSocket(socket); - connect(connection, &ClientSocket::disconnected, this, [connection](){ - connection->deleteLater(); - }); - emit new_connection(connection); + QTcpSocket *socket = server->nextPendingConnection(); + ClientSocket *connection = new ClientSocket(socket); + connect(connection, &ClientSocket::disconnected, this, [connection](){ + connection->deleteLater(); + }); + emit new_connection(connection); } diff --git a/src/network/server_socket.h b/src/network/server_socket.h index 03c52312..a3a8e8c9 100644 --- a/src/network/server_socket.h +++ b/src/network/server_socket.h @@ -4,21 +4,21 @@ class ClientSocket; class ServerSocket : public QObject { - Q_OBJECT + Q_OBJECT public: - ServerSocket(); + ServerSocket(); - bool listen(const QHostAddress &address = QHostAddress::Any, ushort port = 9527u); + bool listen(const QHostAddress &address = QHostAddress::Any, ushort port = 9527u); signals: - void new_connection(ClientSocket *socket); + void new_connection(ClientSocket *socket); private slots: - void processNewConnection(); + void processNewConnection(); private: - QTcpServer *server; + QTcpServer *server; }; #endif // _SERVER_SOCKET_H diff --git a/src/resources.qrc b/src/resources.qrc deleted file mode 100644 index 7c75584c..00000000 --- a/src/resources.qrc +++ /dev/null @@ -1,5 +0,0 @@ - - - qml/main.qml - - diff --git a/src/server/room.cpp b/src/server/room.cpp index 21b01cb4..64f89e0a 100644 --- a/src/server/room.cpp +++ b/src/server/room.cpp @@ -5,306 +5,306 @@ Room::Room(Server* server) { - id = server->nextRoomId; - server->nextRoomId++; - this->server = server; - setParent(server); - owner = nullptr; - gameStarted = false; - robot_id = -1; - timeout = 15; - if (!isLobby()) { - connect(this, &Room::playerAdded, server->lobby(), &Room::removePlayer); - connect(this, &Room::playerRemoved, server->lobby(), &Room::addPlayer); - } + id = server->nextRoomId; + server->nextRoomId++; + this->server = server; + setParent(server); + owner = nullptr; + gameStarted = false; + robot_id = -1; + timeout = 15; + if (!isLobby()) { + connect(this, &Room::playerAdded, server->lobby(), &Room::removePlayer); + connect(this, &Room::playerRemoved, server->lobby(), &Room::addPlayer); + } - L = CreateLuaState(); - DoLuaScript(L, "lua/freekill.lua"); - if (isLobby()) { - DoLuaScript(L, "lua/server/lobby.lua"); - } else { - DoLuaScript(L, "lua/server/room.lua"); - } - initLua(); + L = CreateLuaState(); + DoLuaScript(L, "lua/freekill.lua"); + if (isLobby()) { + DoLuaScript(L, "lua/server/lobby.lua"); + } else { + DoLuaScript(L, "lua/server/room.lua"); + } + initLua(); } Room::~Room() { - // TODO - if (isRunning()) { - callLua("RoomDeleted", ""); - unlockLua(__FUNCTION__); - wait(); - } - lua_close(L); + // TODO + if (isRunning()) { + callLua("RoomDeleted", ""); + unlockLua(__FUNCTION__); + wait(); + } + lua_close(L); } Server *Room::getServer() const { - return server; + return server; } int Room::getId() const { - return id; + return id; } bool Room::isLobby() const { - return id == 0; + return id == 0; } QString Room::getName() const { - return name; + return name; } void Room::setName(const QString &name) { - this->name = name; + this->name = name; } int Room::getCapacity() const { - return capacity; + return capacity; } void Room::setCapacity(int capacity) { - this->capacity = capacity; + this->capacity = capacity; } bool Room::isFull() const { - return players.count() == capacity; + return players.count() == capacity; } bool Room::isAbandoned() const { - if (players.isEmpty()) - return true; - - foreach (ServerPlayer *p, players) { - if (p->getState() == Player::Online) - return false; - } + if (players.isEmpty()) return true; + + foreach (ServerPlayer *p, players) { + if (p->getState() == Player::Online) + return false; + } + return true; } ServerPlayer *Room::getOwner() const { - return owner; + return owner; } void Room::setOwner(ServerPlayer *owner) { - this->owner = owner; - QJsonArray jsonData; - jsonData << owner->getId(); - doBroadcastNotify(players, "RoomOwner", QJsonDocument(jsonData).toJson()); + this->owner = owner; + QJsonArray jsonData; + jsonData << owner->getId(); + doBroadcastNotify(players, "RoomOwner", QJsonDocument(jsonData).toJson()); } void Room::addPlayer(ServerPlayer *player) { - if (!player) return; + if (!player) return; - if (isFull() || gameStarted) { - player->doNotify("ErrorMsg", "Room is full or already started!"); - if (runned_players.contains(player->getId())) { - player->doNotify("ErrorMsg", "Running away is shameful."); - } - return; + if (isFull() || gameStarted) { + player->doNotify("ErrorMsg", "Room is full or already started!"); + if (runned_players.contains(player->getId())) { + player->doNotify("ErrorMsg", "Running away is shameful."); + } + return; + } + + QJsonArray jsonData; + + // First, notify other players the new player is entering + if (!isLobby()) { + jsonData << player->getId(); + jsonData << player->getScreenName(); + jsonData << player->getAvatar(); + doBroadcastNotify(getPlayers(), "AddPlayer", QJsonDocument(jsonData).toJson()); + } + + players.append(player); + player->setRoom(this); + if (isLobby()) { + player->doNotify("EnterLobby", "[]"); + } else { + // 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)) { + jsonData = QJsonArray(); + jsonData << p->getId(); + jsonData << p->getScreenName(); + jsonData << p->getAvatar(); + player->doNotify("AddPlayer", QJsonDocument(jsonData).toJson()); } - QJsonArray jsonData; - - // First, notify other players the new player is entering - if (!isLobby()) { - jsonData << player->getId(); - jsonData << player->getScreenName(); - jsonData << player->getAvatar(); - doBroadcastNotify(getPlayers(), "AddPlayer", QJsonDocument(jsonData).toJson()); + if (this->owner != nullptr) { + jsonData = QJsonArray(); + jsonData << this->owner->getId(); + player->doNotify("RoomOwner", QJsonDocument(jsonData).toJson()); } - players.append(player); - player->setRoom(this); - if (isLobby()) { - player->doNotify("EnterLobby", "[]"); - } else { - // 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)) { - jsonData = QJsonArray(); - jsonData << p->getId(); - jsonData << p->getScreenName(); - jsonData << p->getAvatar(); - player->doNotify("AddPlayer", QJsonDocument(jsonData).toJson()); - } - - if (this->owner != nullptr) { - jsonData = QJsonArray(); - jsonData << this->owner->getId(); - player->doNotify("RoomOwner", QJsonDocument(jsonData).toJson()); - } - - if (isFull() && !gameStarted) - start(); - } - emit playerAdded(player); + if (isFull() && !gameStarted) + start(); + } + emit playerAdded(player); } void Room::addRobot(ServerPlayer *player) { - if (player != owner || isFull()) return; + if (player != owner || isFull()) return; - ServerPlayer *robot = new ServerPlayer(this); - robot->setState(Player::Robot); - robot->setId(robot_id); - robot->setAvatar("guanyu"); - robot->setScreenName(QString("COMP-%1").arg(robot_id)); - robot_id--; + ServerPlayer *robot = new ServerPlayer(this); + robot->setState(Player::Robot); + robot->setId(robot_id); + robot->setAvatar("guanyu"); + robot->setScreenName(QString("COMP-%1").arg(robot_id)); + robot_id--; - addPlayer(robot); + addPlayer(robot); } void Room::removePlayer(ServerPlayer *player) { - players.removeOne(player); - emit playerRemoved(player); + players.removeOne(player); + emit playerRemoved(player); - if (isLobby()) return; + if (isLobby()) return; - if (gameStarted) { - // TODO: if the player is died.. + if (gameStarted) { + // TODO: if the player is died.. - // create robot first - ServerPlayer *robot = new ServerPlayer(this); - robot->setState(Player::Robot); - robot->setId(robot_id); - robot->setAvatar(player->getAvatar()); - robot->setScreenName(QString("COMP-%1").arg(robot_id)); - robot_id--; + // create robot first + ServerPlayer *robot = new ServerPlayer(this); + robot->setState(Player::Robot); + robot->setId(robot_id); + robot->setAvatar(player->getAvatar()); + robot->setScreenName(QString("COMP-%1").arg(robot_id)); + robot_id--; - players.append(robot); + players.append(robot); - // tell lua & clients - QJsonArray jsonData; - jsonData << player->getId(); - jsonData << robot->getId(); - callLua("PlayerRunned", QJsonDocument(jsonData).toJson()); - doBroadcastNotify(getPlayers(), "PlayerRunned", QJsonDocument(jsonData).toJson()); - runned_players << player->getId(); + // tell lua & clients + QJsonArray jsonData; + jsonData << player->getId(); + jsonData << robot->getId(); + callLua("PlayerRunned", QJsonDocument(jsonData).toJson()); + doBroadcastNotify(getPlayers(), "PlayerRunned", QJsonDocument(jsonData).toJson()); + runned_players << player->getId(); - // FIXME: abortRequest here will result crash - // but if dont abort and room is abandoned, the main thread will wait until replyed - // player->abortRequest(); - } else { - QJsonArray jsonData; - jsonData << player->getId(); - doBroadcastNotify(getPlayers(), "RemovePlayer", QJsonDocument(jsonData).toJson()); - } + // FIXME: abortRequest here will result crash + // but if dont abort and room is abandoned, the main thread will wait until replyed + // player->abortRequest(); + } else { + QJsonArray jsonData; + jsonData << player->getId(); + doBroadcastNotify(getPlayers(), "RemovePlayer", QJsonDocument(jsonData).toJson()); + } - if (isAbandoned()) { - // FIXME: do not delete room here - // create a new thread and delete the room - emit abandoned(); - } else if (player == owner) { - setOwner(players.first()); - } + if (isAbandoned()) { + // FIXME: do not delete room here + // create a new thread and delete the room + emit abandoned(); + } else if (player == owner) { + setOwner(players.first()); + } } QList Room::getPlayers() const { - return players; + return players; } QList Room::getOtherPlayers(ServerPlayer* expect) const { - QList others = getPlayers(); - others.removeOne(expect); - return others; + QList others = getPlayers(); + others.removeOne(expect); + return others; } ServerPlayer *Room::findPlayer(int id) const { - foreach (ServerPlayer *p, players) { - if (p->getId() == id) - return p; - } - return nullptr; + foreach (ServerPlayer *p, players) { + if (p->getId() == id) + return p; + } + return nullptr; } int Room::getTimeout() const { - return timeout; + return timeout; } void Room::setTimeout(int timeout) { - this->timeout = timeout; + this->timeout = timeout; } bool Room::isStarted() const { - return gameStarted; + return gameStarted; } void Room::doRequest(const QList targets, int timeout) { - // TODO + // TODO } void Room::doNotify(const QList targets, int timeout) { - // TODO + // TODO } void Room::doBroadcastNotify(const QList targets, - const QString& command, const QString& jsonData) + const QString& command, const QString& jsonData) { - foreach (ServerPlayer *p, targets) { - p->doNotify(command, jsonData); - } + foreach (ServerPlayer *p, targets) { + p->doNotify(command, jsonData); + } } void Room::gameOver() { - gameStarted = false; - runned_players.clear(); - // clean not online players - foreach (ServerPlayer *p, players) { - if (p->getState() != Player::Online) { - p->deleteLater(); - } + gameStarted = false; + runned_players.clear(); + // clean not online players + foreach (ServerPlayer *p, players) { + if (p->getState() != Player::Online) { + p->deleteLater(); } + } } void Room::lockLua(const QString &caller) { - if (!gameStarted) return; - lua_mutex.lock(); + if (!gameStarted) return; + lua_mutex.lock(); #ifdef QT_DEBUG - //qDebug() << caller << "=> room->L is locked."; + //qDebug() << caller << "=> room->L is locked."; #endif } void Room::unlockLua(const QString &caller) { - if (!gameStarted) return; - lua_mutex.unlock(); + if (!gameStarted) return; + lua_mutex.unlock(); #ifdef QT_DEBUG - //qDebug() << caller << "=> room->L is unlocked."; + //qDebug() << caller << "=> room->L is unlocked."; #endif } void Room::run() { - gameStarted = true; - lockLua(__FUNCTION__); - roomStart(); - unlockLua(__FUNCTION__); + gameStarted = true; + lockLua(__FUNCTION__); + roomStart(); + unlockLua(__FUNCTION__); } diff --git a/src/server/room.h b/src/server/room.h index 5ac23691..9d05d3fa 100644 --- a/src/server/room.h +++ b/src/server/room.h @@ -5,86 +5,86 @@ class Server; class ServerPlayer; class Room : public QThread { - Q_OBJECT + Q_OBJECT public: - explicit Room(Server *m_server); - ~Room(); + explicit Room(Server *m_server); + ~Room(); - // Property reader & setter - // ==================================={ - Server *getServer() const; - int getId() const; - bool isLobby() const; - QString getName() const; - void setName(const QString &name); - int getCapacity() const; - void setCapacity(int capacity); - bool isFull() const; - bool isAbandoned() const; + // Property reader & setter + // ==================================={ + Server *getServer() const; + int getId() const; + bool isLobby() const; + QString getName() const; + void setName(const QString &name); + int getCapacity() const; + void setCapacity(int capacity); + bool isFull() const; + bool isAbandoned() const; - ServerPlayer *getOwner() const; - void setOwner(ServerPlayer *owner); + ServerPlayer *getOwner() const; + void setOwner(ServerPlayer *owner); - void addPlayer(ServerPlayer *player); - void addRobot(ServerPlayer *player); - void removePlayer(ServerPlayer *player); - QList getPlayers() const; - QList getOtherPlayers(ServerPlayer *expect) const; - ServerPlayer *findPlayer(int id) const; + void addPlayer(ServerPlayer *player); + void addRobot(ServerPlayer *player); + void removePlayer(ServerPlayer *player); + QList getPlayers() const; + QList getOtherPlayers(ServerPlayer *expect) const; + ServerPlayer *findPlayer(int id) const; - int getTimeout() const; - void setTimeout(int timeout); + int getTimeout() const; + void setTimeout(int timeout); - bool isStarted() const; - // ====================================} + bool isStarted() const; + // ====================================} - void doRequest(const QList targets, int timeout); - void doNotify(const QList targets, int timeout); + void doRequest(const QList targets, int timeout); + void doNotify(const QList targets, int timeout); - void doBroadcastNotify( - const QList targets, - const QString &command, - const QString &jsonData - ); + void doBroadcastNotify( + const QList targets, + const QString &command, + const QString &jsonData + ); - void gameOver(); + void gameOver(); - void initLua(); - void callLua(const QString &command, const QString &jsonData); - LuaFunction callback; + void initLua(); + void callLua(const QString &command, const QString &jsonData); + LuaFunction callback; - void roomStart(); - LuaFunction startGame; + void roomStart(); + LuaFunction startGame; - void lockLua(const QString &caller); - void unlockLua(const QString &caller); + void lockLua(const QString &caller); + void unlockLua(const QString &caller); signals: - void abandoned(); + void abandoned(); - void playerAdded(ServerPlayer *player); - void playerRemoved(ServerPlayer *player); + void playerAdded(ServerPlayer *player); + void playerRemoved(ServerPlayer *player); protected: - virtual void run(); + virtual void run(); private: - Server *server; - int id; // Lobby's id is 0 - QString name; // “阴间大乱斗” - int capacity; // by default is 5, max is 8 - bool m_abandoned; // If room is empty, delete it + Server *server; + int id; // Lobby's id is 0 + QString name; // “阴间大乱斗” + int capacity; // by default is 5, max is 8 + bool m_abandoned; // If room is empty, delete it - ServerPlayer *owner; // who created this room? - QList players; - QList runned_players; - int robot_id; - bool gameStarted; + ServerPlayer *owner; // who created this room? + QList players; + QList runned_players; + int robot_id; + bool gameStarted; - int timeout; + int timeout; - lua_State *L; - QMutex lua_mutex; + lua_State *L; + QMutex lua_mutex; }; #endif // _ROOM_H diff --git a/src/server/server.cpp b/src/server/server.cpp index eea3c2a4..8b451f64 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -9,253 +9,253 @@ Server *ServerInstance; Server::Server(QObject* parent) - : QObject(parent) + : QObject(parent) { - ServerInstance = this; - db = OpenDatabase(); - server = new ServerSocket(); - server->setParent(this); - connect(server, &ServerSocket::new_connection, - this, &Server::processNewConnection); + ServerInstance = this; + db = OpenDatabase(); + server = new ServerSocket(); + server->setParent(this); + connect(server, &ServerSocket::new_connection, + this, &Server::processNewConnection); - // create lobby - nextRoomId = 0; - createRoom(nullptr, "Lobby", INT32_MAX); - connect(lobby(), &Room::playerAdded, this, &Server::updateRoomList); - connect(lobby(), &Room::playerRemoved, this, &Server::updateRoomList); + // create lobby + nextRoomId = 0; + createRoom(nullptr, "Lobby", INT32_MAX); + connect(lobby(), &Room::playerAdded, this, &Server::updateRoomList); + connect(lobby(), &Room::playerRemoved, this, &Server::updateRoomList); } Server::~Server() { - ServerInstance = nullptr; - m_lobby->deleteLater(); - sqlite3_close(db); + ServerInstance = nullptr; + m_lobby->deleteLater(); + sqlite3_close(db); } bool Server::listen(const QHostAddress& address, ushort port) { - return server->listen(address, port); + return server->listen(address, port); } void Server::createRoom(ServerPlayer* owner, const QString &name, int capacity) { - Room *room = new Room(this); - connect(room, &Room::abandoned, this, &Server::onRoomAbandoned); - if (room->isLobby()) - m_lobby = room; - else - rooms.insert(room->getId(), room); + Room *room = new Room(this); + connect(room, &Room::abandoned, this, &Server::onRoomAbandoned); + if (room->isLobby()) + m_lobby = room; + else + rooms.insert(room->getId(), room); - room->setName(name); - room->setCapacity(capacity); - room->addPlayer(owner); - if (!room->isLobby()) room->setOwner(owner); + room->setName(name); + room->setCapacity(capacity); + room->addPlayer(owner); + if (!room->isLobby()) room->setOwner(owner); } Room *Server::findRoom(int id) const { - return rooms.value(id); + return rooms.value(id); } Room *Server::lobby() const { - return m_lobby; + return m_lobby; } ServerPlayer *Server::findPlayer(int id) const { - return players.value(id); + return players.value(id); } void Server::removePlayer(int id) { - players.remove(id); + players.remove(id); } void Server::updateRoomList() { - QJsonArray arr; - foreach (Room *room, rooms) { - QJsonArray obj; - obj << room->getId(); // roomId - obj << room->getName(); // roomName - obj << "Role"; // gameMode - obj << room->getPlayers().count(); // playerNum - obj << room->getCapacity(); // capacity - arr << obj; - } - lobby()->doBroadcastNotify( - lobby()->getPlayers(), - "UpdateRoomList", - QJsonDocument(arr).toJson() - ); + QJsonArray arr; + foreach (Room *room, rooms) { + QJsonArray obj; + obj << room->getId(); // roomId + obj << room->getName(); // roomName + obj << "Role"; // gameMode + obj << room->getPlayers().count(); // playerNum + obj << room->getCapacity(); // capacity + arr << obj; + } + lobby()->doBroadcastNotify( + lobby()->getPlayers(), + "UpdateRoomList", + QJsonDocument(arr).toJson() + ); } sqlite3 *Server::getDatabase() { - return db; + return db; } void Server::processNewConnection(ClientSocket* client) { - qDebug() << client->peerAddress() << "connected"; - // version check, file check, ban IP, reconnect, etc + qDebug() << client->peerAddress() << "connected"; + // version check, file check, ban IP, reconnect, etc - connect(client, &ClientSocket::disconnected, this, [client](){ - qDebug() << client->peerAddress() << "disconnected"; - }); + connect(client, &ClientSocket::disconnected, this, [client](){ + qDebug() << client->peerAddress() << "disconnected"; + }); - // network delay test - QJsonArray body; - body << -2; - body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER | Router::DEST_CLIENT); - body << "NetworkDelayTest"; - body << "[]"; - client->send(QJsonDocument(body).toJson(QJsonDocument::Compact)); - // Note: the client should send a setup string next - connect(client, &ClientSocket::message_got, this, &Server::processRequest); - client->timerSignup.start(30000); + // network delay test + QJsonArray body; + body << -2; + body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER | Router::DEST_CLIENT); + body << "NetworkDelayTest"; + body << "[]"; + client->send(QJsonDocument(body).toJson(QJsonDocument::Compact)); + // Note: the client should send a setup string next + connect(client, &ClientSocket::message_got, this, &Server::processRequest); + client->timerSignup.start(30000); } void Server::processRequest(const QByteArray& msg) { - ClientSocket *client = qobject_cast(sender()); - client->disconnect(this, SLOT(processRequest(const QByteArray &))); - client->timerSignup.stop(); + ClientSocket *client = qobject_cast(sender()); + client->disconnect(this, SLOT(processRequest(const QByteArray &))); + client->timerSignup.stop(); - bool valid = true; - QJsonDocument doc = QJsonDocument::fromJson(msg); - if (doc.isNull() || !doc.isArray()) { - valid = false; - } else { - if (doc.array().size() != 4 - || doc[0] != -2 - || doc[1] != (Router::TYPE_NOTIFICATION | Router::SRC_CLIENT | Router::DEST_SERVER) - || doc[2] != "Setup" - ) - valid = false; - else - valid = (QJsonDocument::fromJson(doc[3].toString().toUtf8()).array().size() == 2); - } + bool valid = true; + QJsonDocument doc = QJsonDocument::fromJson(msg); + if (doc.isNull() || !doc.isArray()) { + valid = false; + } else { + if (doc.array().size() != 4 + || doc[0] != -2 + || doc[1] != (Router::TYPE_NOTIFICATION | Router::SRC_CLIENT | Router::DEST_SERVER) + || doc[2] != "Setup" + ) + valid = false; + else + valid = (QJsonDocument::fromJson(doc[3].toString().toUtf8()).array().size() == 2); + } - if (!valid) { - qDebug() << "Invalid setup string:" << msg; - QJsonArray body; - body << -2; - body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER | Router::DEST_CLIENT); - body << "ErrorMsg"; - body << "INVALID SETUP STRING"; - client->send(QJsonDocument(body).toJson(QJsonDocument::Compact)); - client->disconnectFromHost(); - return; - } + if (!valid) { + qDebug() << "Invalid setup string:" << msg; + QJsonArray body; + body << -2; + body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER | Router::DEST_CLIENT); + body << "ErrorMsg"; + body << "INVALID SETUP STRING"; + client->send(QJsonDocument(body).toJson(QJsonDocument::Compact)); + client->disconnectFromHost(); + return; + } - QJsonArray arr = QJsonDocument::fromJson(doc[3].toString().toUtf8()).array(); - handleNameAndPassword(client, arr[0].toString(), arr[1].toString()); + QJsonArray arr = QJsonDocument::fromJson(doc[3].toString().toUtf8()).array(); + handleNameAndPassword(client, arr[0].toString(), arr[1].toString()); } void Server::handleNameAndPassword(ClientSocket *client, const QString& name, const QString& password) { - // First check the name and password - // Matches a string that does not contain special characters - QRegExp nameExp("[^\\0000-\\0057\\0072-\\0100\\0133-\\0140\\0173-\\0177]+"); - QByteArray passwordHash = QCryptographicHash::hash(password.toLatin1(), QCryptographicHash::Sha256).toHex(); - bool passed = false; - QString error_msg; - QJsonObject result; + // First check the name and password + // Matches a string that does not contain special characters + QRegExp nameExp("[^\\0000-\\0057\\0072-\\0100\\0133-\\0140\\0173-\\0177]+"); + QByteArray passwordHash = QCryptographicHash::hash(password.toLatin1(), QCryptographicHash::Sha256).toHex(); + bool passed = false; + QString error_msg; + QJsonObject result; - if (nameExp.exactMatch(name)) { - // Then we check the database, - QString sql_find = QString("SELECT * FROM userinfo \ - WHERE name='%1';").arg(name); - result = SelectFromDatabase(db, sql_find); - QJsonArray arr = result["password"].toArray(); - if (arr.isEmpty()) { - // not present in database, register - QString sql_reg = QString("INSERT INTO userinfo (name,password,\ - avatar,lastLoginIp,banned) VALUES ('%1','%2','%3','%4',%5);") - .arg(name) - .arg(QString(passwordHash)) - .arg("liubei") - .arg(client->peerAddress()) - .arg("FALSE"); - ExecSQL(db, sql_reg); - result = SelectFromDatabase(db, sql_find); // refresh result - passed = true; - } else { - // check if this username already login - int id = result["id"].toArray()[0].toString().toInt(); - if (!players.value(id)) { - // check if password is the same - passed = (passwordHash == arr[0].toString()); - if (!passed) error_msg = "username or password error"; - } else { - // TODO: reconnect here - error_msg = "others logged in with this name"; - } - } + if (nameExp.exactMatch(name)) { + // Then we check the database, + QString sql_find = QString("SELECT * FROM userinfo \ + WHERE name='%1';").arg(name); + result = SelectFromDatabase(db, sql_find); + QJsonArray arr = result["password"].toArray(); + if (arr.isEmpty()) { + // not present in database, register + QString sql_reg = QString("INSERT INTO userinfo (name,password,\ + avatar,lastLoginIp,banned) VALUES ('%1','%2','%3','%4',%5);") + .arg(name) + .arg(QString(passwordHash)) + .arg("liubei") + .arg(client->peerAddress()) + .arg("FALSE"); + ExecSQL(db, sql_reg); + result = SelectFromDatabase(db, sql_find); // refresh result + passed = true; } else { - error_msg = "invalid user name"; + // check if this username already login + int id = result["id"].toArray()[0].toString().toInt(); + if (!players.value(id)) { + // check if password is the same + passed = (passwordHash == arr[0].toString()); + if (!passed) error_msg = "username or password error"; + } else { + // TODO: reconnect here + error_msg = "others logged in with this name"; + } } + } else { + error_msg = "invalid user name"; + } - if (passed) { - // create new ServerPlayer and setup - ServerPlayer *player = new ServerPlayer(lobby()); - player->setSocket(client); - client->disconnect(this); - connect(player, &ServerPlayer::disconnected, this, &Server::onUserDisconnected); - connect(player, &Player::stateChanged, this, &Server::onUserStateChanged); - player->setScreenName(name); - player->setAvatar(result["avatar"].toArray()[0].toString()); - player->setId(result["id"].toArray()[0].toString().toInt()); - players.insert(player->getId(), player); + if (passed) { + // create new ServerPlayer and setup + ServerPlayer *player = new ServerPlayer(lobby()); + player->setSocket(client); + client->disconnect(this); + connect(player, &ServerPlayer::disconnected, this, &Server::onUserDisconnected); + connect(player, &Player::stateChanged, this, &Server::onUserStateChanged); + player->setScreenName(name); + player->setAvatar(result["avatar"].toArray()[0].toString()); + player->setId(result["id"].toArray()[0].toString().toInt()); + players.insert(player->getId(), player); - // tell the lobby player's basic property - QJsonArray arr; - arr << player->getId(); - arr << player->getScreenName(); - arr << player->getAvatar(); - player->doNotify("Setup", QJsonDocument(arr).toJson()); + // tell the lobby player's basic property + QJsonArray arr; + arr << player->getId(); + arr << player->getScreenName(); + arr << player->getAvatar(); + player->doNotify("Setup", QJsonDocument(arr).toJson()); - lobby()->addPlayer(player); - } else { - qDebug() << client->peerAddress() << "lost connection:" << error_msg; - QJsonArray body; - body << -2; - body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER | Router::DEST_CLIENT); - body << "ErrorMsg"; - body << error_msg; - client->send(QJsonDocument(body).toJson(QJsonDocument::Compact)); - client->disconnectFromHost(); - return; - } + lobby()->addPlayer(player); + } else { + qDebug() << client->peerAddress() << "lost connection:" << error_msg; + QJsonArray body; + body << -2; + body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER | Router::DEST_CLIENT); + body << "ErrorMsg"; + body << error_msg; + client->send(QJsonDocument(body).toJson(QJsonDocument::Compact)); + client->disconnectFromHost(); + return; + } } void Server::onRoomAbandoned() { - Room *room = qobject_cast(sender()); - room->gameOver(); - rooms.remove(room->getId()); - updateRoomList(); - room->deleteLater(); + Room *room = qobject_cast(sender()); + room->gameOver(); + rooms.remove(room->getId()); + updateRoomList(); + room->deleteLater(); } void Server::onUserDisconnected() { - ServerPlayer *player = qobject_cast(sender()); - qDebug() << "Player" << player->getId() << "disconnected"; - Room *room = player->getRoom(); - if (room->isStarted()) { - player->setState(Player::Offline); - // TODO: add a robot - } else { - player->deleteLater(); - } + ServerPlayer *player = qobject_cast(sender()); + qDebug() << "Player" << player->getId() << "disconnected"; + Room *room = player->getRoom(); + if (room->isStarted()) { + player->setState(Player::Offline); + // TODO: add a robot + } else { + player->deleteLater(); + } } void Server::onUserStateChanged() { - ServerPlayer *player = qobject_cast(sender()); - QJsonArray arr; - arr << player->getId(); - arr << player->getStateString(); - player->getRoom()->callLua("PlayerStateChanged", QJsonDocument(arr).toJson()); + ServerPlayer *player = qobject_cast(sender()); + QJsonArray arr; + arr << player->getId(); + arr << player->getStateString(); + player->getRoom()->callLua("PlayerStateChanged", QJsonDocument(arr).toJson()); } diff --git a/src/server/server.h b/src/server/server.h index 8d963246..58101306 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -8,49 +8,49 @@ class ServerPlayer; #include "room.h" class Server : public QObject { - Q_OBJECT + Q_OBJECT public: - explicit Server(QObject *parent = nullptr); - ~Server(); + explicit Server(QObject *parent = nullptr); + ~Server(); - bool listen(const QHostAddress &address = QHostAddress::Any, ushort port = 9527u); + bool listen(const QHostAddress &address = QHostAddress::Any, ushort port = 9527u); - void createRoom(ServerPlayer *owner, const QString &name, int capacity); - Room *findRoom(int id) const; - Room *lobby() const; + void createRoom(ServerPlayer *owner, const QString &name, int capacity); + Room *findRoom(int id) const; + Room *lobby() const; - ServerPlayer *findPlayer(int id) const; - void removePlayer(int id); + ServerPlayer *findPlayer(int id) const; + void removePlayer(int id); - void updateRoomList(); + void updateRoomList(); - sqlite3 *getDatabase(); + sqlite3 *getDatabase(); signals: - void roomCreated(Room *room); - void playerAdded(ServerPlayer *player); - void playerRemoved(ServerPlayer *player); + void roomCreated(Room *room); + void playerAdded(ServerPlayer *player); + void playerRemoved(ServerPlayer *player); public slots: - void processNewConnection(ClientSocket *client); - void processRequest(const QByteArray &msg); + void processNewConnection(ClientSocket *client); + void processRequest(const QByteArray &msg); - void onRoomAbandoned(); - void onUserDisconnected(); - void onUserStateChanged(); + void onRoomAbandoned(); + void onUserDisconnected(); + void onUserStateChanged(); private: - ServerSocket *server; - Room *m_lobby; - QMap rooms; - int nextRoomId; - friend Room::Room(Server *server); - QHash players; + ServerSocket *server; + Room *m_lobby; + QMap rooms; + int nextRoomId; + friend Room::Room(Server *server); + QHash players; - sqlite3 *db; + sqlite3 *db; - void handleNameAndPassword(ClientSocket *client, const QString &name, const QString &password); + void handleNameAndPassword(ClientSocket *client, const QString &name, const QString &password); }; extern Server *ServerInstance; diff --git a/src/server/serverplayer.cpp b/src/server/serverplayer.cpp index a863484d..a456dbea 100644 --- a/src/server/serverplayer.cpp +++ b/src/server/serverplayer.cpp @@ -6,111 +6,111 @@ ServerPlayer::ServerPlayer(Room *room) { - socket = nullptr; - router = new Router(this, socket, Router::TYPE_SERVER); - setState(Player::Online); - this->room = room; - server = room->getServer(); + socket = nullptr; + router = new Router(this, socket, Router::TYPE_SERVER); + setState(Player::Online); + this->room = room; + server = room->getServer(); } ServerPlayer::~ServerPlayer() { - // clean up, quit room and server + // clean up, quit room and server + room->removePlayer(this); + if (room != nullptr) { + // now we are in lobby, so quit lobby room->removePlayer(this); - if (room != nullptr) { - // now we are in lobby, so quit lobby - room->removePlayer(this); - } - server->removePlayer(getId()); - router->deleteLater(); + } + server->removePlayer(getId()); + router->deleteLater(); } void ServerPlayer::setSocket(ClientSocket *socket) { - if (this->socket != nullptr) { - this->socket->disconnect(this); - disconnect(this->socket); - this->socket->deleteLater(); - } + if (this->socket != nullptr) { + this->socket->disconnect(this); + disconnect(this->socket); + this->socket->deleteLater(); + } - this->socket = nullptr; - if (socket != nullptr) { - connect(socket, &ClientSocket::disconnected, this, &ServerPlayer::disconnected); - this->socket = socket; - } + this->socket = nullptr; + if (socket != nullptr) { + connect(socket, &ClientSocket::disconnected, this, &ServerPlayer::disconnected); + this->socket = socket; + } - router->setSocket(socket); + router->setSocket(socket); } Server *ServerPlayer::getServer() const { - return server; + return server; } Room *ServerPlayer::getRoom() const { - return room; + return room; } void ServerPlayer::setRoom(Room* room) { - this->room = room; + this->room = room; } void ServerPlayer::speak(const QString& message) { - ; + ; } void ServerPlayer::doRequest(const QString& command, const QString& jsonData, int timeout) { - if (getState() != Player::Online) return; - int type = Router::TYPE_REQUEST | Router::SRC_SERVER | Router::DEST_CLIENT; - router->request(type, command, jsonData, timeout); + if (getState() != Player::Online) return; + int type = Router::TYPE_REQUEST | Router::SRC_SERVER | Router::DEST_CLIENT; + router->request(type, command, jsonData, timeout); } void ServerPlayer::abortRequest() { - router->abortRequest(); + router->abortRequest(); } QString ServerPlayer::waitForReply() { - room->unlockLua(__FUNCTION__); - QString ret; - if (getState() != Player::Online) { - QThread::sleep(1); - ret = ""; - } else { - ret = router->waitForReply(); - } - room->lockLua(__FUNCTION__); - return ret; + room->unlockLua(__FUNCTION__); + QString ret; + if (getState() != Player::Online) { + QThread::sleep(1); + ret = ""; + } else { + ret = router->waitForReply(); + } + room->lockLua(__FUNCTION__); + return ret; } QString ServerPlayer::waitForReply(int timeout) { - room->unlockLua(__FUNCTION__); - QString ret; - if (getState() != Player::Online) { - QThread::sleep(1); - ret = ""; - } else { - ret = router->waitForReply(timeout); - } - room->lockLua(__FUNCTION__); - return ret; + room->unlockLua(__FUNCTION__); + QString ret; + if (getState() != Player::Online) { + QThread::sleep(1); + ret = ""; + } else { + ret = router->waitForReply(timeout); + } + room->lockLua(__FUNCTION__); + return ret; } void ServerPlayer::doNotify(const QString& command, const QString& jsonData) { - if (getState() != Player::Online) return; - int type = Router::TYPE_NOTIFICATION | Router::SRC_SERVER | Router::DEST_CLIENT; - router->notify(type, command, jsonData); + if (getState() != Player::Online) return; + int type = Router::TYPE_NOTIFICATION | Router::SRC_SERVER | Router::DEST_CLIENT; + router->notify(type, command, jsonData); } void ServerPlayer::prepareForRequest(const QString& command, const QString& data) { - requestCommand = command; - requestData = data; + requestCommand = command; + requestData = data; } diff --git a/src/server/serverplayer.h b/src/server/serverplayer.h index d2640535..b385b6bd 100644 --- a/src/server/serverplayer.h +++ b/src/server/serverplayer.h @@ -9,40 +9,40 @@ class Server; class Room; class ServerPlayer : public Player { - Q_OBJECT + Q_OBJECT public: - explicit ServerPlayer(Room *room); - ~ServerPlayer(); + explicit ServerPlayer(Room *room); + ~ServerPlayer(); - void setSocket(ClientSocket *socket); + void setSocket(ClientSocket *socket); - Server *getServer() const; - Room *getRoom() const; - void setRoom(Room *room); + Server *getServer() const; + Room *getRoom() const; + void setRoom(Room *room); - void speak(const QString &message); + void speak(const QString &message); - void doRequest(const QString &command, - const QString &jsonData, int timeout = -1); - void abortRequest(); - QString waitForReply(int timeout); - QString waitForReply(); - void doNotify(const QString &command, const QString &jsonData); + void doRequest(const QString &command, + const QString &jsonData, int timeout = -1); + void abortRequest(); + QString waitForReply(int timeout); + QString waitForReply(); + void doNotify(const QString &command, const QString &jsonData); - void prepareForRequest(const QString &command, - const QString &data); + void prepareForRequest(const QString &command, + const QString &data); signals: - void disconnected(); - + void disconnected(); + private: - ClientSocket *socket; // socket for communicating with client - Router *router; - Server *server; - Room *room; // Room that player is in, maybe lobby + ClientSocket *socket; // socket for communicating with client + Router *router; + Server *server; + Room *room; // Room that player is in, maybe lobby - QString requestCommand; - QString requestData; + QString requestCommand; + QString requestData; }; #endif // _SERVERPLAYER_H diff --git a/src/swig/client.i b/src/swig/client.i index 3667aad6..fb8bd81b 100644 --- a/src/swig/client.i +++ b/src/swig/client.i @@ -2,13 +2,13 @@ %nodefaultdtor QmlBackend; class QmlBackend : public QObject { public: - void emitNotifyUI(const QString &command, const QString &json_data); + void emitNotifyUI(const QString &command, const QString &json_data); - static void cd(const QString &path); - static QStringList ls(const QString &dir); - static QString pwd(); - static bool exists(const QString &file); - static bool isDir(const QString &file); + static void cd(const QString &path); + static QStringList ls(const QString &dir); + static QString pwd(); + static bool exists(const QString &file); + static bool isDir(const QString &file); }; extern QmlBackend *Backend; @@ -17,13 +17,13 @@ extern QmlBackend *Backend; %nodefaultdtor Client; class Client : public QObject { public: - void replyToServer(const QString &command, const QString &json_data); - void notifyServer(const QString &command, const QString &json_data); + void replyToServer(const QString &command, const QString &json_data); + void notifyServer(const QString &command, const QString &json_data); - LuaFunction callback; + LuaFunction callback; - ClientPlayer *addPlayer(int id, const QString &name, const QString &avatar); - void removePlayer(int id); + ClientPlayer *addPlayer(int id, const QString &name, const QString &avatar); + void removePlayer(int id); }; extern Client *ClientInstance; @@ -31,24 +31,24 @@ extern Client *ClientInstance; %{ void Client::callLua(const QString& command, const QString& json_data) { - Q_ASSERT(callback); + Q_ASSERT(callback); - lua_getglobal(L, "debug"); - lua_getfield(L, -1, "traceback"); - lua_replace(L, -2); + lua_getglobal(L, "debug"); + lua_getfield(L, -1, "traceback"); + lua_replace(L, -2); - lua_rawgeti(L, LUA_REGISTRYINDEX, callback); - SWIG_NewPointerObj(L, this, SWIGTYPE_p_Client, 0); - lua_pushstring(L, command.toUtf8()); - lua_pushstring(L, json_data.toUtf8()); + lua_rawgeti(L, LUA_REGISTRYINDEX, callback); + SWIG_NewPointerObj(L, this, SWIGTYPE_p_Client, 0); + lua_pushstring(L, command.toUtf8()); + lua_pushstring(L, json_data.toUtf8()); - int error = lua_pcall(L, 3, 0, -5); + int error = lua_pcall(L, 3, 0, -5); - if (error) { - const char *error_msg = lua_tostring(L, -1); - qDebug() << error_msg; - lua_pop(L, 2); - } - lua_pop(L, 1); + if (error) { + const char *error_msg = lua_tostring(L, -1); + qDebug() << error_msg; + lua_pop(L, 2); + } + lua_pop(L, 1); } %} diff --git a/src/swig/naturalvar.i b/src/swig/naturalvar.i index 59e49070..03b736d4 100644 --- a/src/swig/naturalvar.i +++ b/src/swig/naturalvar.i @@ -7,10 +7,10 @@ %typemap(in) LuaFunction %{ if (lua_isfunction(L, $input)) { - lua_pushvalue(L, $input); - $1 = luaL_ref(L, LUA_REGISTRYINDEX); + lua_pushvalue(L, $input); + $1 = luaL_ref(L, LUA_REGISTRYINDEX); } else { - $1 = 0; + $1 = 0; } %} @@ -35,8 +35,8 @@ SWIG_arg ++; %typemap(in, checkfn = "lua_isstring") QString const & %{ - $1_str = QString::fromUtf8(lua_tostring(L, $input)); - $1 = &$1_str; + $1_str = QString::fromUtf8(lua_tostring(L, $input)); + $1 = &$1_str; %} %typemap(out) QString const & @@ -48,10 +48,10 @@ SWIG_arg ++; %typemap(in, checkfn = "lua_istable") QStringList %{ for (size_t i = 0; i < lua_rawlen(L, $input); ++i) { - lua_rawgeti(L, $input, i + 1); - const char *elem = luaL_checkstring(L, -1); - $1 << QString::fromUtf8(QByteArray(elem)); - lua_pop(L, 1); + lua_rawgeti(L, $input, i + 1); + const char *elem = luaL_checkstring(L, -1); + $1 << QString::fromUtf8(QByteArray(elem)); + lua_pop(L, 1); } %} @@ -60,9 +60,9 @@ for (size_t i = 0; i < lua_rawlen(L, $input); ++i) { lua_createtable(L, $1.length(), 0); for (int i = 0; i < $1.length(); i++) { - QString str = $1.at(i); - lua_pushstring(L, str.toUtf8().constData()); - lua_rawseti(L, -2, i + 1); + QString str = $1.at(i); + lua_pushstring(L, str.toUtf8().constData()); + lua_rawseti(L, -2, i + 1); } SWIG_arg++; @@ -70,7 +70,7 @@ SWIG_arg++; %typemap(typecheck) QStringList %{ - $1 = lua_istable(L, $input) ? 1 : 0; + $1 = lua_istable(L, $input) ? 1 : 0; %} diff --git a/src/swig/player.i b/src/swig/player.i index f9b6841e..cd0ad9e2 100644 --- a/src/swig/player.i +++ b/src/swig/player.i @@ -2,29 +2,29 @@ %nodefaultdtor Player; class Player : public QObject { public: - enum State{ - Invalid, - Online, - Trust, - Offline - }; + enum State{ + Invalid, + Online, + Trust, + Offline + }; - int getId() const; - void setId(int id); + int getId() const; + void setId(int id); - QString getScreenName() const; - void setScreenName(const QString &name); + QString getScreenName() const; + void setScreenName(const QString &name); - QString getAvatar() const; - void setAvatar(const QString &avatar); + QString getAvatar() const; + void setAvatar(const QString &avatar); - State getState() const; - QString getStateString() const; - void setState(State state); - void setStateString(const QString &state); + State getState() const; + QString getStateString() const; + void setState(State state); + void setStateString(const QString &state); - bool isReady() const; - void setReady(bool ready); + bool isReady() const; + void setReady(bool ready); }; %nodefaultctor ClientPlayer; @@ -39,17 +39,17 @@ extern ClientPlayer *Self; %nodefaultdtor ServerPlayer; class ServerPlayer : public Player { public: - Server *getServer() const; - Room *getRoom() const; - void setRoom(Room *room); + Server *getServer() const; + Room *getRoom() const; + void setRoom(Room *room); - void speak(const QString &message); + void speak(const QString &message); - void doRequest(const QString &command, - const QString &json_data, int timeout); - QString waitForReply(); - QString waitForReply(int timeout); - void doNotify(const QString &command, const QString &json_data); + void doRequest(const QString &command, + 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); + void prepareForRequest(const QString &command, const QString &data); }; diff --git a/src/swig/qt.i b/src/swig/qt.i index d8968898..195c79aa 100644 --- a/src/swig/qt.i +++ b/src/swig/qt.i @@ -5,29 +5,29 @@ class QThread {}; template class QList { public: - QList(); - ~QList(); - int length() const; - void append(const T &elem); - void prepend(const T &elem); - bool isEmpty() const; - bool contains(const T &value) const; - T first() const; - T last() const; - void removeAt(int i); - int removeAll(const T &value); - bool removeOne(const T &value); - QList mid(int pos, int length = -1) const; - int indexOf(const T &value, int from = 0); - void replace(int i, const T &value); - void swapItemsAt(int i, int j); + QList(); + ~QList(); + int length() const; + void append(const T &elem); + void prepend(const T &elem); + bool isEmpty() const; + bool contains(const T &value) const; + T first() const; + T last() const; + void removeAt(int i); + int removeAll(const T &value); + bool removeOne(const T &value); + QList mid(int pos, int length = -1) const; + int indexOf(const T &value, int from = 0); + void replace(int i, const T &value); + void swapItemsAt(int i, int j); }; %extend QList { - T at(int i) const - { - return $self->value(i); - } + T at(int i) const + { + return $self->value(i); + } } %template(SPlayerList) QList; @@ -39,10 +39,10 @@ public: %{ #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; -} + 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 c2494d6e..a3c5f4cc 100644 --- a/src/swig/server.i +++ b/src/swig/server.i @@ -2,12 +2,12 @@ %nodefaultdtor Server; class Server : public QObject { public: - Room *lobby() const; - void createRoom(ServerPlayer *owner, const QString &name, int capacity); - Room *findRoom(int id) const; - ServerPlayer *findPlayer(int id) const; + Room *lobby() const; + void createRoom(ServerPlayer *owner, const QString &name, int capacity); + Room *findRoom(int id) const; + ServerPlayer *findPlayer(int id) const; - sqlite3 *getDatabase(); + sqlite3 *getDatabase(); }; extern Server *ServerInstance; @@ -16,104 +16,104 @@ extern Server *ServerInstance; %nodefaultdtor Room; class Room : public QThread { public: - // Property reader & setter - // ==================================={ - Server *getServer() const; - int getId() const; - bool isLobby() const; - QString getName() const; - void setName(const QString &name); - int getCapacity() const; - void setCapacity(int capacity); - bool isFull() const; - bool isAbandoned() const; + // Property reader & setter + // ==================================={ + Server *getServer() const; + int getId() const; + bool isLobby() const; + QString getName() const; + void setName(const QString &name); + int getCapacity() const; + void setCapacity(int capacity); + bool isFull() const; + bool isAbandoned() const; - ServerPlayer *getOwner() const; - void setOwner(ServerPlayer *owner); + ServerPlayer *getOwner() const; + void setOwner(ServerPlayer *owner); - void addPlayer(ServerPlayer *player); - void addRobot(ServerPlayer *player); - void removePlayer(ServerPlayer *player); - QList getPlayers() const; - ServerPlayer *findPlayer(int id) const; + void addPlayer(ServerPlayer *player); + void addRobot(ServerPlayer *player); + void removePlayer(ServerPlayer *player); + QList getPlayers() const; + ServerPlayer *findPlayer(int id) const; - int getTimeout() const; + int getTimeout() const; - bool isStarted() const; - // ====================================} + bool isStarted() const; + // ====================================} - void doRequest(const QList targets, int timeout); - void doNotify(const QList targets, int timeout); - void doBroadcastNotify( - const QList targets, - const QString &command, - const QString &jsonData - ); - - void gameOver(); + void doRequest(const QList targets, int timeout); + void doNotify(const QList targets, int timeout); + void doBroadcastNotify( + const QList targets, + const QString &command, + const QString &jsonData + ); + + void gameOver(); - LuaFunction callback; - LuaFunction startGame; + LuaFunction callback; + LuaFunction startGame; }; %{ void Room::initLua() { - lua_getglobal(L, "debug"); - lua_getfield(L, -1, "traceback"); - lua_replace(L, -2); - lua_getglobal(L, "CreateRoom"); - SWIG_NewPointerObj(L, this, SWIGTYPE_p_Room, 0); - int error = lua_pcall(L, 1, 0, -2); - lua_pop(L, 1); - if (error) { - const char *error_msg = lua_tostring(L, -1); - qDebug() << error_msg; - } + lua_getglobal(L, "debug"); + lua_getfield(L, -1, "traceback"); + lua_replace(L, -2); + lua_getglobal(L, "CreateRoom"); + SWIG_NewPointerObj(L, this, SWIGTYPE_p_Room, 0); + int error = lua_pcall(L, 1, 0, -2); + lua_pop(L, 1); + if (error) { + const char *error_msg = lua_tostring(L, -1); + qDebug() << error_msg; + } } void Room::callLua(const QString& command, const QString& json_data) { - Q_ASSERT(callback); + Q_ASSERT(callback); - lua_getglobal(L, "debug"); - lua_getfield(L, -1, "traceback"); - lua_replace(L, -2); + lua_getglobal(L, "debug"); + lua_getfield(L, -1, "traceback"); + lua_replace(L, -2); - lua_rawgeti(L, LUA_REGISTRYINDEX, callback); - SWIG_NewPointerObj(L, this, SWIGTYPE_p_Room, 0); - lua_pushstring(L, command.toUtf8()); - lua_pushstring(L, json_data.toUtf8()); + lua_rawgeti(L, LUA_REGISTRYINDEX, callback); + SWIG_NewPointerObj(L, this, SWIGTYPE_p_Room, 0); + lua_pushstring(L, command.toUtf8()); + lua_pushstring(L, json_data.toUtf8()); - int error = lua_pcall(L, 3, 0, -5); + int error = lua_pcall(L, 3, 0, -5); - if (error) { - const char *error_msg = lua_tostring(L, -1); - qDebug() << error_msg; - lua_pop(L, 2); - } - lua_pop(L, 1); + if (error) { + const char *error_msg = lua_tostring(L, -1); + qDebug() << error_msg; + lua_pop(L, 2); + } + lua_pop(L, 1); } void Room::roomStart() { - Q_ASSERT(startGame); + Q_ASSERT(startGame); - lua_getglobal(L, "debug"); - lua_getfield(L, -1, "traceback"); - lua_replace(L, -2); + lua_getglobal(L, "debug"); + lua_getfield(L, -1, "traceback"); + lua_replace(L, -2); - lua_rawgeti(L, LUA_REGISTRYINDEX, startGame); - SWIG_NewPointerObj(L, this, SWIGTYPE_p_Room, 0); + lua_rawgeti(L, LUA_REGISTRYINDEX, startGame); + SWIG_NewPointerObj(L, this, SWIGTYPE_p_Room, 0); - int error = lua_pcall(L, 1, 0, -3); + int error = lua_pcall(L, 1, 0, -3); - if (error) { - const char *error_msg = lua_tostring(L, -1); - qDebug() << error_msg; - lua_pop(L, 2); - } - lua_pop(L, 1); + if (error) { + const char *error_msg = lua_tostring(L, -1); + qDebug() << error_msg; + lua_pop(L, 2); + } + lua_pop(L, 1); } - + %} diff --git a/src/ui/qmlbackend.cpp b/src/ui/qmlbackend.cpp index d2a157ef..5d702e8a 100644 --- a/src/ui/qmlbackend.cpp +++ b/src/ui/qmlbackend.cpp @@ -5,150 +5,151 @@ QmlBackend *Backend; QmlBackend::QmlBackend(QObject* parent) - : QObject(parent) + : QObject(parent) { - Backend = this; - engine = nullptr; + Backend = this; + engine = nullptr; } QQmlApplicationEngine *QmlBackend::getEngine() const { - return engine; + return engine; } void QmlBackend::setEngine(QQmlApplicationEngine *engine) { - this->engine = engine; + this->engine = engine; } void QmlBackend::startServer(ushort port) { - if (!ServerInstance) { - Server *server = new Server(this); + if (!ServerInstance) { + Server *server = new Server(this); - if (!server->listen(QHostAddress::Any, port)) { - server->deleteLater(); - emit notifyUI("ErrorMsg", tr("Cannot start server!")); - } + if (!server->listen(QHostAddress::Any, port)) { + server->deleteLater(); + emit notifyUI("ErrorMsg", tr("Cannot start server!")); } + } } void QmlBackend::joinServer(QString address) { - if (ClientInstance != nullptr) return; - Client *client = new Client(this); - connect(client, &Client::error_message, [this, client](const QString &msg){ - client->deleteLater(); - emit notifyUI("ErrorMsg", msg); - emit notifyUI("BackToStart", "[]"); - }); - QString addr = "127.0.0.1"; - ushort port = 9527u; + if (ClientInstance != nullptr) return; + Client *client = new Client(this); + connect(client, &Client::error_message, [this, client](const QString &msg){ + client->deleteLater(); + emit notifyUI("ErrorMsg", msg); + emit notifyUI("BackToStart", "[]"); + }); + QString addr = "127.0.0.1"; + ushort port = 9527u; - if (address.contains(QChar(':'))) { - QStringList texts = address.split(QChar(':')); - addr = texts.value(0); - port = texts.value(1).toUShort(); - } else { - addr = address; - } + if (address.contains(QChar(':'))) { + QStringList texts = address.split(QChar(':')); + addr = texts.value(0); + port = texts.value(1).toUShort(); + } else { + addr = address; + } - client->connectToHost(QHostAddress(addr), port); + client->connectToHost(QHostAddress(addr), port); } void QmlBackend::quitLobby() { - delete ClientInstance; + delete ClientInstance; } void QmlBackend::emitNotifyUI(const QString &command, const QString &jsonData) { - emit notifyUI(command, jsonData); + emit notifyUI(command, jsonData); } void QmlBackend::cd(const QString &path) { - QDir::setCurrent(path); + QDir::setCurrent(path); } QStringList QmlBackend::ls(const QString &dir) { - return QDir(dir).entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); + return QDir(dir).entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); } QString QmlBackend::pwd() { - return QDir::currentPath(); + return QDir::currentPath(); } bool QmlBackend::exists(const QString &file) { - return QFile::exists(file); + return QFile::exists(file); } bool QmlBackend::isDir(const QString &file) { - return QFileInfo(file).isDir(); + return QFileInfo(file).isDir(); } -#define CALLFUNC int err = lua_pcall(L, 1, 1, 0); \ - const char *result = lua_tostring(L, -1); \ - if (err) { \ - qDebug() << result; \ - lua_pop(L, 1); \ - return ""; \ - } \ - lua_pop(L, 1); \ - return QString(result); \ - QString QmlBackend::translate(const QString &src) { - lua_State *L = ClientInstance->getLuaState(); - lua_getglobal(L, "Translate"); - lua_pushstring(L, src.toUtf8().data()); + lua_State *L = ClientInstance->getLuaState(); + lua_getglobal(L, "Translate"); + lua_pushstring(L, src.toUtf8().data()); - CALLFUNC + int err = lua_pcall(L, 1, 1, 0); + const char *result = lua_tostring(L, -1); + if (err) { + qDebug() << result; + lua_pop(L, 1); + return ""; + } + lua_pop(L, 1); + return QString(result); } -QString QmlBackend::getGeneralData(const QString &general_name) { - lua_State *L = ClientInstance->getLuaState(); - lua_getglobal(L, "GetGeneralData"); - lua_pushstring(L, general_name.toUtf8().data()); - - CALLFUNC +void QmlBackend::pushLuaValue(lua_State *L, QVariant v) { + QVariantList list; + switch(v.type()) { + case QVariant::Bool: + lua_pushboolean(L, v.toBool()); + break; + case QVariant::Int: + case QVariant::UInt: + lua_pushinteger(L, v.toInt()); + break; + case QVariant::Double: + lua_pushnumber(L, v.toDouble()); + break; + case QVariant::String: + lua_pushstring(L, v.toString().toUtf8().data()); + break; + case QVariant::List: + lua_newtable(L); + list = v.toList(); + for (int i = 1; i <= list.length(); i++) { + lua_pushinteger(L, i); + pushLuaValue(L, list[i - 1]); + lua_settable(L, -3); + } + break; + default: + qDebug() << "cannot handle QVariant type" << v.type(); + lua_pushnil(L); + break; + } } -QString QmlBackend::getCardData(int id) { - lua_State *L = ClientInstance->getLuaState(); - lua_getglobal(L, "GetCardData"); - lua_pushinteger(L, id); +QString QmlBackend::callLuaFunction(const QString &func_name, + QVariantList params) +{ + lua_State *L = ClientInstance->getLuaState(); + lua_getglobal(L, func_name.toLatin1().data()); - CALLFUNC + foreach (QVariant v, params) { + pushLuaValue(L, v); + } + + int err = lua_pcall(L, params.length(), 1, 0); + const char *result = lua_tostring(L, -1); + if (err) { + qDebug() << result; + lua_pop(L, 1); + return ""; + } + lua_pop(L, 1); + return QString(result); } - -QString QmlBackend::getAllGeneralPack() { - lua_State *L = ClientInstance->getLuaState(); - lua_getglobal(L, "GetAllGeneralPack"); - lua_pushinteger(L, 0); - - CALLFUNC -} - -QString QmlBackend::getGenerals(const QString &pack_name) { - lua_State *L = ClientInstance->getLuaState(); - lua_getglobal(L, "GetGenerals"); - lua_pushstring(L, pack_name.toUtf8().data()); - - CALLFUNC -} - -QString QmlBackend::getAllCardPack() { - lua_State *L = ClientInstance->getLuaState(); - lua_getglobal(L, "GetAllCardPack"); - lua_pushinteger(L, 0); - - CALLFUNC -} - -QString QmlBackend::getCards(const QString &pack_name) { - lua_State *L = ClientInstance->getLuaState(); - lua_getglobal(L, "GetCards"); - lua_pushstring(L, pack_name.toUtf8().data()); - - CALLFUNC -} - -#undef CALLFUNC diff --git a/src/ui/qmlbackend.h b/src/ui/qmlbackend.h index abf700c7..fec1b4f5 100644 --- a/src/ui/qmlbackend.h +++ b/src/ui/qmlbackend.h @@ -2,43 +2,41 @@ #define _QMLBACKEND_H class QmlBackend : public QObject { - Q_OBJECT + Q_OBJECT public: - QmlBackend(QObject *parent = nullptr); + QmlBackend(QObject *parent = nullptr); - QQmlApplicationEngine *getEngine() const; - void setEngine(QQmlApplicationEngine *engine); + QQmlApplicationEngine *getEngine() const; + void setEngine(QQmlApplicationEngine *engine); - Q_INVOKABLE void startServer(ushort port); - Q_INVOKABLE void joinServer(QString address); + Q_INVOKABLE void startServer(ushort port); + Q_INVOKABLE void joinServer(QString address); - // Lobby - Q_INVOKABLE void quitLobby(); + // Lobby + Q_INVOKABLE void quitLobby(); - // lua --> qml - void emitNotifyUI(const QString &command, const QString &jsonData); + // lua --> qml + void emitNotifyUI(const QString &command, const QString &jsonData); - // File used by both Lua and Qml - static Q_INVOKABLE void cd(const QString &path); - static Q_INVOKABLE QStringList ls(const QString &dir = ""); - static Q_INVOKABLE QString pwd(); - static Q_INVOKABLE bool exists(const QString &file); - static Q_INVOKABLE bool isDir(const QString &file); + // File used by both Lua and Qml + static Q_INVOKABLE void cd(const QString &path); + static Q_INVOKABLE QStringList ls(const QString &dir = ""); + static Q_INVOKABLE QString pwd(); + static Q_INVOKABLE bool exists(const QString &file); + static Q_INVOKABLE bool isDir(const QString &file); - // read data from lua, call lua functions - Q_INVOKABLE QString translate(const QString &src); - Q_INVOKABLE QString getGeneralData(const QString &general_name); - Q_INVOKABLE QString getCardData(int id); - Q_INVOKABLE QString getAllGeneralPack(); - Q_INVOKABLE QString getGenerals(const QString &pack_name); - Q_INVOKABLE QString getAllCardPack(); - Q_INVOKABLE QString getCards(const QString &pack_name); + // read data from lua, call lua functions + Q_INVOKABLE QString translate(const QString &src); + Q_INVOKABLE QString callLuaFunction(const QString &func_name, + QVariantList params); signals: - void notifyUI(const QString &command, const QString &jsonData); + void notifyUI(const QString &command, const QString &jsonData); private: - QQmlApplicationEngine *engine; + QQmlApplicationEngine *engine; + + void pushLuaValue(lua_State *L, QVariant v); }; extern QmlBackend *Backend;