diff --git a/lua/client/client.lua b/lua/client/client.lua index b2668ba1..0a85f8e8 100644 --- a/lua/client/client.lua +++ b/lua/client/client.lua @@ -2,12 +2,12 @@ ---@class Client ---@field public client fk.Client ----@field public players ClientPlayer[] ----@field public alive_players ClientPlayer[] ----@field public observers ClientPlayer[] ----@field public current ClientPlayer ----@field public discard_pile integer[] ----@field public status_skills Skill[] +---@field public players ClientPlayer[] @ 所有参战玩家的数组 +---@field public alive_players ClientPlayer[] @ 所有存活玩家的数组 +---@field public observers ClientPlayer[] @ 观察者的数组 +---@field public current ClientPlayer @ 当前回合玩家 +---@field public discard_pile integer[] @ 弃牌堆 +---@field public status_skills Skill[] @ 状态技总和 ---@field public observing boolean Client = class('Client') diff --git a/lua/core/engine.lua b/lua/core/engine.lua index ebbb63e6..90a9ec57 100644 --- a/lua/core/engine.lua +++ b/lua/core/engine.lua @@ -252,9 +252,11 @@ function Engine:addGenerals(generals) end end -local function canUseGeneral(g) - local r = Fk:currentRoom() - local general = Fk.generals[g] +--- 判断一个武将是否在本房间可用。 +---@param g string @ 武将名 +function Engine:canUseGeneral(g) + local r = self:currentRoom() + local general = self.generals[g] if not general then return false end return not table.contains(r.disabled_packs, general.package.name) and not table.contains(r.disabled_generals, g) and not general.hidden and not general.total_hidden @@ -270,7 +272,7 @@ function Engine:getSameGenerals(name) local tName = tmp[#tmp] local ret = self.same_generals[tName] or {} return table.filter(ret, function(g) - return g ~= name and self.generals[g] ~= nil and canUseGeneral(g) + return g ~= name and self.generals[g] ~= nil and self:canUseGeneral(g) end) end @@ -393,7 +395,7 @@ function Engine:getAllGenerals(except) local result = {} for _, general in pairs(self.generals) do if not (except and table.contains(except, general)) then - if canUseGeneral(general.name) then + if self:canUseGeneral(general.name) then table.insert(result, general) end end diff --git a/lua/core/game_mode.lua b/lua/core/game_mode.lua index 658821f7..1c05c510 100644 --- a/lua/core/game_mode.lua +++ b/lua/core/game_mode.lua @@ -8,8 +8,8 @@ ---@field public name string @ 游戏模式名 ---@field public minPlayer integer @ 最小玩家数 ---@field public maxPlayer integer @ 最大玩家数 ----@field public rule TriggerSkill @ 规则(通过技能完成,通常用来为特定角色及特定时机提供触发事件) ----@field public logic fun() @ 逻辑(通过function完成,通常用来初始化、分配身份及座次) +---@field public rule nil|TriggerSkill @ 规则(通过技能完成,通常用来为特定角色及特定时机提供触发事件) +---@field public logic nil|fun() @ 逻辑(通过function完成,通常用来初始化、分配身份及座次) ---@field public whitelist string[]|nil @ 白名单 ---@field public blacklist string[]|nil @ 黑名单 local GameMode = class("GameMode") diff --git a/lua/core/player.lua b/lua/core/player.lua index 5d978940..695d3124 100644 --- a/lua/core/player.lua +++ b/lua/core/player.lua @@ -508,7 +508,7 @@ function Player:distanceTo(other, mode, ignore_dead) temp = temp.next end if temp ~= other then - print("Distance malfunction: start and end does not matched.") + print("Distance malfunction: start and end does not match.") end local left = #(ignore_dead and Fk:currentRoom().players or Fk:currentRoom().alive_players) - right - #table.filter(Fk:currentRoom().alive_players, function(p) return p:isRemoved() end) local ret = 0 diff --git a/lua/server/event.lua b/lua/server/event.lua index 09f91e0c..737b7dad 100644 --- a/lua/server/event.lua +++ b/lua/server/event.lua @@ -129,3 +129,7 @@ fk.GeneralRevealed = 89 fk.GeneralHidden = 90 fk.NumOfEvents = 91 + +fk.BeforePropertyChange = 92 +fk.PropertyChange = 93 +fk.AfterPropertyChange = 94 diff --git a/lua/server/events/gameflow.lua b/lua/server/events/gameflow.lua index a8bfc061..05e6f00e 100644 --- a/lua/server/events/gameflow.lua +++ b/lua/server/events/gameflow.lua @@ -294,7 +294,7 @@ GameEvent.functions[GameEvent.Phase] = function(self) n = 2 } room.logic:trigger(fk.DrawNCards, player, data) - room:drawCards(player, data.n, self.name) + room:drawCards(player, data.n, "game_rule") room.logic:trigger(fk.AfterDrawNCards, player, data) end, [Player.Play] = function() diff --git a/lua/server/events/misc.lua b/lua/server/events/misc.lua index 8896f509..5be32ae8 100644 --- a/lua/server/events/misc.lua +++ b/lua/server/events/misc.lua @@ -9,6 +9,9 @@ GameEvent.functions[GameEvent.ChangeProperty] = function(self) data.sendLog = data.sendLog or false local skills = {} + if logic:trigger(fk.PropertyChange, player, data) then + logic:breakEvent() + end if data.general and data.general ~= "" and data.general ~= player.general then local originalGeneral = Fk.generals[player.general] or Fk.generals["blank_shibing"] diff --git a/lua/server/gamelogic.lua b/lua/server/gamelogic.lua index b0d28f53..9fce8f6a 100644 --- a/lua/server/gamelogic.lua +++ b/lua/server/gamelogic.lua @@ -90,32 +90,8 @@ function GameLogic:chooseGenerals() if lord ~= nil then room.current = lord - local generals = {} - local lordlist = {} - local lordpools = {} - if room.settings.gameMode == "aaa_role_mode" then - for _, general in pairs(Fk:getAllGenerals()) do - if (not general.hidden and not general.total_hidden) and - table.find(general.skills, function(s) - return s.lordSkill - end) and - not table.find(lordlist, function(g) - return g.trueName == general.trueName - end) then - table.insert(lordlist, general) - end - end - lordlist = table.random(lordlist, 3) or {} - end - table.insertTable(generals, Fk:getGeneralsRandomly(generalNum, Fk:getAllGenerals(), nil, function(g) - return table.contains(table.map(lordlist, function(g) return g.trueName end), g.trueName) - end)) - for i = 1, #generals do - generals[i] = generals[i].name - end - lordpools = table.simpleClone(generals) - table.insertTable(lordpools, table.map(lordlist, function(g) return g.name end)) - lord_generals = room:askForGeneral(lord, lordpools, n) + local generals = room:getNGenerals(generalNum) + lord_generals = room:askForGeneral(lord, generals, n) local lord_general, deputy if type(lord_generals) == "table" then deputy = lord_generals[2] @@ -125,6 +101,9 @@ function GameLogic:chooseGenerals() lord_generals = {lord_general} end + generals = table.filter(generals, function(g) return not table.contains(lord_generals, g) end) + room:returnToGeneralPile(generals) + room:setPlayerGeneral(lord, lord_general, true) room:askForChooseKingdom({lord}) room:broadcastProperty(lord, "general") @@ -134,13 +113,10 @@ function GameLogic:chooseGenerals() end local nonlord = room:getOtherPlayers(lord, true) - local generals = Fk:getGeneralsRandomly(#nonlord * generalNum, nil, lord_generals) + local generals = room:getNGenerals(#nonlord * generalNum) table.shuffle(generals) - for _, p in ipairs(nonlord) do - local arg = {} - for i = 1, generalNum do - table.insert(arg, table.remove(generals, 1).name) - end + for i, p in ipairs(nonlord) do + local arg = table.slice(generals, (i - 1) * generalNum + 1, i * generalNum + 1) p.request_data = json.encode{ arg, n } p.default_reply = table.random(arg, n) end @@ -148,11 +124,13 @@ function GameLogic:chooseGenerals() room:notifyMoveFocus(nonlord, "AskForGeneral") room:doBroadcastRequest("AskForGeneral", nonlord) + local selected = {} for _, p in ipairs(nonlord) do if p.general == "" and p.reply_ready then - local generals = json.decode(p.client_reply) - local general = generals[1] - local deputy = generals[2] + local general_ret = json.decode(p.client_reply) + local general = general_ret[1] + local deputy = general_ret[2] + table.insertTableIfNeed(selected, general_ret) room:setPlayerGeneral(p, general, true, true) room:setDeputyGeneral(p, deputy) else @@ -162,6 +140,9 @@ function GameLogic:chooseGenerals() p.default_reply = "" end + generals = table.filter(generals, function(g) return not table.contains(selected, g) end) + room:returnToGeneralPile(generals) + room:askForChooseKingdom(nonlord) end diff --git a/lua/server/room.lua b/lua/server/room.lua index 721cc1ef..c092c8f9 100644 --- a/lua/server/room.lua +++ b/lua/server/room.lua @@ -15,6 +15,7 @@ ---@field public game_finished boolean @ 游戏是否已经结束 ---@field public timeout integer @ 出牌时长上限 ---@field public tag table @ Tag清单,其实跟Player的标记是差不多的东西 +---@field public general_pile string[] @ 武将牌堆,这是可用武将名的数组 ---@field public draw_pile integer[] @ 摸牌堆,这是卡牌id的数组 ---@field public discard_pile integer[] @ 弃牌堆,也是卡牌id的数组 ---@field public processing_area integer[] @ 处理区,依然是卡牌id数组 @@ -76,6 +77,7 @@ function Room:initialize(_room) self.game_finished = false self.timeout = _room:getTimeout() self.tag = {} + self.general_pile = {} self.draw_pile = {} self.discard_pile = {} self.processing_area = {} @@ -108,7 +110,7 @@ function Room:resume() -- 如果还没运行的话就先创建自己的主协程 if not self.main_co then self.main_co = coroutine.create(function() - self.tag["_general_pile"] = Fk:getAllGenerals() + self:makeGeneralPile() self:run() end) end @@ -144,6 +146,26 @@ function Room:resume() return true end +-- 构造武将牌堆 +function Room:makeGeneralPile() + local trueNames = {} + local ret = {} + if self.game_started then + for _, player in ipairs(self.players) do + trueNames[Fk.generals[player.general].trueName] = true + end + end + for name, general in pairs(Fk.generals) do + if Fk:canUseGeneral(name) and not trueNames[general.trueName] then + table.insert(ret, name) + trueNames[general.trueName] = true + end + end + table.shuffle(ret) + self.general_pile = ret + return true +end + -- 供调度器使用的函数,用来指示房间是否就绪。 -- 如果没有就绪的话,可能会返回第二个值来告诉调度器自己还有多久就绪。 function Room:isReady() @@ -1241,6 +1263,10 @@ function Room:askForCard(player, minNum, maxNum, includeEquip, skillName, cancel if includeEquip then table.insertTable(hands, player:getCardIds(Player.Equip)) end + local exp = Exppattern:Parse(pattern) + hands = table.filter(hands, function(cid) + return exp:match(Fk:getCardById(cid)) + end) for _ = 1, minNum do local randomId = hands[math.random(1, #hands)] table.insert(chosenCards, randomId) @@ -1296,6 +1322,79 @@ function Room:askForChooseCardAndPlayers(player, targets, minNum, maxNum, patter end end +--- 抽个武将 +--- +--- 同getNCards,抽出来就没有了,所以记得放回去。 +---@param n number @ 数量 +---@param position string|nil @位置,top/bottom,默认top +---@return string[] @ 武将名数组 +function Room:getNGenerals(n, position) + position = position or "top" + assert(position == "top" or position == "bottom") + + local generals = {} + while n > 0 do + + local index = position == "top" and 1 or #self.general_pile + table.insert(generals, table.remove(self.general_pile, index)) + + n = n - 1 + end + + if #generals < 1 then + self:gameOver("") + end + return generals +end + +--- 把武将牌塞回去(……) +---@param g string[] @ 武将名数组 +---@param position string|nil @位置,top/bottom,默认bottom +---@return boolean @ 是否成功 +function Room:returnToGeneralPile(g, position) + position = position or "bottom" + assert(position == "top" or position == "bottom") + if position == "bottom" then + table.insertTable(self.general_pile, g) + elseif position == "top" then + while #g > 0 do + table.insert(self.general_pile, 1, table.remove(g)) + end + end + + return true +end + +--- 抽特定名字的武将(抽了就没了) +---@param name string @ 武将name,如找不到则查找truename,再找不到则返回nil +---@return string | nil @ 抽出的武将名 +function Room:findGeneral(name) + for i, g in ipairs(self.general_pile) do + if g == name or Fk.generals[g].trueName == Fk.generals[name].trueName then + return table.remove(self.general_pile, i) + end + end + return nil +end + +--- 自上而下抽符合特定情况的N个武将(抽了就没了) +---@param func fun(name: string):any @ 武将筛选函数 +---@param n integer | nil @ 抽取数量,数量不足则直接抽干净 +---@return string[] @ 武将组合,可能为空 +function Room:findGenerals(func, n) + n = n or 1 + local ret = {} + local index = 1 + repeat + if func(self.general_pile[index]) then + table.insert(ret, table.remove(self.general_pile, index)) + else + index = index + 1 + end + until index >= #self.general_pile or #ret >= n + return ret +end + --- 询问玩家选择一名武将。 ---@param player ServerPlayer @ 询问目标 ---@param generals string[] @ 可选武将 @@ -2078,7 +2177,12 @@ function Room:askForMoveCardInBoard(player, targetOne, targetTwo, skillName, fla result = json.decode(result) end - local from, to = result.pos == 0 and targetOne, targetTwo or targetTwo, targetOne + local from, to + if result.pos == 0 then + from, to = targetOne, targetTwo + else + from, to = targetTwo, targetOne + end local cardToMove = self:getCardOwner(result.cardId):getVirualEquip(result.cardId) or Fk:getCardById(result.cardId) self:moveCardTo( cardToMove, @@ -2270,7 +2374,7 @@ function Room:doCardUseEffect(cardUseEvent) local realCardIds = self:getSubcardsByRule(cardUseEvent.card, { Card.Processing }) - self.logic:trigger(fk.BeforeCardUseEffect, cardUseEvent.from, cardUseEvent) + self.logic:trigger(fk.BeforeCardUseEffect, self:getPlayerById(cardUseEvent.from), cardUseEvent) -- If using Equip or Delayed trick, move them to the area and return if cardUseEvent.card.type == Card.TypeEquip then if #realCardIds == 0 then @@ -2919,7 +3023,8 @@ function Room:retrial(card, player, judge, skillName, exchange) arg = skillName, } - self:moveCards(move1, move2) + self:moveCards(move1) + self:moveCards(move2) if triggerResponded then self.logic:trigger(fk.CardRespondFinished, player, resp) diff --git a/lua/server/serverplayer.lua b/lua/server/serverplayer.lua index 4f253350..c1d8ec26 100644 --- a/lua/server/serverplayer.lua +++ b/lua/server/serverplayer.lua @@ -743,6 +743,7 @@ function ServerPlayer:setChainState(chained) end function ServerPlayer:reset() + if self.faceup and not self.chained then return end self.room:sendLog{ type = "#ChainStateChange", from = self.id, diff --git a/packages/standard/init.lua b/packages/standard/init.lua index cf7b4daa..fea6ed9a 100644 --- a/packages/standard/init.lua +++ b/packages/standard/init.lua @@ -1164,10 +1164,88 @@ local diaochan = General:new(extension, "diaochan", "qun", 3, 3, General.Female) diaochan:addSkill(lijian) diaochan:addSkill(biyue) +local role_getlogic = function() + local role_logic = GameLogic:subclass("role_logic") + + function role_logic:chooseGenerals() + local room = self.room ---@class Room + local generalNum = room.settings.generalNum + local n = room.settings.enableDeputy and 2 or 1 + local lord = room:getLord() + local lord_generals = {} + local lord_num = 3 + + if lord ~= nil then + room.current = lord + local generals = table.connect(room:findGenerals(function(g) + return table.find(Fk.generals[g].skills, function(s) return s.lordSkill end) + end, lord_num), room:getNGenerals(generalNum)) + if #room.general_pile < (#room.players - 1) * generalNum then + room:gameOver("") + end + lord_generals = room:askForGeneral(lord, generals, n) + local lord_general, deputy + if type(lord_generals) == "table" then + deputy = lord_generals[2] + lord_general = lord_generals[1] + else + lord_general = lord_generals + lord_generals = {lord_general} + end + + generals = table.filter(generals, function(g) return not table.contains(lord_generals, g) end) + room:returnToGeneralPile(generals) + + room:setPlayerGeneral(lord, lord_general, true) + room:askForChooseKingdom({lord}) + room:broadcastProperty(lord, "general") + room:broadcastProperty(lord, "kingdom") + room:setDeputyGeneral(lord, deputy) + room:broadcastProperty(lord, "deputyGeneral") + end + + local nonlord = room:getOtherPlayers(lord, true) + local generals = room:getNGenerals(#nonlord * generalNum) + table.shuffle(generals) + for i, p in ipairs(nonlord) do + local arg = table.slice(generals, (i - 1) * generalNum + 1, i * generalNum + 1) + p.request_data = json.encode{ arg, n } + p.default_reply = table.random(arg, n) + end + + room:notifyMoveFocus(nonlord, "AskForGeneral") + room:doBroadcastRequest("AskForGeneral", nonlord) + + local selected = {} + for _, p in ipairs(nonlord) do + if p.general == "" and p.reply_ready then + local general_ret = json.decode(p.client_reply) + local general = general_ret[1] + local deputy = general_ret[2] + table.insertTableIfNeed(selected, general_ret) + room:setPlayerGeneral(p, general, true, true) + room:setDeputyGeneral(p, deputy) + else + room:setPlayerGeneral(p, p.default_reply[1], true, true) + room:setDeputyGeneral(p, p.default_reply[2]) + end + p.default_reply = "" + end + + generals = table.filter(generals, function(g) return not table.contains(selected, g) end) + room:returnToGeneralPile(generals) + + room:askForChooseKingdom(nonlord) + end + + return role_logic +end + local role_mode = fk.CreateGameMode{ name = "aaa_role_mode", -- just to let it at the top of list minPlayer = 2, maxPlayer = 8, + logic = role_getlogic, is_counted = function(self, room) return #room.players >= 5 end, diff --git a/packages/test/init.lua b/packages/test/init.lua index 95ce37d8..d0a1b3a0 100644 --- a/packages/test/init.lua +++ b/packages/test/init.lua @@ -295,12 +295,14 @@ local change_hero = fk.CreateActiveSkill{ local from = room:getPlayerById(effect.from) local target = room:getPlayerById(effect.tos[1]) local choice = self.interaction.data - local generals = table.map(Fk:getGeneralsRandomly(8, Fk:getAllGenerals()), function(p) return p.name end) + local generals = room:getNGenerals(8) local general = room:askForGeneral(from, generals, 1) if general == nil then general = table.random(generals) end + table.removeOne(generals, general) room:changeHero(target, general, false, choice == "deputyGeneral", true) + room:returnToGeneralPile(generals) end, } local test_zhenggong = fk.CreateTriggerSkill{