diff --git a/docs/dev/hegemony.rst b/docs/dev/hegemony.rst new file mode 100644 index 00000000..01e47ea4 --- /dev/null +++ b/docs/dev/hegemony.rst @@ -0,0 +1,71 @@ +.. SPDX-License-Identifier: GFDL-1.3-or-later + +国战实现相关碎碎念 +================= + +国战流程与身份版基本相同,就有一些有区别: + +- 双将,并且是同势力双将 +- 开局所有人暗将状态 +- 胜利判断和奖惩不同 +- 亮将 +- 预亮技能 + +下面来一个个分析一下。胜利和奖惩已经在22写过了,不分析。 + +双将与同势力选将 +----------- + +双将好说,已经做完了。主要是同势力选将,可能需要单独写个处理函数吧。还有抽武将的时候也要避免出现没人同势力的情况啊。 + +简而言之就是UI加个同势力选项;同时,随机生成武将环节中如果可选人数小于X+1(X为游戏内势力数,一般为4),那么就抽X+1张,然后去掉多余的。 + +暗将与亮将 +---- + +首先不要亮出武将,即别broadcast general,或者把general先设为暗将。 + +每个玩家选完武将后可以用两个标记保存着自己实际的选将。而实际上会用暗将来代替选好的武将。 + +那设置体力上限也得重写了,总之就是GameLogic中选将和准备开始都需要重写呢。 + +暗将是没有任何技能的。但是亮将后,就可以用changeHero修改武将牌,然后就真的获得了该获得的所有技能了。 + +预亮 +---- + +实现国战模式最大的难点。 + +预亮技能可以激活武将的某个技能,这样游戏会对该技能进行询问。 + +既然是询问,那么能预亮的技能只会是触发技咯,或者附带有隐藏触发技的技能。 + +那么,能预亮的技能从哪来?暗将可是个白板,没有任何技能的。 + +答案是游戏开始前公布武将(也就是公布暗将)环节的时候,用doNotify告诉每个玩家都获得了哪些技能。事实上呢,(在服务端这边)他们根本没有这些技能呢。但这样又有个问题,有些技能是在客户端有判断的,这意味着暗将的马术可能可以发挥作用。 + +解决办法是写个global的失效技令暗将的所有未预亮技能失效。然后在亮将后,先哟doNotify让玩家认为自己失去所有技能,再用changeHero变将,changeHero函数中有替换技能的代码。 + +那么预亮技能怎么处理呢?预亮和游戏流程没啥关系,纯属玩家想预亮就预亮想取消就取消。既然与游戏流程无关,那么这块就得用到异步IO了,pushRequest解决。 + +然后技能只要被预亮了,那么服务端就知道有这个技能了,先挂给Room。然后也给Player加上技能。此时只在服务端加技能!不对,不能加技能啊,把技能贴给logic就行了啊。那我既然要询问的话肯定得满足hasSkill啊。怎么办呢?果然还是要加给玩家啊,但又怕状态技捣乱,那就让状态技失效呗。 + +这样一来基本解决了预亮的游戏逻辑问题。接下来有一点要操心的是UI。首先对于有状态技的,找个地方塞个亮将按钮。手刹直接是在技能区加这个按钮了,那正好可以做个亮将技能啊。 + +然后就是UI了。这个得走cpp了谁让Lua服务端没有这种处理函数呢?(也根本不应该有,Lua和处理消息根本不在一个线程好吧)先不管这些,总之在暗将状态下,对于服务端notify到的触发型技能,给个预亮按钮。就换个按钮样式而已。主动和视为因为不影响烧条就照旧处理。就触发技弄成预亮键。视为技拼触发技的情况也弄成预亮hhh。 + +变回暗将 +----- + +点名批评邹氏 + +changeHero成暗将然后notify获得即可。两人都暗了的话还要变势力。 + +明将就是先notify失去再changeHero。 + +调虎离山 +----- + +将目标设为死人即可(删除) + +真正实现的话,可能还是就按照牌面描述来吧,但是要对距离计算和上家/下家做修改。前者可能还能用距离技顶一下,后者非改源码不可。 diff --git a/docs/dev/index.rst b/docs/dev/index.rst index 23666dd0..7b98a3d3 100644 --- a/docs/dev/index.rst +++ b/docs/dev/index.rst @@ -16,3 +16,4 @@ Dev文档 scenario.rst todo.rst ui.rst + hegemony.rst diff --git a/image/button/skill/prelight.png b/image/button/skill/prelight.png new file mode 100644 index 00000000..c62edae0 Binary files /dev/null and b/image/button/skill/prelight.png differ diff --git a/image/button/skill/prelight/disabled.png b/image/button/skill/prelight/disabled.png new file mode 100644 index 00000000..0f4d9122 Binary files /dev/null and b/image/button/skill/prelight/disabled.png differ diff --git a/image/button/skill/prelight/normal.png b/image/button/skill/prelight/normal.png new file mode 100644 index 00000000..50e5b71a Binary files /dev/null and b/image/button/skill/prelight/normal.png differ diff --git a/image/button/skill/prelight/pressed.png b/image/button/skill/prelight/pressed.png new file mode 100644 index 00000000..09edf96d Binary files /dev/null and b/image/button/skill/prelight/pressed.png differ diff --git a/image/button/skill/unprelight.png b/image/button/skill/unprelight.png new file mode 100644 index 00000000..de072afe Binary files /dev/null and b/image/button/skill/unprelight.png differ diff --git a/image/card/general/unknown-magatama.png b/image/card/general/unknown-magatama.png new file mode 100644 index 00000000..3d3949c9 Binary files /dev/null and b/image/card/general/unknown-magatama.png differ diff --git a/image/card/general/unknown.png b/image/card/general/unknown.png new file mode 100644 index 00000000..3d3949c9 Binary files /dev/null and b/image/card/general/unknown.png differ diff --git a/image/photo/back/unknown.png b/image/photo/back/unknown.png new file mode 100644 index 00000000..b04e2937 Binary files /dev/null and b/image/photo/back/unknown.png differ diff --git a/image/photo/back/wild.png b/image/photo/back/wild.png new file mode 100644 index 00000000..159e21b2 Binary files /dev/null and b/image/photo/back/wild.png differ diff --git a/image/photo/death/unknown.png b/image/photo/death/hidden.png similarity index 100% rename from image/photo/death/unknown.png rename to image/photo/death/hidden.png diff --git a/lua/client/client.lua b/lua/client/client.lua index d14a7467..b1255f38 100644 --- a/lua/client/client.lua +++ b/lua/client/client.lua @@ -113,10 +113,20 @@ function Client:appendLog(msg) if not pid then return "" end - local ret = self:getPlayerById(pid) - ret = ret.general + local p = self:getPlayerById(pid) + local str = '%s' + if p.general == "anjiang" and (p.deputyGeneral == "anjiang" + or not p.deputyGeneral) then + local ret = Fk:translate("seat#" .. p.seat) + return string.format(str, color, ret) + end + + local ret = p.general ret = Fk:translate(ret) - ret = string.format('%s', ret) + if p.deputyGeneral then + ret = ret .. "/" .. Fk:translate(p.deputyGeneral) + end + ret = string.format(str, color, ret) return ret end @@ -490,10 +500,13 @@ end fk.client_callback["LoseSkill"] = function(jsonData) -- jsonData: [ int player_id, string skill_name ] local data = json.decode(jsonData) - local id, skill_name = data[1], data[2] + local id, skill_name, prelight = data[1], data[2], data[3] local target = ClientInstance:getPlayerById(id) local skill = Fk.skills[skill_name] - target:loseSkill(skill) + if not prelight then + target:loseSkill(skill) + end + if skill.visible then ClientInstance:notifyUI("LoseSkill", jsonData) end diff --git a/lua/client/client_util.lua b/lua/client/client_util.lua index 5abf1268..a0243a87 100644 --- a/lua/client/client_util.lua +++ b/lua/client/client_util.lua @@ -16,6 +16,8 @@ function GetGeneralData(name) hp = general.hp, maxHp = general.maxHp, shield = general.shield, + hidden = general.hidden, + total_hidden = general.total_hidden, } end @@ -108,7 +110,9 @@ end function GetGenerals(pack_name) local ret = {} for _, g in ipairs(Fk.packages[pack_name].generals) do - table.insert(ret, g.name) + if not g.total_hidden then + table.insert(ret, g.name) + end end return json.encode(ret) end diff --git a/lua/client/i18n/zh_CN.lua b/lua/client/i18n/zh_CN.lua index 95d0bc27..8d00d74c 100644 --- a/lua/client/i18n/zh_CN.lua +++ b/lua/client/i18n/zh_CN.lua @@ -150,6 +150,19 @@ FreeKill使用的是libgit2的C API,与此同时使用Git完成拓展包的下 ["#AskForDiscard"] = "请弃置 %arg 张牌,最少 %arg2 张", ["#AskForCard"] = "请选择 %arg 张牌,最少 %arg2 张", ["#askForPindian"] = "请选择一张手牌作为拼点牌", + ["#StartPindianReason"] = "%from 由于 %arg 而发起拼点", + + ["#RevealGeneral"] = "%from 亮出 %arg %arg2", + ["mainGeneral"] = "主将", + ["deputyGeneral"] = "副将", + ["seat#1"] = "一号位", + ["seat#2"] = "二号位", + ["seat#3"] = "三号位", + ["seat#4"] = "四号位", + ["seat#5"] = "五号位", + ["seat#6"] = "六号位", + ["seat#7"] = "七号位", + ["seat#8"] = "八号位", ["Trust"] = "托管", ["Sort Cards"] = "牌序", diff --git a/lua/core/engine.lua b/lua/core/engine.lua index e318d58b..2b24a631 100644 --- a/lua/core/engine.lua +++ b/lua/core/engine.lua @@ -295,7 +295,8 @@ function Engine:getGeneralsRandomly(num, generalPool, except, filter) local availableGenerals = {} for _, general in pairs(generalPool) do if not table.contains(except, general.name) and not (filter and filter(general)) then - if #table.filter(availableGenerals, function(g) + if (not general.hidden and not general.total_hidden) and + #table.filter(availableGenerals, function(g) return g.trueName == general.trueName end) == 0 then table.insert(availableGenerals, general) diff --git a/lua/core/general.lua b/lua/core/general.lua index 5ca667b3..866015cd 100644 --- a/lua/core/general.lua +++ b/lua/core/general.lua @@ -19,6 +19,8 @@ ---@field public other_skills string[] @ 武将身上属于其他武将的技能,通过字符串调用 ---@field public related_skills Skill[] @ 武将相关的不属于其他武将的技能,例如邓艾的急袭 ---@field public related_other_skills string [] @ 武将相关的属于其他武将的技能,例如孙策的英姿 +---@field public hidden boolean +---@field public total_hidden boolean General = class("General") ---@alias Gender integer @@ -78,4 +80,13 @@ function General:addRelatedSkill(skill) end end +function General:getSkillNameList(include_lord) + local ret = table.map(self.skills, Util.NameMapper) + table.insertTable(ret, self.other_skills) + + if not include_lord then + end + return ret +end + return General diff --git a/lua/core/player.lua b/lua/core/player.lua index 010ff50b..7b6e9da1 100644 --- a/lua/core/player.lua +++ b/lua/core/player.lua @@ -118,6 +118,17 @@ function Player:setGeneral(general, setHp, addSkills) end end +function Player:getGeneralMaxHp() + local general = Fk.generals[self:getMark("__heg_general") or self.general] + local deputy = Fk.generals[self:getMark("__heg_deputy") or self.deputy] + + if not deputy then + return general.maxHp + else + return (general.maxHp + deputy.maxHp) // 2 + end +end + --- 查询角色是否存在flag。 ---@param flag string @ 一种标记 function Player:hasFlag(flag) diff --git a/lua/core/skill.lua b/lua/core/skill.lua index 6cf723d7..2f18910d 100644 --- a/lua/core/skill.lua +++ b/lua/core/skill.lua @@ -69,11 +69,9 @@ function Skill:isEquipmentSkill() return self.attached_equip and type(self.attached_equip) == 'string' and self.attached_equip ~= "" end ---- 确认技能是否对特定玩家生效。 +--- 判断技能是不是对于某玩家而言失效了。 --- ---- 据说你一般用不到这个,只要你把on_cost(代价)和on_use(效果)区分得当, ---- ---- 涉及技能无效时,不需要这个函数也可以实现效果。 +--- 它影响的是hasSkill,但也可以单独拿出来判断。 ---@param player Player @ 玩家 ---@return boolean function Skill:isEffectable(player) diff --git a/lua/core/util.lua b/lua/core/util.lua index 04f13b2c..9597a3d5 100644 --- a/lua/core/util.lua +++ b/lua/core/util.lua @@ -69,6 +69,9 @@ Util.Id2CardMapper = function(id) return Fk:getCardById(id) end Util.Id2PlayerMapper = function(id) return Fk:currentRoom():getPlayerById(id) end +Util.NameMapper = function(e) return e.name end +Util.Name2GeneralMapper = function(e) return Fk.generals[e] end +Util.Name2SkillMapper = function(e) return Fk.skills[e] end ---@generic T ---@param self T[] diff --git a/lua/server/events/gameflow.lua b/lua/server/events/gameflow.lua index 3c951b2e..71bca5aa 100644 --- a/lua/server/events/gameflow.lua +++ b/lua/server/events/gameflow.lua @@ -98,20 +98,22 @@ GameEvent.cleaners[GameEvent.Turn] = function(self) end end + local current = room.current + local logic = room.logic if self.interrupted then - room.current.phase = Player.Finish - room.logic:trigger(fk.EventPhaseStart, room.current, nil, true) - room.logic:trigger(fk.EventPhaseEnd, room.current, nil, true) + current.phase = Player.Finish + logic:trigger(fk.EventPhaseStart, current, nil, true) + logic:trigger(fk.EventPhaseEnd, current, nil, true) - room.current.phase = Player.NotActive - room:notifyProperty(room.current, room.current, "phase") - room.logic:trigger(fk.EventPhaseChanging, room.current, + current.phase = Player.NotActive + room:notifyProperty(current, current, "phase") + logic:trigger(fk.EventPhaseChanging, current, { from = Player.Finish, to = Player.NotActive }, true) - room.logic:trigger(fk.EventPhaseStart, room.current, nil, true) + logic:trigger(fk.EventPhaseStart, current, nil, true) - room.current.skipped_phases = {} + current.skipped_phases = {} - room.logic:trigger(fk.TurnEnd, room.current, nil, true) + logic:trigger(fk.TurnEnd, current, nil, true) end end @@ -184,18 +186,15 @@ GameEvent.functions[GameEvent.Phase] = function(self) end, [Player.Finish] = function() - end, - [Player.NotActive] = function() - end, }) end end - if self.phase ~= Player.NotActive then - logic:trigger(fk.EventPhaseEnd, self) + if player.phase ~= Player.NotActive then + logic:trigger(fk.EventPhaseEnd, player) else - self.skipped_phases = {} + player.skipped_phases = {} end end diff --git a/lua/server/gamelogic.lua b/lua/server/gamelogic.lua index a5104a1d..3945787f 100644 --- a/lua/server/gamelogic.lua +++ b/lua/server/gamelogic.lua @@ -39,7 +39,13 @@ function GameLogic:run() self.room:adjustSeats() self:chooseGenerals() + + self:buildPlayerCircle() + self:broadcastGeneral() + self:prepareDrawPile() + self:attachSkillToPlayers() self:prepareForStart() + self.room.game_started = true self:action() end @@ -123,7 +129,7 @@ function GameLogic:chooseGenerals() end end -function GameLogic:prepareForStart() +function GameLogic:buildPlayerCircle() local room = self.room local players = room.players room.alive_players = {table.unpack(players)} @@ -131,6 +137,11 @@ function GameLogic:prepareForStart() players[i].next = players[i + 1] end players[#players].next = players[1] +end + +function GameLogic:broadcastGeneral() + local room = self.room + local players = room.players for _, p in ipairs(players) do assert(p.general ~= "") @@ -153,13 +164,21 @@ function GameLogic:prepareForStart() room:broadcastProperty(p, "hp") room:broadcastProperty(p, "shield") end +end +function GameLogic:prepareDrawPile() + local room = self.room 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, nil) end +end + +function GameLogic:attachSkillToPlayers() + local room = self.room + local players = room.players local addRoleModSkills = function(player, skillName) local skill = Fk.skills[skillName] @@ -189,6 +208,11 @@ function GameLogic:prepareForStart() end end end +end + +function GameLogic:prepareForStart() + local room = self.room + local players = room.players self:addTriggerSkill(GameRule) for _, trig in ipairs(Fk.global_trigger) do diff --git a/lua/server/room.lua b/lua/server/room.lua index 91ac0d58..b18d84ee 100644 --- a/lua/server/room.lua +++ b/lua/server/room.lua @@ -444,6 +444,47 @@ function Room:setDeputyGeneral(player, general) self:notifyProperty(player, player, "deputyGeneral") end +---@param player ServerPlayer @ 要换将的玩家 +---@param new_general string @ 要变更的武将,若不存在则变身为孙策,孙策也不存在则nil错 +---@param full boolean @ 是否血量满状态变身 +---@param isDeputy boolean @ 是否变的是副将 +---@param sendLog boolean @ 是否发Log +function Room:changeHero(player, new_general, full, isDeputy, sendLog) + local orig = isDeputy and (player.deputyGeneral or "") or player.general + + orig = Fk.generals[orig] + local orig_skills = orig and orig:getSkillNameList() or {} + + local new = Fk.generals[new_general] or Fk.generals["sunce"] + local new_skills = new:getSkillNameList() + + table.insertTable(new_skills, table.map(orig_skills, function(e) + return "-" .. e + end)) + + self:handleAddLoseSkills(player, table.concat(new_skills, "|"), nil, false) + + if isDeputy then + player.deputyGeneral = new_general + self:broadcastProperty(player, "deputyGeneral") + else + player.general = new_general + self:broadcastProperty(player, "general") + end + + if player.kingdom == "unknown" then + player.kingdom = new.kingdom + self:broadcastProperty(player, "kingdom") + end + + player.maxHp = player:getGeneralMaxHp() + self:broadcastProperty(player, "hp") + if full then + player.hp = player.maxHp + self:broadcastProperty(player, "maxHp") + end +end + ------------------------------------------------------------------------ -- 网络通信有关 ------------------------------------------------------------------------ @@ -601,7 +642,7 @@ function Room:requestLoop(rest_time) end player:doNotify("UpdateDrawPile", #self.draw_pile) - player:doNotify("UpdateRoundNum", self:getTag("RoundCount")) + player:doNotify("UpdateRoundNum", self:getTag("RoundCount") or 0) table.insert(self.observers, {observee.id, player}) end @@ -639,14 +680,17 @@ function Room:requestLoop(rest_time) local request = self.room:fetchRequest() if request ~= "" then ret = true - local id, command = table.unpack(request:split(",")) - id = tonumber(id) + local reqlist = request:split(",") + local id = tonumber(reqlist[1]) + local command = reqlist[2] if command == "reconnect" then self:getPlayerById(id):reconnect() elseif command == "observe" then addObserver(id) elseif command == "leave" then removeObserver(id) + elseif command == "prelight" then + self:getPlayerById(id):prelightSkill(reqlist[3], reqlist[4] == "true") end elseif rest_time > 10 then -- let current thread sleep 10ms @@ -2349,6 +2393,7 @@ end ---@param skill Skill @ 发动的技能 ---@param effect_cb fun() @ 实际要调用的函数 function Room:useSkill(player, skill, effect_cb) + player:revealBySkillName(skill.name) if not skill.mute then if skill.attached_equip then local equip = Fk:cloneCard(skill.attached_equip) diff --git a/lua/server/serverplayer.lua b/lua/server/serverplayer.lua index 2488df87..eed1c807 100644 --- a/lua/server/serverplayer.lua +++ b/lua/server/serverplayer.lua @@ -261,7 +261,7 @@ function ServerPlayer:reconnect() end self:doNotify("UpdateDrawPile", #room.draw_pile) - self:doNotify("UpdateRoundNum", room:getTag("RoundCount")) + self:doNotify("UpdateRoundNum", room:getTag("RoundCount") or 0) room:broadcastProperty(self, "state") end @@ -568,4 +568,82 @@ function ServerPlayer:pindian(tos, skillName, initialCard) return pindianData end +-- Hegemony func + +function ServerPlayer:prelightSkill(skill, isPrelight) + if isPrelight then + self:addSkill(skill) + else + self:loseSkill(skill) + end + self:doNotify("PrelightSkill", json.encode{ skill, isPrelight }) +end + +function ServerPlayer:revealGeneral(isDeputy) + local room = self.room + local generalName + if isDeputy then + if self.deputyGeneral ~= "anjiang" then return end + generalName = self:getMark("__heg_deputy") + else + if self.general ~= "anjiang" then return end + generalName = self:getMark("__heg_general") + end + + local general = Fk.generals[generalName] + for _, s in ipairs(general:getSkillNameList()) do + local skill = Fk.skills[s] + if skill:isInstanceOf(TriggerSkill) or table.find(skill.related_skills, + function(s) return s:isInstanceOf(TriggerSkill) end) then + self:doNotify("LoseSkill", json.encode{ self.id, s, true }) + end + end + + local oldKingdom = self.kingdom + room:changeHero(self, generalName, false, isDeputy) + local kingdom = general.kingdom + if oldKingdom == "unknown" and #table.filter(room:getOtherPlayers(self), + function(p) + return p.kingdom == kingdom + end) >= #room.alive_players // 2 then + + self.kingdom = "wild" + room:broadcastProperty(self, "kingdom") + end + + if self.gender ~= General.Male and self.gender ~= General.Female then + self.gender = general.gender + end + + room:sendLog{ + type = "#RevealGeneral", + from = self.id, + arg = isDeputy and "deputyGeneral" or "mainGeneral", + arg2 = generalName, + } + + -- TODO: 阴阳鱼摸牌 +end + +function ServerPlayer:revealBySkillName(skill_name) + local main = self.general == "anjiang" + local deputy = self.deputyGeneral == "anjiang" + + if main then + if table.contains(Fk.generals[self:getMark("__heg_general")] + :getSkillNameList(), skill_name) then + self:revealGeneral(false) + return + end + end + + if deputy then + if table.contains(Fk.generals[self:getMark("__heg_deputy")] + :getSkillNameList(), skill_name) then + self:revealGeneral(true) + return + end + end +end + return ServerPlayer diff --git a/packages/standard/hegemony.lua b/packages/standard/hegemony.lua new file mode 100644 index 00000000..7613c042 --- /dev/null +++ b/packages/standard/hegemony.lua @@ -0,0 +1,377 @@ +local heg_description = [==[ +# 国战简介 + +《三国杀·国战》是一款可以支持2\~12人(线上版4\~8人)同时参与的桌面卡牌游戏。在游戏中,每名玩家将甄选三国时代中包括魏国、蜀国、吴国、群雄在内的各位名将,组成自己的战斗团队,利用他们珠联璧合的组合技能发起进攻,消灭其他各方势力,赢得最终的胜利。 + +## 准备游戏 + +**挑选武将:** + +发给每位玩家四张武将牌(会员5张),选出两张势力相同的武将牌并背面朝上放置,称为“暗置”(参考段落“明置和暗置”)。 + +靠近体力牌的武将视为副将,另一个视为主将。 + +游戏中,每名玩家扮演的角色由两张武将牌组成。 + +**分发体力牌:** + +每位玩家拿取一张体力牌,翻到对应体力上限的一面,放置在武将牌旁边。体力上限为两张武将牌上的完整阴阳鱼的数量之和。两个单独的阴阳鱼可以组成一个完整的阴阳鱼。 + +注:当一名角色的两张武将牌第一次均明置时,若其武将牌上有单独的阴阳鱼没有组成1 点体力,则他可以立即摸一张牌。 + +扣减体力时,用主将挡住扣减的体力,露出当前体力值。 + +## 进行游戏 + +随机选择一名玩家作为起始玩家。由该玩家开始,按逆时针方向以回合的方式进行。即每名玩家有一个自己的回合,一名玩家的回合结束后, 右边玩家的回合开始,依次轮流进行。 + +每个玩家的回合可以分为六个阶段: + +回合开始阶段 -> 判定阶段 -> 摸牌阶段 -> 出牌阶段 -> 弃牌阶段 -> 回合结束阶段 + +**回合开始阶段:** + +有些技能可以在此阶段发动。你的暗置的武将牌也可于此阶段明置。 + +**判定阶段:** + +若你的面前横置着延时类锦囊,你必须依次对这些延时类锦囊进行判定。若有多张延时类锦囊,先判定最后放置的那张,然后以此类推。 + +**摸牌阶段:** + +你从牌堆顶摸两张牌。 + +**出牌阶段:** + +你可以使用任意张牌,但必须遵守以下两条规则: + +1. 每个出牌阶段仅限使用一次【杀】。 +2. 任何一名角色面前的判定区里不能放有两张同名的牌。 + +每使用一张牌,即执行该牌之效果,详见“游戏牌详解”。如无特殊说明,游戏牌在使用后均需置入弃牌堆。 + +**弃牌阶段:** + +在出牌阶段中,不想出或没法出牌时,就进入弃牌阶段。此时检查你的手牌数是否超出你当前的体力值( 你的手牌上限等于你当前的体力值), 每超出一张,须弃置一张手牌。 + +**回合结束阶段:** + +有些技能可以在此阶段发动。 + + +## 角色死亡 + +当一名角色的体力降到0 时,即进入濒死状态,除非该角色在此时使用【酒】,或有角色使用【桃】来挽救该角色,否则该角色死亡。 + +死亡的角色明置其武将牌,弃置该角色所有牌及其判定区里的牌,然后执行奖惩。 + +奖惩方式: + +1. 已经确定势力的角色杀死相同势力的角色须弃置所有手牌和装备区的牌; +2. 已经确定势力的角色杀死不同势力的角色,从牌堆摸取等同于该势力人数(包括刚刚杀死的角色)张牌。 + +例:“蜀”势力角色杀死了一名“魏”势力角色,此时还有其他两名“魏”势力角色存活,则该“蜀”势力角色摸三张牌。 + +注:若被杀死的角色还没有明置武将牌(即没确定势力),则须明置验明势力。 没有势力的角色(即武将牌没有明置的角色)杀死其他角色没有奖惩。 + +## 胜负结算 + +玩家的游戏目标与势力有关:消灭所有与自己不同势力的角色。 + +特殊的:野心家需要消灭所有其他角色。 + +当全场所有角色均确定势力后, 才可以进行胜利条件的判断: +当全场只剩下一种势力存活时, 该势力的角色获胜( 没有确定势力的角色无法取得游戏胜利, 即使与存活的其他角色为同一势力)。 + +## 暗将规则 + +处于暗置状态的武将牌没有任何武将技能、性别以及势力。当暗置的武将牌发动技能时,将武将牌明置,然后发动相应的技能。 + +暗置的武将牌只有两个时机可以将武将牌明置:1. 回合开始阶段开始时;2. 发动武将牌的技能时。 + +例:郭嘉、司马懿等,受到伤害后发动技能时明置武将牌; +马超、黄忠等,使用【杀】指定一名角色为目标后,发动技能并明置武将牌; +孙权、甘宁等,在出牌阶段发动技能时明置武将牌; + +没有明置武将牌的角色没有性别,任何与性别有关的技能和武器效果均不能对其发动。 + +有一张武将牌明置时,角色性别与明置的武将牌相同。当一名角色的两张武将牌均亮明后,性别与主将的武将牌相同。 + +没有明置武将牌的角色没有势力,明置一张武将牌后确定势力:与武将牌左上角所示的势力相同,或成为野心家。野心家用“野心家牌”表示。 + +野心家规则: + +当一名角色明置武将牌确定势力时,若该势力的角色超过了游戏总玩家数的一半,则他视为野心家,拿取一张野心家牌表示。若之后仍然有该势力的角色明置武将牌,均视为野心家。野心家为单独的一种势力,与其他角色的势力均不同。他(们)需要杀死所有其他角色,成为唯一的存活者。 + +注意:野心家与野心家之间也是不同势力。 + +例: + +★ 6 人、7 人游戏时,当出现第四名同势力角色时,该角色及之后明置的该势力角色均成为野心家。 + +★ 8 人、9 人游戏时,当出现第五名同势力角色时,该角色及之后明置的该势力角色均成为野心家。 + +## 珠联璧合 + +珠联璧合表示了部分武将之间的特殊联系。 + +武将牌中下方的其他武将姓名表示了可以和此武将牌形成珠联璧合的其他武将。 + +若你选择的两张武将牌形成珠联璧合,则在第一次两张武将牌均明置时,可以立即选择摸两张牌或回复1 点体力。 + +若触发珠联璧合时正在进行其他事件的结算,则先进行“珠联璧合”的选择,再继续结算该事件。 +]==] + +local heg + +---@class HegLogic: GameLogic +local HegLogic = {} + +function HegLogic:assignRoles() + local room = self.room + for _, p in ipairs(room.players) do + p.role_shown = true + p.role = "hidden" + room:broadcastProperty(p, "role") + end + + -- for adjustSeats + room.players[1].role = "lord" +end + +function HegLogic:chooseGenerals() + local room = self.room + local generalNum = math.max(room.settings.generalNum, 6) + + local lord = room:getLord() + room.current = lord + lord.role = "hidden" + + local nonlord = room.players + local generals = Fk:getGeneralsRandomly(#nonlord * generalNum) + -- table.shuffle(generals) + for _, p in ipairs(nonlord) do + local arg = { map = table.map } + for i = 1, generalNum do + table.insert(arg, table.remove(generals, 1)) + end + table.sort(arg, function(a, b) return a.kingdom > b.kingdom end) + + for idx, _ in ipairs(arg) do + if arg[idx].kingdom == arg[idx + 1].kingdom then + p.default_reply = { arg[idx].name, arg[idx + 1].name } + break + end + end + + arg = arg:map(function(g) return g.name end) + p.request_data = json.encode({ arg, 2, true }) + end + + room:notifyMoveFocus(nonlord, "AskForGeneral") + room:doBroadcastRequest("AskForGeneral", nonlord) + for _, p in ipairs(nonlord) do + local general, deputy + if p.general == "" and p.reply_ready then + local generals = json.decode(p.client_reply) + general = generals[1] + deputy = generals[2] + room:setPlayerGeneral(p, general, true) + room:setDeputyGeneral(p, deputy) + else + general = p.default_reply[1] + deputy = p.default_reply[2] + end + + p:setMark("__heg_general", general) + p:setMark("__heg_deputy", deputy) + p:doNotify("SetPlayerMark", json.encode{ p.id, "__heg_general", general}) + p:doNotify("SetPlayerMark", json.encode{ p.id, "__heg_deputy", deputy}) + + room:setPlayerGeneral(p, "anjiang", true) + room:setDeputyGeneral(p, "anjiang") + + p.default_reply = "" + end +end + +function HegLogic:broadcastGeneral() + local room = self.room + local players = room.players + + for _, p in ipairs(players) do + assert(p.general ~= "") + local general = Fk.generals[p:getMark("__heg_general")] + local deputy = Fk.generals[p:getMark("__heg_deputy")] + p.maxHp = math.floor((deputy.maxHp + general.maxHp) / 2) + p.hp = math.floor((deputy.hp + general.hp) / 2) + -- p.shield = math.min(general.shield + deputy.shield, 5) + p.shield = 0 + -- TODO: setup AI here + + room:broadcastProperty(p, "general") + room:broadcastProperty(p, "deputyGeneral") + room:broadcastProperty(p, "maxHp") + room:broadcastProperty(p, "hp") + room:broadcastProperty(p, "shield") + end +end + +function HegLogic:attachSkillToPlayers() + local room = self.room + local players = room.players + + room:handleAddLoseSkills(players[1], "#_heg_invalid", nil, false, true) + + local addHegSkills = function(player, skillName) + local skill = Fk.skills[skillName] + if skill.lordSkill and (player.role ~= "lord" or #room.players < 5) then + return + end + + -- room:handleAddLoseSkills(player, skillName, nil, false) + player:doNotify("AddSkill", json.encode{ player.id, skillName }) + + if skill:isInstanceOf(TriggerSkill) or table.find(skill.related_skills, + function(s) return s:isInstanceOf(TriggerSkill) end) then + player:doNotify("AddSkill", json.encode{ player.id, skillName, true }) + end + end + + for _, p in ipairs(room.alive_players) do + local general = Fk.generals[p:getMark("__heg_general")] + local skills = general.skills + for _, s in ipairs(skills) do + addHegSkills(p, s.name) + end + for _, sname in ipairs(general.other_skills) do + addHegSkills(p, sname) + end + + local deputy = Fk.generals[p:getMark("__heg_deputy")] + if deputy then + skills = deputy.skills + for _, s in ipairs(skills) do + addHegSkills(p, s.name) + end + for _, sname in ipairs(deputy.other_skills) do + addHegSkills(p, sname) + end + end + end + + room:setTag("SkipNormalDeathProcess", true) +end + +local heg_getlogic = function() + local h = GameLogic:subclass("HegLogic") + for k, v in pairs(HegLogic) do + h[k] = v + end + return h +end + +local heg_invalid = fk.CreateInvaliditySkill{ + name = "#_heg_invalid", + invalidity_func = function(self, player, skill) + end, +} + +local function getWinnerHeg(victim) + local room = victim.room + local alive = room.alive_players + if #alive == 1 then + local p = alive[1] + p:revealGeneral(false) + p:revealGeneral(true) + return p.kingdom + end + + local winner = alive[1].kingdom + if winner == "unknown" then return "" end + for _, p in ipairs(alive) do + if p.kingdom ~= winner or p.kingdom == "wild" then + return "" + end + end + + return winner +end + +local heg_rule = fk.CreateTriggerSkill{ + name = "#heg_rule", + priority = 0.001, + events = {fk.TurnStart, fk.GameOverJudge, fk.BuryVictim}, + can_trigger = function(self, event, target, player, data) + return target == player + end, + on_trigger = function(self, event, target, player, data) + local room = player.room + if event == fk.TurnStart then + local choices = {} + if player.general == "anjiang" then + table.insert(choices, "revealMain") + end + if player.deputyGeneral == "anjiang" then + table.insert(choices, "revealDeputy") + end + if #choices == 0 then return end + if #choices == 2 then table.insert(choices, "revealAll") end + table.insert(choices, "Cancel") + + local choice = room:askForChoice(player, choices, self.name) + if choice == "revealMain" then player:revealGeneral(false) + elseif choice == "revealDeputy" then player:revealGeneral(true) + elseif choice == "revealAll" then + player:revealGeneral(false) + player:revealGeneral(true) + end + elseif event == fk.GameOverJudge then + player:revealGeneral(false) + player:revealGeneral(true) + local winner = getWinnerHeg(player) + if winner ~= "" then + room:gameOver("hidden") + return true + end + room:setTag("SkipGameRule", true) + elseif event == fk.BuryVictim then + local damage = data.damage + if damage and damage.from then + local killer = damage.from + if killer.kingdom == "unknown" then return end + + local victim = damage.to + if killer.kingdom == victim.kingdom then + killer:throwAllCards("he") + else + killer:drawCards(victim.kingdom == "wild" and 1 or + #table.filter(room.alive_players, function(p) + return p.kingdom == victim.kingdom + end) + 1) + end + end + end + end, +} +Fk:addSkill(heg_rule) + +heg = fk.CreateGameMode{ + name = "heg_mode", + minPlayer = 2, + maxPlayer = 8, + rule = heg_rule, + logic = heg_getlogic, +} + +Fk:loadTranslationTable{ + ["heg_mode"] = "国战测试版", + [":heg_mode"] = heg_description, + ["wild"] = "野心家", + ["#heg_rule"] = "国战规则", + ["revealMain"] = "明置主将", + ["revealDeputy"] = "明置副将", + ["revealAll"] = "背水:全部明置", +} + +return heg diff --git a/packages/standard/image/generals/anjiang.jpg b/packages/standard/image/generals/anjiang.jpg new file mode 100644 index 00000000..cb08aaf3 Binary files /dev/null and b/packages/standard/image/generals/anjiang.jpg differ diff --git a/packages/standard/init.lua b/packages/standard/init.lua index af56c406..80488bd7 100644 --- a/packages/standard/init.lua +++ b/packages/standard/init.lua @@ -1108,6 +1108,17 @@ local role_mode = fk.CreateGameMode{ } extension:addGameMode(role_mode) +local anjiang = General(extension, "anjiang", "unknown", 5) +anjiang.gender = General.Agender +anjiang.total_hidden = true + +Fk:loadTranslationTable{ + ["anjiang"] = "暗将", +} + +local heg_mode = require "packages.standard.hegemony" +extension:addGameMode(heg_mode) + -- load translations of this package dofile "packages/standard/i18n/init.lua" diff --git a/qml/Pages/Room.qml b/qml/Pages/Room.qml index 3b26411c..75b53431 100644 --- a/qml/Pages/Room.qml +++ b/qml/Pages/Room.qml @@ -816,7 +816,7 @@ Item { deputyGeneral: "", screenName: Self.screenName, role: "unknown", - kingdom: "qun", + kingdom: "unknown", netstate: "online", maxHp: 0, hp: 0, @@ -841,7 +841,7 @@ Item { deputyGeneral: "", screenName: "", role: "unknown", - kingdom: "qun", + kingdom: "unknown", netstate: "online", maxHp: 0, hp: 0, diff --git a/qml/Pages/RoomElement/ChooseGeneralBox.qml b/qml/Pages/RoomElement/ChooseGeneralBox.qml index 34214ceb..090b4af9 100644 --- a/qml/Pages/RoomElement/ChooseGeneralBox.qml +++ b/qml/Pages/RoomElement/ChooseGeneralBox.qml @@ -11,6 +11,7 @@ GraphicsBox { property var choices: [] property var selectedItem: [] property bool loaded: false + property bool needSameKingdom: false ListModel { id: generalList @@ -115,10 +116,12 @@ GraphicsBox { GeneralCardItem { name: model.name - selectable: true + //enabled: //!(choices[0] && choices[0].kingdom !== this.kingdom) + selectable: !(selectedItem[0] && selectedItem[0].kingdom !== kingdom) draggable: true onClicked: { + if (!selectable) return; let toSelect = true; for (let i = 0; i < selectedItem.length; i++) { if (selectedItem[i] === this) { @@ -149,7 +152,7 @@ GraphicsBox { selectedItem = []; for (i = 0; i < generalList.count; i++) { item = generalCardList.itemAt(i); - if (item.y > splitLine.y) + if (item.y > splitLine.y && item.selectable) selectedItem.push(item); } @@ -181,6 +184,7 @@ GraphicsBox { for (i = 0; i < generalCardList.count; i++) { item = generalCardList.itemAt(i); + item.selectable = needSameKingdom ? !(selectedItem[0] && (selectedItem[0].kingdom !== item.kingdom)) : true; if (selectedItem.indexOf(item) != -1) continue; diff --git a/qml/Pages/RoomElement/Dashboard.qml b/qml/Pages/RoomElement/Dashboard.qml index f2a9f9eb..1d3347f3 100644 --- a/qml/Pages/RoomElement/Dashboard.qml +++ b/qml/Pages/RoomElement/Dashboard.qml @@ -328,12 +328,23 @@ RowLayout { cardSelected(-1); } - function addSkill(skill_name) { - skillPanel.addSkill(skill_name); + function addSkill(skill_name, prelight) { + skillPanel.addSkill(skill_name, prelight); } - function loseSkill(skill_name) { - skillPanel.loseSkill(skill_name); + function loseSkill(skill_name, prelight) { + skillPanel.loseSkill(skill_name, prelight); + } + + function prelightSkill(skill_name, prelight) { + let btns = skillPanel.prelight_buttons; + for (let i = 0; i < btns.count; i++) { + let btn = btns.itemAt(i); + if (btn.orig === skill_name) { + btn.prelighted = prelight; + btn.enabled = true; + } + } } function enableSkills(cname) { diff --git a/qml/Pages/RoomElement/Photo.qml b/qml/Pages/RoomElement/Photo.qml index 4ef3158a..bb9fb5d4 100644 --- a/qml/Pages/RoomElement/Photo.qml +++ b/qml/Pages/RoomElement/Photo.qml @@ -394,10 +394,10 @@ Item { LimitSkillArea { id: limitSkills - anchors.top: role.bottom - anchors.left: role.left - anchors.topMargin: 2 - anchors.leftMargin: -2 + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: role.height + 2 + anchors.rightMargin: 30 } GlowText { diff --git a/qml/Pages/RoomElement/SkillArea.qml b/qml/Pages/RoomElement/SkillArea.qml index 9251c119..4be7412b 100644 --- a/qml/Pages/RoomElement/SkillArea.qml +++ b/qml/Pages/RoomElement/SkillArea.qml @@ -6,15 +6,20 @@ import QtQuick.Layouts Flickable { id: root property alias skill_buttons: skill_buttons + property alias prelight_buttons: prelight_buttons clip: true contentWidth: panel.width contentHeight: panel.height contentX: contentWidth - width - width: Math.min(150, panel.width) - height: Math.min(180, panel.height) + width: Math.min(180, panel.width) + height: Math.min(200, panel.height) flickableDirection: Flickable.AutoFlickIfNeeded + ListModel { + id: prelight_skills + } + ListModel { id: active_skills } @@ -25,10 +30,37 @@ Flickable { Item { id: panel - width: Math.max(grid1.width, grid2.width) - height: grid1.height + grid2.height + width: Math.max(grid0.width, grid1.width, grid2.width) + height: grid0.height + grid1.height + grid2.height + Grid { + id: grid0 + columns: 2 + columnSpacing: 2 + rowSpacing: 2 + Repeater { + id: prelight_buttons + model: prelight_skills + onItemAdded: parent.forceLayout() + SkillButton { + skill: model.skill + type: "prelight" + enabled: !config.observing + orig: model.orig_skill + + onPressedChanged: { + if (!pressed) return; + enabled = false; + ClientInstance.notifyServer("PrelightSkill", [ + "prelight", orig, (!prelighted).toString() + ].join(",")); + } + } + } + } + Grid { id: grid1 + anchors.top: grid0.bottom columns: 2 columnSpacing: 2 rowSpacing: 2 @@ -69,19 +101,46 @@ Flickable { } } - function addSkill(skill_name) { + function addSkill(skill_name, prelight) { + const modelContains = (m, e) => { + for (let i = 0; i < m.count; i++) { + if (m.get(i).orig_skill === e.orig_skill) { + return true; + } + } + return false; + }; + let data = JSON.parse(Backend.callLuaFunction( "GetSkillData", [skill_name] )); + + if (prelight) { + if (!modelContains(prelight_skills, data)) + prelight_skills.append(data); + return; + } + if (data.freq === "active") { - active_skills.append(data); + if (!modelContains(active_skills, data)) active_skills.append(data); } else { - not_active_skills.append(data); + if (!modelContains(not_active_skills, data)) + not_active_skills.append(data); } } - function loseSkill(skill_name) { + function loseSkill(skill_name, prelight) { + if (prelight) { + for (let i = 0; i < prelight_skills.count; i++) { + let item = prelight_skills.get(i); + if (item.orig_skill == skill_name) { + prelight_skills.remove(i); + } + } + return; + } + for (let i = 0; i < active_skills.count; i++) { let item = active_skills.get(i); if (item.orig_skill == skill_name) { diff --git a/qml/Pages/RoomElement/SkillButton.qml b/qml/Pages/RoomElement/SkillButton.qml index 5a1b48f9..7087e28d 100644 --- a/qml/Pages/RoomElement/SkillButton.qml +++ b/qml/Pages/RoomElement/SkillButton.qml @@ -9,24 +9,34 @@ Item { property string type: "active" property string orig: "" property bool pressed: false + property bool prelighted: false onEnabledChanged: { if (!enabled) pressed = false; } - width: type === "active" ? Math.max(80, skill.width + 8) : skill.width - height: type === "active" ? 36 : 24 + width: type !== "notactive" ? Math.max(80, skill.width + 8) : skill.width + height: type !== "notactive" ? 36 : 24 Image { x: -13 - 120 * 0.166 y: -6 - 55 * 0.166 scale: 0.66 - source: type !== "active" ? "" - : AppPath + "/image/button/skill/active/" + source: type === "notactive" ? "" + : AppPath + "/image/button/skill/" + type + "/" + (enabled ? (pressed ? "pressed" : "normal") : "disabled") } + Image { + visible: type === "prelight" + source: AppPath + "/image/button/skill/" + + (prelighted ? "prelight.png" : "unprelight.png") + transformOrigin: Item.TopLeft + x: -10 + scale: 0.7 + } + Text { anchors.centerIn: parent id: skill @@ -54,7 +64,7 @@ Item { } TapHandler { - enabled: root.type === "active" && root.enabled + enabled: root.type !== "notactive" && root.enabled onTapped: parent.pressed = !parent.pressed; } } diff --git a/qml/Pages/RoomLogic.js b/qml/Pages/RoomLogic.js index 2bbe9774..ed0bb211 100644 --- a/qml/Pages/RoomLogic.js +++ b/qml/Pages/RoomLogic.js @@ -573,15 +573,16 @@ callbacks["AskForGeneral"] = function(jsonData) { let data = JSON.parse(jsonData); let generals = data[0]; let n = data[1]; + let heg = data[2]; roomScene.promptText = Backend.translate("#AskForGeneral"); 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)); }); box.choiceNum = n; + box.needSameKingdom = !!heg; for (let i = 0; i < generals.length; i++) box.generalList.append({ "name": generals[i] }); box.updatePosition(); @@ -751,8 +752,9 @@ callbacks["LoseSkill"] = function(jsonData) { let data = JSON.parse(jsonData); let id = data[0]; let skill_name = data[1]; + let prelight = data[2]; if (id === Self.id) { - dashboard.loseSkill(skill_name); + dashboard.loseSkill(skill_name, prelight); } } @@ -761,11 +763,20 @@ callbacks["AddSkill"] = function(jsonData) { let data = JSON.parse(jsonData); let id = data[0]; let skill_name = data[1]; + let prelight = data[2]; if (id === Self.id) { - dashboard.addSkill(skill_name); + dashboard.addSkill(skill_name, prelight); } } +callbacks["PrelightSkill"] = function(jsonData) { + let data = JSON.parse(jsonData); + let skill_name = data[0]; + let prelight = data[1]; + + dashboard.prelightSkill(skill_name, prelight); +} + // prompt: 'string::::' function processPrompt(prompt) { let data = prompt.split(":"); diff --git a/qml/Pages/skin-bank.js b/qml/Pages/skin-bank.js index 2ec1181e..039bed1c 100644 --- a/qml/Pages/skin-bank.js +++ b/qml/Pages/skin-bank.js @@ -71,7 +71,7 @@ function getPhotoBack(kingdom) { } else { return path; } - return PHOTO_BACK_DIR + "qun"; + return PHOTO_BACK_DIR + "unknown"; } function getGeneralCardDir(kingdom) { diff --git a/src/network/router.cpp b/src/network/router.cpp index 4909edd4..23bf88a5 100644 --- a/src/network/router.cpp +++ b/src/network/router.cpp @@ -250,6 +250,8 @@ void Router::handlePacket(const QByteArray &rawPacket) { room->addRobot(player); } else if (command == "Chat") { room->chat(player, jsonData); + } else if (command == "PrelightSkill") { + room->pushRequest(QString("%1,").arg(player->getId()) + jsonData); } } } diff --git a/src/server/room.cpp b/src/server/room.cpp index c32c8624..2ab6a81d 100644 --- a/src/server/room.cpp +++ b/src/server/room.cpp @@ -286,6 +286,12 @@ void Room::chat(ServerPlayer *sender, const QString &jsonData) { auto doc = String2Json(jsonData).object(); auto type = doc["type"].toInt(); doc["sender"] = sender->getId(); + + // 屏蔽.号,防止有人在HTML文本发链接,而正常发链接看不出来有啥改动 + auto msg = doc["msg"].toString(); + msg.replace(".", "․"); + doc["msg"] = msg; + if (type == 1) { doc["userName"] = sender->getScreenName(); auto json = QJsonDocument(doc).toJson(QJsonDocument::Compact); @@ -296,6 +302,7 @@ void Room::chat(ServerPlayer *sender, const QString &jsonData) { doBroadcastNotify(observers, "Chat", json); } + qInfo("[Chat] %s: %s", sender->getScreenName().toUtf8().constData(), doc["msg"].toString().toUtf8().constData()); }