From 0029949a408ec273323e530083fe95361aac0c5e Mon Sep 17 00:00:00 2001 From: notify Date: Tue, 20 Dec 2022 12:51:54 +0800 Subject: [PATCH] Basiccard (#33) * update nullification * Slash * kill player * correct players to alive_players * add log for changehp and dying * usecard log & indicator * setemotion, logevent * fix distanceTo * shutdown server when console start * game over * complete slash * change format of flist.txt to avoid '\r\n' * fix \r\n * peach, zhiheng * ask for peach --- lua/client/client.lua | 28 +++ lua/client/client_util.lua | 41 ++++ lua/core/player.lua | 23 ++- lua/server/event.lua | 10 +- lua/server/gamelogic.lua | 4 +- lua/server/room.lua | 261 +++++++++++++++++++++----- lua/server/serverplayer.lua | 42 ++++- packages/standard/game_rule.lua | 84 ++++++++- packages/standard/init.lua | 4 +- packages/standard_cards/init.lua | 53 +++++- qml/Pages/Room.qml | 31 ++- qml/Pages/RoomElement/Dashboard.qml | 4 + qml/Pages/RoomElement/GameOverBox.qml | 32 ++++ qml/Pages/RoomElement/Photo.qml | 20 +- qml/Pages/RoomLogic.js | 55 +++++- src/core/util.cpp | 4 +- src/server/serverplayer.cpp | 2 + 17 files changed, 622 insertions(+), 76 deletions(-) create mode 100644 qml/Pages/RoomElement/GameOverBox.qml diff --git a/lua/client/client.lua b/lua/client/client.lua index c4ca6edc..ec28608a 100644 --- a/lua/client/client.lua +++ b/lua/client/client.lua @@ -143,6 +143,7 @@ fk.client_callback["Setup"] = function(jsonData) end fk.client_callback["EnterRoom"] = function(jsonData) + Self = ClientPlayer:new(fk.Self) ClientInstance.players = {Self} ClientInstance.alive_players = {Self} ClientInstance.discard_pile = {} @@ -188,6 +189,12 @@ fk.client_callback["ArrangeSeats"] = function(jsonData) p.seat = i table.insert(players, p) end + + for i = 1, #players - 1 do + players[i].next = players[i + 1] + end + players[#players].next = players[1] + ClientInstance.players = players ClientInstance:notifyUI("ArrangeSeats", jsonData) @@ -355,6 +362,27 @@ fk.client_callback["GameLog"] = function(jsonData) ClientInstance:appendLog(data) end +fk.client_callback["LogEvent"] = function(jsonData) + local data = json.decode(jsonData) + if data.type == "Death" then + table.removeOne( + ClientInstance.alive_players, + ClientInstance:getPlayerById(data.to) + ) + end + ClientInstance:notifyUI("LogEvent", jsonData) +end + +fk.client_callback["AddCardUseHistory"] = function(jsonData) + local data = json.decode(jsonData) + Self:addCardUseHistory(data[1], data[2]) +end + +fk.client_callback["ResetCardUseHistory"] = function(jsonData) + if jsonData == "" then jsonData = nil end + Self:resetCardUseHistory(jsonData) +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 1bd23bbc..0e975be9 100644 --- a/lua/client/client_util.lua +++ b/lua/client/client_util.lua @@ -77,6 +77,12 @@ function GetCards(pack_name) return json.encode(ret) end +function DistanceTo(from, to) + local a = ClientInstance:getPlayerById(from) + local b = ClientInstance:getPlayerById(to) + return a:distanceTo(b) +end + ---@param card string | integer ---@param player integer function CanUseCard(card, player) @@ -96,6 +102,9 @@ end ---@param selected integer[] @ ids of selected targets ---@param selected_cards integer[] @ ids of selected cards function CanUseCardToTarget(card, to_select, selected) + if ClientInstance:getPlayerById(to_select).dead then + return "false" + end local c ---@type Card local selected_cards if type(card) == "number" then @@ -264,6 +273,23 @@ Fk:loadTranslationTable{ ["Sort Cards"] = "牌序", ["Chat"] = "聊天", ["Log"] = "战报", + + ["$GameOver"] = "游戏结束", + ["$Winner"] = "%1 获胜", + ["Back To Lobby"] = "返回大厅", +} + +-- Game concepts +Fk:loadTranslationTable{ + ["lord"] = "主公", + ["loyalist"] = "忠臣", + ["rebel"] = "反贼", + ["renegade"] = "内奸", + ["lord+loyalist"] = "主忠", + + ["normal_damage"] = "无属性", + ["fire_damage"] = "火属性", + ["thunder_damage"] = "雷属性", } -- related to sendLog @@ -296,6 +322,9 @@ Fk:loadTranslationTable{ -- useCard ["#UseCard"] = "%from 使用了牌 %card", ["#UseCardToTargets"] = "%from 使用了牌 %card,目标是 %to", + ["#CardUseCollaborator"] = "%from 在此次 %card 中的子目标是 %to", + ["#UseCardToCard"] = "%from 使用了牌 %card,目标是 %arg", + ["#ResponsePlayCard"] = "%from 打出了牌 %card", -- judge ["#InitialJudge"] = "%from 的判定牌为 %card", @@ -306,4 +335,16 @@ Fk:loadTranslationTable{ ["#TurnOver"] = "%from 将武将牌翻面,现在是 %arg", ["face_up"] = "正面朝上", ["face_down"] = "背面朝上", + + -- damage, heal and lose HP + ["#Damage"] = "%to 对 %from 造成了 %arg 点 %arg2 伤害", + ["#DamageWithNoFrom"] = "%from 受到了 %arg 点 %arg2 伤害", + ["#LoseHP"] = "%from 失去了 %arg 点体力", + ["#HealHP"] = "%from 回复了 %arg 点体力", + ["#ShowHPAndMaxHP"] = "%from 现在的体力值为 %arg,体力上限为 %arg2", + + -- dying and death + ["#EnterDying"] = "%from 进入了濒死阶段", + ["#KillPlayer"] = "%from [%arg] 阵亡,凶手是 %to", + ["#KillPlayerWithNoKiller"] = "%from [%arg] 阵亡,无伤害来源", } diff --git a/lua/core/player.lua b/lua/core/player.lua index 8c71d4fe..fff16fc1 100644 --- a/lua/core/player.lua +++ b/lua/core/player.lua @@ -7,6 +7,7 @@ ---@field general string ---@field handcard_num integer ---@field seat integer +---@field next Player ---@field phase Phase ---@field faceup boolean ---@field chained boolean @@ -50,6 +51,7 @@ function Player:initialize() self.role = "" self.general = "" self.seat = 0 + self.next = nil self.phase = Player.PhaseNone self.faceup = true self.chained = false @@ -240,7 +242,15 @@ end ---@param other Player function Player:distanceTo(other) - local right = math.abs(self.seat - other.seat) + assert(other:isInstanceOf(Player)) + local right = 0 + local temp = self + while temp ~= other do + if not temp.dead then + right = right + 1 + end + temp = temp.next + end local left = #Fk:currentRoom().alive_players - right local ret = math.min(left, right) -- TODO: corrent distance here using skills @@ -260,11 +270,18 @@ function Player:addCardUseHistory(cardName, num) end function Player:resetCardUseHistory(cardName) + if not cardName then + self.cardUsedHistory = {} + end if self.cardUsedHistory[cardName] then self.cardUsedHistory[cardName] = 0 end end +function Player:usedTimes(cardName) + return self.cardUsedHistory[cardName] or 0 +end + function Player:isKongcheng() return #self:getCardIds(Player.Hand) == 0 end @@ -277,6 +294,10 @@ function Player:isAllNude() return #self:getCardIds() == 0 end +function Player:isWounded() + return self.hp < self.maxHp +end + ---@param skill string | Skill ---@return Skill local function getActualSkill(skill) diff --git a/lua/server/event.lua b/lua/server/event.lua index 9d0055e7..eb2effe0 100644 --- a/lua/server/event.lua +++ b/lua/server/event.lua @@ -71,4 +71,12 @@ fk.CardEffecting = 56 fk.CardEffectFinished = 57 fk.CardEffectCancelledOut = 58 -fk.NumOfEvents = 59 +fk.AskForPeaches = 59 +fk.AskForPeachesDone = 60 +fk.Death = 61 +fk.BuryVictim = 62 +fk.BeforeGameOverJudge = 63 +fk.GameOverJudge = 64 +fk.GameFinished = 65 + +fk.NumOfEvents = 66 diff --git a/lua/server/gamelogic.lua b/lua/server/gamelogic.lua index 6decba99..76baab26 100644 --- a/lua/server/gamelogic.lua +++ b/lua/server/gamelogic.lua @@ -74,7 +74,7 @@ function GameLogic:chooseGenerals() room:broadcastProperty(lord, "general") end - local nonlord = room:getOtherPlayers(lord) + local nonlord = room:getOtherPlayers(lord, true) local generals = Fk:getGeneralsRandomly(#nonlord * 3, Fk.generals, {lord_general}) table.shuffle(generals) for _, p in ipairs(nonlord) do @@ -150,7 +150,7 @@ function GameLogic:action() self:trigger(fk.GameStart) local room = self.room - for _, p in ipairs(room.players) do + for _, p in ipairs(room.alive_players) do self:trigger(fk.DrawInitialCards, p, { num = 4 }) end diff --git a/lua/server/room.lua b/lua/server/room.lua index 21b79204..13f2e647 100644 --- a/lua/server/room.lua +++ b/lua/server/room.lua @@ -49,7 +49,14 @@ function Room:initialize(_room) self.room.startGame = function(_self) Room.initialize(self, _room) -- clear old data - self:run() + local co_func = function() + self:run() + end + local co = coroutine.create(co_func) + while not self.game_finished do + coroutine.resume(co) + end + fk.qInfo("Game Finished.") end self.players = {} @@ -126,19 +133,18 @@ function Room:deadPlayerFilter(playerIds) return newPlayerIds end ----@param sortBySeat boolean ---@return ServerPlayer[] -function Room:getAlivePlayers(sortBySeat) - sortBySeat = sortBySeat or true - - local alivePlayers = {} - for _, player in ipairs(self.players) do - if player:isAlive() then - table.insert(alivePlayers, player) +function Room:getAlivePlayers() + local current = self.current + local temp = current.next + local ret = {current} + while temp ~= current do + if not temp.dead then + table.insert(ret, temp) end + temp = temp.next end - - return alivePlayers + return ret end ---@param player ServerPlayer @@ -169,8 +175,13 @@ end ---@param expect ServerPlayer ---@return ServerPlayer[] -function Room:getOtherPlayers(expect) - local ret = {table.unpack(self.players)} +function Room:getOtherPlayers(expect, include_dead) + local ret + if include_dead then + ret = {table.unpack(self.players)} + else + ret = {table.unpack(self.alive_players)} + end table.removeOne(ret, expect) return ret end @@ -391,6 +402,25 @@ function Room:sendLog(log) self:doBroadcastNotify("GameLog", json.encode(log)) end +function Room:doAnimate(type, data, players) + players = players or self.players + data.type = type + self:doBroadcastNotify("Animate", json.encode(data), players) +end + +function Room:setEmotion(player, name) + self:doAnimate("Emotion", { + player = player.id, + emotion = name + }) +end + +function Room:sendLogEvent(type, data, players) + players = players or self.players + data.type = type + self:doBroadcastNotify("LogEvent", json.encode(data), players) +end + ------------------------------------------------------------------------ -- interactive functions ------------------------------------------------------------------------ @@ -465,14 +495,7 @@ function Room:askForDiscard(player, minNum, maxNum, includeEquip, skillName) end end - self:moveCards({ - ids = toDiscard, - from = player.id, - toArea = Card.DiscardPile, - moveReason = fk.ReasonDiscard, - proposer = player.id, - skillName = skillName - }) + self:throwCard(toDiscard, skillName, player, player) end ---@param player ServerPlayer @@ -644,7 +667,7 @@ function Room:askForNullification(players, card_name, prompt, cancelable, extra_ extra_data = extra_data or {} prompt = prompt or "#AskForUseCard" - self:notifyMoveFocus(self.players, card_name) + self:notifyMoveFocus(self.alive_players, card_name) self:doBroadcastNotify("WaitForNullification", "") local data = {card_name, prompt, cancelable, extra_data} @@ -776,13 +799,57 @@ end ---@param cardUseEvent CardUseStruct ---@return boolean function Room:useCard(cardUseEvent) + local from = cardUseEvent.customFrom or cardUseEvent.from self:moveCards({ ids = { cardUseEvent.cardId }, - from = cardUseEvent.customFrom or cardUseEvent.from, + from = from, toArea = Card.Processing, moveReason = fk.ReasonUse, }) - + + self:setEmotion(self:getPlayerById(from), Fk:getCardById(cardUseEvent.cardId).name) + self:doAnimate("Indicate", { + from = from, + to = cardUseEvent.tos or {}, + }) + if cardUseEvent.tos then + local to = {} + for _, t in ipairs(cardUseEvent.tos) do + table.insert(to, t[1]) + end + self:sendLog{ + type = "#UseCardToTargets", + from = from, + to = to, + card = {cardUseEvent.cardId}, + } + for _, t in ipairs(cardUseEvent.tos) do + if t[2] then + local temp = {table.unpack(t)} + table.remove(temp, 1) + self:sendLog{ + type = "#CardUseCollaborator", + from = t[1], + to = temp, + card = {cardUseEvent.cardId}, + } + end + end + elseif cardUseEvent.toCardId then + self:sendLog{ + type = "#UseCardToCard", + from = from, + card = {cardUseEvent.cardId}, + arg = Fk:getCardById(cardUseEvent.toCardId).name, + } + else + self:sendLog{ + type = "#UseCard", + from = from, + card = {cardUseEvent.cardId}, + } + end + if Fk:getCardById(cardUseEvent.cardId).skill then Fk:getCardById(cardUseEvent.cardId).skill:onUse(self, cardUseEvent) end @@ -992,7 +1059,7 @@ function Room:doCardEffect(cardEffectEvent) end elseif Fk:getCardById(cardEffectEvent.cardId).type == Card.TypeTrick then local players = {} - for _, p in ipairs(self.players) do + for _, p in ipairs(self.alive_players) do local cards = p.player_cards[Player.Hand] for _, cid in ipairs(cards) do if Fk:getCardById(cid).name == "nullification" then @@ -1071,7 +1138,7 @@ function Room:moveCards(...) return false end - self:notifyMoveCards(self.players, cardsMoveStructs) + self:notifyMoveCards(nil, cardsMoveStructs) for _, data in ipairs(cardsMoveStructs) do if #data.moveInfo > 0 then @@ -1227,6 +1294,54 @@ function Room:changeHp(player, num, reason, skillName, damageStruct) assert(not (data.reason == "recover" and data.num < 0)) player.hp = math.min(player.hp + data.num, player.maxHp) + self:broadcastProperty(player, "hp") + + if reason == "damage" then + local damage_nature_table = { + [fk.NormalDamage] = "normal_damage", + [fk.FireDamage] = "fire_damage", + [fk.ThunderDamage] = "thunder_damage", + } + if damageStruct.from then + self:sendLog{ + type = "#Damage", + to = {damageStruct.from}, + from = player.id, + arg = 0 - num, + arg2 = damage_nature_table[damageStruct.damageType], + } + else + self:sendLog{ + type = "#DamageWithNoFrom", + from = player.id, + arg = 0 - num, + arg2 = damage_nature_table[damageStruct.damageType], + } + end + self:sendLogEvent("Damage", { + to = player.id, + damageType = damage_nature_table[damageStruct.damageType], + }) + elseif reason == "loseHp" then + self:sendLog{ + type = "#LoseHP", + from = player.id, + arg = 0 - num, + } + elseif reason == "recover" then + self:sendLog{ + type = "#HealHP", + from = player.id, + arg = num, + } + end + + self:sendLog{ + type = "#ShowHPAndMaxHP", + from = player.id, + arg = player.hp, + arg2 = player.maxHp, + } self.logic:trigger(fk.HpChanged, player, data) @@ -1281,6 +1396,7 @@ function Room:changeMaxHp(player, num) end player.maxHp = math.max(player.maxHp + num, 0) + self:broadcastProperty(player, "maxHp") local diff = player.hp - player.maxHp if diff > 0 then if not self:changeHp(player, -diff) then @@ -1328,7 +1444,7 @@ function Room:damage(damageStruct) if not self:changeHp(victim, -damageStruct.damage, "damage", damageStruct.skillName, damageStruct) then return false - end + end stages = { [fk.Damage] = damageStruct.from, @@ -1368,35 +1484,58 @@ end function Room:enterDying(dyingStruct) local dyingPlayer = self:getPlayerById(dyingStruct.who) dyingPlayer.dying = true + self:broadcastProperty(dyingPlayer, "dying") + self:sendLog{ + type = "#EnterDying", + from = dyingPlayer.id, + } 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 - ---@type DeathStruct - local deathData = { - who = dyingPlayer.id, - damage = dyingStruct.damage, - } - self:killPlayer(deathData) - end + self.logic:trigger(fk.Dying, dyingPlayer, dyingStruct) + self.logic:trigger(fk.AskForPeaches, dyingPlayer, dyingStruct) + self.logic:trigger(fk.AskForPeachesDone, dyingPlayer, dyingStruct) end + if not dyingPlayer.dead then + dyingPlayer.dying = false + self:broadcastProperty(dyingPlayer, "dying") + 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() + local victim = self:getPlayerById(deathStruct.who) + victim.dead = true + table.removeOne(self.alive_players, victim) + + local logic = self.logic + logic:trigger(fk.BeforeGameOverJudge, victim, deathStruct) + + local killer = deathStruct.damage and deathStruct.damage.from or nil + if killer then + self:sendLog{ + type = "#KillPlayer", + to = {killer}, + from = victim.id, + arg = victim.role, + } + else + self:sendLog{ + type = "#KillPlayerWithNoKiller", + from = victim.id, + arg = victim.role, + } + end + self:sendLogEvent("Death", {to = victim.id}) + + self:broadcastProperty(victim, "role") + self:broadcastProperty(victim, "dead") + + logic:trigger(fk.GameOverJudge, victim, deathStruct) + logic:trigger(fk.Death, victim, deathStruct) + logic:trigger(fk.BuryVictim, victim, deathStruct) end -- lose/acquire skill actions @@ -1502,6 +1641,26 @@ function Room:judge(data) end end +---@param card_ids integer[] +---@param skillName string +---@param who ServerPlayer +---@param thrower ServerPlayer +function Room:throwCard(card_ids, skillName, who, thrower) + if type(card_ids) == "number" then + card_ids = {card_ids} + end + skillName = skillName or "" + thrower = thrower or who + self:moveCards({ + ids = card_ids, + from = who.id, + toArea = Card.DiscardPile, + moveReason = fk.ReasonDiscard, + proposer = thrower.id, + skillName = skillName + }) +end + -- other helpers function Room:adjustSeats() @@ -1545,10 +1704,16 @@ function Room:shuffleDrawPile() table.shuffle(self.draw_pile) end -function Room:gameOver() +function Room:gameOver(winner) self.game_finished = true - -- dosomething + + for _, p in ipairs(self.players) do + self:broadcastProperty(p, "role") + end + self:doBroadcastNotify("GameOver", winner) + self.room:gameOver() + coroutine.yield() end function CreateRoom(_room) diff --git a/lua/server/serverplayer.lua b/lua/server/serverplayer.lua index e301487e..1404bb22 100644 --- a/lua/server/serverplayer.lua +++ b/lua/server/serverplayer.lua @@ -19,8 +19,6 @@ function ServerPlayer:initialize(_self) self.state = _self:getStateString() self.room = nil - self.next = nil - -- Below are for doBroadcastRequest self.request_data = "" self.client_reply = "" @@ -208,4 +206,44 @@ function ServerPlayer:play(phase_table) end end +function ServerPlayer:drawCards(num, skillName, fromPlace) + return self.room:drawCards(self, num, skillName, fromPlace) +end + +function ServerPlayer:bury() + -- self:clearFlags() + -- self:clearHistory() + self:throwAllCards() + -- self:throwAllMarks() + -- self:clearPiles() + + -- self.room:clearPlayerCardLimitation(self, false) +end + +function ServerPlayer:throwAllCards(flag) + local room = self.room + flag = flag or "hej" + if string.find(flag, "h") then + room:throwCard(self.player_cards[Player.Hand], "", self) + end + + if string.find(flag, "e") then + room:throwCard(self.player_cards[Player.Equip], "", self) + end + + if string.find(flag, "j") then + room:throwCard(self.player_cards[Player.Judge], "", self) + end +end + +function ServerPlayer:addCardUseHistory(cardName, num) + Player.addCardUseHistory(self, cardName, num) + self:doNotify("AddCardUseHistory", json.encode{cardName, num}) +end + +function ServerPlayer:resetCardUseHistory(cardName) + Player.resetCardUseHistory(self, cardName) + self:doNotify("ResetCardUseHistory", cardName or "") +end + return ServerPlayer diff --git a/packages/standard/game_rule.lua b/packages/standard/game_rule.lua index e4ea2188..0953caf7 100644 --- a/packages/standard/game_rule.lua +++ b/packages/standard/game_rule.lua @@ -1,8 +1,48 @@ +---@param victim ServerPlayer +local function getWinner(victim) + local room = victim.room + local winner = "" + local alive = room.alive_players + + if victim.role == "lord" then + if #alive == 1 and alive[1].role == "renegade" then + winner = "renegede" + else + winner = "rebel" + end + elseif victim.role ~= "loyalist" then + local lord_win = true + for _, p in ipairs(alive) do + if p.role == "rebel" or p.role == "renegade" then + lord_win = false + break + end + end + if lord_win then + winner = "lord+loyalist" + end + end + + return winner +end + +---@param killer ServerPlayer +local function rewardAndPunish(killer, victim) + if killer.dead then return end + if victim.role == "rebel" then + killer:drawCards(3, "kill") + elseif victim.role == "loyalist" and killer.role == "lord" then + killer:throwAllCards("he") + end +end + GameRule = fk.CreateTriggerSkill{ name = "game_rule", events = { fk.GameStart, fk.DrawInitialCards, fk.TurnStart, fk.EventPhaseProceeding, fk.EventPhaseEnd, fk.EventPhaseChanging, + fk.AskForPeaches, fk.AskForPeachesDone, + fk.GameOverJudge, fk.BuryVictim, }, priority = 0, @@ -40,7 +80,7 @@ GameRule = fk.CreateTriggerSkill{ table.insert(move_to_notify.moveInfo, { cardId = id, fromArea = Card.DrawPile }) end - room:notifyMoveCards(room.players, {move_to_notify}) + room:notifyMoveCards(nil, {move_to_notify}) for _, id in ipairs(cardIds) do room:setCardArea(id, Card.PlayerHand, player.id) @@ -129,12 +169,52 @@ GameRule = fk.CreateTriggerSkill{ end, [fk.EventPhaseEnd] = function() if player.phase == Player.Play then - -- TODO: clear history + player:resetCardUseHistory() end end, [fk.EventPhaseChanging] = function() -- TODO: copy but dont copy all end, + [fk.AskForPeaches] = function() + local savers = room:getAlivePlayers() + for _, p in ipairs(savers) do + if player.hp > 0 or player.dead then break end + while player.hp < 1 do + local peach_use = room:askForUseCard(p, "peach") + if not peach_use then break end + peach_use.tos = { {player.id} } + room:useCard(peach_use) + end + end + end, + [fk.AskForPeachesDone] = function() + if player.hp < 1 then + ---@type DeathStruct + local deathData = { + who = player.id, + damage = data.damage, + } + room:killPlayer(deathData) + end + end, + [fk.GameOverJudge] = function() + local winner = getWinner(player) + if winner ~= "" then + room:gameOver(winner) + return true + end + end, + [fk.BuryVictim] = function() + player:bury() + if room.tag["SkipNormalDeathProcess"] then + return false + end + local damage = data.damage + if damage and damage.from then + local killer = room:getPlayerById(damage.from) + rewardAndPunish(killer, player); + end + end, default = function() print("game_rule: Event=" .. event) room:askForSkillInvoke(player, "rule") diff --git a/packages/standard/init.lua b/packages/standard/init.lua index 673cf81d..1d14b2b6 100644 --- a/packages/standard/init.lua +++ b/packages/standard/init.lua @@ -101,7 +101,9 @@ local zhiheng = fk.CreateActiveSkill{ return #selected == 0 and #selected_cards > 0 end, on_effect = function(self, room, effect) - room:drawCards(room:getPlayerById(effect.from), #effect.cards, "zhiheng") + local from = room:getPlayerById(effect.from) + room:throwCard(effect.cards, self.name, from) + room:drawCards(from, #effect.cards, self.name) end } local sunquan = General:new(extension, "sunquan", "wu", 4) diff --git a/packages/standard_cards/init.lua b/packages/standard_cards/init.lua index 25785201..e1f2ba01 100644 --- a/packages/standard_cards/init.lua +++ b/packages/standard_cards/init.lua @@ -7,26 +7,31 @@ Fk:loadTranslationTable{ local slashSkill = fk.CreateActiveSkill{ name = "slash_skill", + can_use = function(self, player) + -- TODO: tmd skill + return player:usedTimes("slash") < 1 + end, target_filter = function(self, to_select, selected) if #selected == 0 then local player = Fk:currentRoom():getPlayerById(to_select) - return Self ~= player + return Self ~= player and Self:inMyAttackRange(player) end end, feasible = function(self, selected) + -- TODO: tmd return #selected == 1 end, on_effect = function(self, room, effect) local to = effect.to local from = effect.from - local cid = room:askForCardChosen( - room:getPlayerById(from), - room:getPlayerById(to), - "hej", - "snatch" - ) - - room:obtainCard(from, cid) + + room:damage({ + from = from, + to = to, + damage = 1, + damageType = fk.NormalDamage, + skillName = self.name + }) end } local slash = fk.CreateBasicCard{ @@ -115,10 +120,28 @@ extension:addCards({ jink:clone(Card.Diamond, 11), }) +local peachSkill = fk.CreateActiveSkill{ + name = "peach_skill", + can_use = function(self, player) + return player:isWounded() + end, + on_effect = function(self, room, effect) + local to = effect.to + local from = effect.from + + room:recover{ + who = to, + num = 1, + recoverBy = from, + skillName = self.name + } + end +} local peach = fk.CreateBasicCard{ name = "peach", suit = Card.Heart, number = 3, + skill = peachSkill, } Fk:loadTranslationTable{ ["peach"] = "桃", @@ -261,6 +284,9 @@ extension:addCards({ local nullificationSkill = fk.CreateActiveSkill{ name = "nullification_skill", + can_use = function() + return false + end, on_effect = function(self, room, effect) if effect.responseToEvent then effect.responseToEvent.isCancellOut = true @@ -374,6 +400,7 @@ local crossbow = fk.CreateWeapon{ name = "crossbow", suit = Card.Club, number = 1, + attack_range = 1, } Fk:loadTranslationTable{ ["crossbow"] = "诸葛连弩", @@ -388,6 +415,7 @@ local qingGang = fk.CreateWeapon{ name = "qinggang_sword", suit = Card.Spade, number = 6, + attack_range = 2, } Fk:loadTranslationTable{ ["qinggang_sword"] = "青釭剑", @@ -401,6 +429,7 @@ local iceSword = fk.CreateWeapon{ name = "ice_sword", suit = Card.Spade, number = 2, + attack_range = 2, } Fk:loadTranslationTable{ ["ice_sword"] = "寒冰剑", @@ -414,6 +443,7 @@ local doubleSwords = fk.CreateWeapon{ name = "double_swords", suit = Card.Spade, number = 2, + attack_range = 2, } Fk:loadTranslationTable{ ["double_swords"] = "雌雄双股剑", @@ -427,6 +457,7 @@ local blade = fk.CreateWeapon{ name = "blade", suit = Card.Spade, number = 5, + attack_range = 3, } Fk:loadTranslationTable{ ["blade"] = "青龙偃月刀", @@ -440,6 +471,7 @@ local spear = fk.CreateWeapon{ name = "spear", suit = Card.Spade, number = 12, + attack_range = 3, } Fk:loadTranslationTable{ ["spear"] = "丈八蛇矛", @@ -453,6 +485,7 @@ local axe = fk.CreateWeapon{ name = "axe", suit = Card.Diamond, number = 5, + attack_range = 3, } Fk:loadTranslationTable{ ["axe"] = "贯石斧", @@ -466,6 +499,7 @@ local halberd = fk.CreateWeapon{ name = "halberd", suit = Card.Diamond, number = 12, + attack_range = 4, } Fk:loadTranslationTable{ ["halberd"] = "方天画戟", @@ -479,6 +513,7 @@ local kylinBow = fk.CreateWeapon{ name = "kylin_bow", suit = Card.Heart, number = 5, + attack_range = 5, } Fk:loadTranslationTable{ ["kylin_bow"] = "麒麟弓", diff --git a/qml/Pages/Room.qml b/qml/Pages/Room.qml index 09ce90e6..ea6037d7 100644 --- a/qml/Pages/Room.qml +++ b/qml/Pages/Room.qml @@ -4,6 +4,8 @@ import QtQuick.Layouts import "Common" import "RoomElement" import "RoomLogic.js" as Logic +import "skin-bank.js" as SkinBank + Item { id: roomScene @@ -159,7 +161,7 @@ Item { maxHp: model.maxHp hp: model.hp seatNumber: model.seatNumber - isDead: model.isDead + dead: model.dead dying: model.dying faceup: model.faceup chained: model.chained @@ -224,7 +226,7 @@ Item { self.maxHp: dashboardModel.maxHp self.hp: dashboardModel.hp self.seatNumber: dashboardModel.seatNumber - self.isDead: dashboardModel.isDead + self.dead: dashboardModel.dead self.dying: dashboardModel.dying self.faceup: dashboardModel.faceup self.chained: dashboardModel.chained @@ -459,6 +461,15 @@ Item { } } + Shortcut { + sequence: "D" + property bool show_distance: false + onActivated: { + show_distance = !show_distance; + showDistance(show_distance); + } + } + Shortcut { sequence: "Esc" onActivated: { @@ -491,6 +502,18 @@ Item { log.append(msg); } + function showDistance(show) { + for (let i = 0; i < photoModel.count; i++) { + let item = photos.itemAt(i); + if (show) { + let dis = Backend.callLuaFunction("DistanceTo",[Self.id, item.playerid]); + item.distance = parseInt(dis); + } else { + item.distance = 0; + } + } + } + Component.onCompleted: { toast.show(Backend.translate("$EnterRoom")); @@ -504,7 +527,7 @@ Item { maxHp: 0, hp: 0, seatNumber: 1, - isDead: false, + dead: false, dying: false, faceup: true, chained: false, @@ -527,7 +550,7 @@ Item { maxHp: 0, hp: 0, seatNumber: i + 1, - isDead: false, + dead: false, dying: false, faceup: true, chained: false, diff --git a/qml/Pages/RoomElement/Dashboard.qml b/qml/Pages/RoomElement/Dashboard.qml index a725a4d9..6f334875 100644 --- a/qml/Pages/RoomElement/Dashboard.qml +++ b/qml/Pages/RoomElement/Dashboard.qml @@ -255,4 +255,8 @@ RowLayout { for (let i = 0; i < skillButtons.count; i++) skillButtons.itemAt(i).enabled = false; } + + function tremble() { + selfPhoto.tremble(); + } } diff --git a/qml/Pages/RoomElement/GameOverBox.qml b/qml/Pages/RoomElement/GameOverBox.qml new file mode 100644 index 00000000..3b938482 --- /dev/null +++ b/qml/Pages/RoomElement/GameOverBox.qml @@ -0,0 +1,32 @@ +import QtQuick +import ".." + +GraphicsBox { + property string winner: "" + + id: root + title.text: Backend.translate("$GameOver") + width: Math.max(140, body.width + 20) + height: body.height + title.height + 20 + + Column { + id: body + x: 10 + y: title.height + 5 + spacing: 10 + + Text { + text: Backend.translate("$Winner").arg(Backend.translate(winner)) + color: "#E4D5A0" + } + + MetroButton { + text: Backend.translate("Back To Lobby") + anchors.horizontalCenter: parent.horizontalCenter + + onClicked: { + ClientInstance.notifyServer("QuitRoom", "[]"); + } + } + } +} diff --git a/qml/Pages/RoomElement/Photo.qml b/qml/Pages/RoomElement/Photo.qml index 8482fd5e..d5d937a9 100644 --- a/qml/Pages/RoomElement/Photo.qml +++ b/qml/Pages/RoomElement/Photo.qml @@ -19,12 +19,13 @@ Item { property int maxHp: 0 property int hp: 0 property int seatNumber: 1 - property bool isDead: false + property bool dead: false property bool dying: false property bool faceup: true property bool chained: false property bool drank: false property bool isOwner: false + property int distance: 0 property string status: "normal" property alias handcardArea: handcardAreaItem @@ -164,7 +165,7 @@ Item { anchors.fill: photoMask source: generalImage saturation: 0 - visible: root.isDead + visible: root.dead } Image { @@ -212,8 +213,8 @@ Item { Image { // id: saveme - visible: root.isDead || root.dying - source: SkinBank.DEATH_DIR + (root.isDead ? root.role : "saveme") + visible: root.dead || root.dying + source: SkinBank.DEATH_DIR + (root.dead ? root.role : "saveme") anchors.centerIn: photoMask } @@ -410,6 +411,17 @@ Item { } } + Rectangle { + color: "white" + height: 20 + width: 20 + visible: distance != 0 + Text { + text: distance + anchors.centerIn: parent + } + } + onGeneralChanged: { if (!roomScene.isStarted) return; generalName.text = Backend.translate(general); diff --git a/qml/Pages/RoomLogic.js b/qml/Pages/RoomLogic.js index 552df9b6..6a51e72e 100644 --- a/qml/Pages/RoomLogic.js +++ b/qml/Pages/RoomLogic.js @@ -170,6 +170,14 @@ function moveCards(moves) { } function setEmotion(id, emotion) { + let path = (SkinBank.PIXANIM_DIR + emotion).replace("file://", ""); + if (!Backend.exists(path)) { + return; + } + if (!Backend.isDir(path)) { + // TODO: set picture emotion + return; + } let component = Qt.createComponent("RoomElement/PixmapAnimation.qml"); if (component.status !== Component.Ready) return; @@ -183,7 +191,8 @@ function setEmotion(id, emotion) { } } - let animation = component.createObject(photo, {source: emotion, anchors: {centerIn: photo}}); + let animation = component.createObject(photo, {source: emotion}); + animation.anchors.centerIn = photo; animation.finished.connect(() => animation.destroy()); animation.start(); } @@ -644,3 +653,47 @@ callbacks["AskForResponseCard"] = function(jsonData) { callbacks["WaitForNullification"] = function() { roomScene.state = "notactive"; } + +callbacks["Animate"] = function(jsonData) { + // jsonData: [Object object] + let data = JSON.parse(jsonData); + switch (data.type) { + case "Indicate": + data.to.forEach(item => { + doIndicate(data.from, [item[0]]); + if (item[1]) { + doIndicate(item[0], item.slice(1)); + } + }) + break; + case "Emotion": + setEmotion(data.player, data.emotion); + break; + case "LightBox": + break; + case "SuperLightBox": + break; + default: + break; + } +} + +callbacks["LogEvent"] = function(jsonData) { + // jsonData: [Object object] + let data = JSON.parse(jsonData); + switch (data.type) { + case "Damage": + let item = getPhotoOrDashboard(data.to); + setEmotion(data.to, "damage"); + item.tremble(); + default: + break; + } +} + +callbacks["GameOver"] = function(jsonData) { + roomScene.state = "notactive"; + roomScene.popupBox.source = "RoomElement/GameOverBox.qml"; + let box = roomScene.popupBox.item; + box.winner = jsonData; +} diff --git a/src/core/util.cpp b/src/core/util.cpp index c1eadb2b..75782c50 100644 --- a/src/core/util.cpp +++ b/src/core/util.cpp @@ -159,8 +159,9 @@ static void writeFileMD5(QFile &dest, const QString &fname) { } auto data = f.readAll(); + data.replace(QByteArray("\r\n"), QByteArray("\n")); auto hash = QCryptographicHash::hash(data, QCryptographicHash::Md5).toHex(); - dest.write(fname.toUtf8() + '=' + hash + '\n'); + dest.write(fname.toUtf8() + '=' + hash + ';'); } static void writeDirMD5(QFile &dest, const QString &dir, const QString &filter) { @@ -186,6 +187,7 @@ QString calcFileMD5() { qFatal("Cannot open flist.txt. Quitting."); } + writeDirMD5(flist, "packages", "*.lua"); writeDirMD5(flist, "lua", "*.lua"); writeDirMD5(flist, "qml", "*.qml"); writeDirMD5(flist, "qml", "*.js"); diff --git a/src/server/serverplayer.cpp b/src/server/serverplayer.cpp index 26491ce1..f6417f79 100644 --- a/src/server/serverplayer.cpp +++ b/src/server/serverplayer.cpp @@ -98,7 +98,9 @@ QString ServerPlayer::waitForReply(int timeout) { QString ret; if (getState() != Player::Online) { +#ifndef QT_DEBUG QThread::sleep(1); +#endif ret = "__cancel"; } else { ret = router->waitForReply(timeout);