diff --git a/Fk/RoomElement/DetailedCheckBox.qml b/Fk/RoomElement/DetailedCheckBox.qml index 7a435bc7..c7c94ad3 100644 --- a/Fk/RoomElement/DetailedCheckBox.qml +++ b/Fk/RoomElement/DetailedCheckBox.qml @@ -39,6 +39,7 @@ GraphicsBox { id: choicetitle width: parent.width text: luatr(modelData) + triggered: root.result.includes(index) enabled: options.indexOf(modelData) !== -1 && (root.result.length < max_num || triggered) textFont.pixelSize: 24 diff --git a/lua/core/card.lua b/lua/core/card.lua index 256dff7f..378d3c5c 100644 --- a/lua/core/card.lua +++ b/lua/core/card.lua @@ -24,6 +24,7 @@ ---@field public special_skills? string[] @ 衍生技能,如重铸 ---@field public is_damage_card boolean @ 是否为会造成伤害的牌 ---@field public multiple_targets boolean @ 是否为指定多个目标的牌 +---@field public is_passive? boolean @ 是否只能在响应时使用或打出 ---@field public is_derived? boolean @ 判断是否为衍生牌 local Card = class("Card") @@ -145,11 +146,12 @@ function Card:clone(suit, number) newCard.special_skills = self.special_skills newCard.is_damage_card = self.is_damage_card newCard.multiple_targets = self.multiple_targets + newCard.is_passive = self.is_passive newCard.is_derived = self.is_derived return newCard end ---- 检测是否为虚拟卡牌,如果其ID为0及以下,则为虚拟卡牌。 +--- 检测是否为虚拟卡牌,如果其ID为0,则为虚拟卡牌。 function Card:isVirtual() return self.id == 0 end diff --git a/lua/core/player.lua b/lua/core/player.lua index cf608f55..5862b820 100644 --- a/lua/core/player.lua +++ b/lua/core/player.lua @@ -872,9 +872,20 @@ end --- 确认玩家是否可以使用特定牌。 ---@param card Card @ 特定牌 -function Player:canUse(card) - assert(card, "Error: No Card") - return card.skill:canUse(self, card) +---@param extra_data? UseExtraData @ 额外数据 +function Player:canUse(card, extra_data) + return card.skill:canUse(self, card, extra_data) +end + +--- 确认玩家是否可以对特定玩家使用特定牌。 +---@param card Card @ 特定牌 +---@param to Player @ 特定玩家 +---@param extra_data? UseExtraData @ 额外数据 +function Player:canUseTo(card, to, extra_data) + if self:prohibitUse(card) or self:isProhibited(to, card) then return false end + local distance_limited = not (extra_data and extra_data.bypass_distances) + local can_use = self:canUse(card, extra_data) + return can_use and card.skill:modTargetFilter(to.id, {}, self.id, card, distance_limited) end --- 确认玩家是否被禁止对特定玩家使用特定牌。 diff --git a/lua/core/skill_type/usable_skill.lua b/lua/core/skill_type/usable_skill.lua index 7ef7bd87..34e9d430 100644 --- a/lua/core/skill_type/usable_skill.lua +++ b/lua/core/skill_type/usable_skill.lua @@ -4,6 +4,7 @@ ---@field public main_skill UsableSkill ---@field public max_use_time integer[] ---@field public expand_pile? string | integer[] | fun(self: UsableSkill): integer[]|string? +---@field public derived_piles? string | string[] local UsableSkill = Skill:subclass("UsableSkill") function UsableSkill:initialize(name, frequency) diff --git a/lua/fk_ex.lua b/lua/fk_ex.lua index aaca1ea8..6ceb1866 100644 --- a/lua/fk_ex.lua +++ b/lua/fk_ex.lua @@ -48,6 +48,11 @@ end local function readUsableSpecToSkill(skill, spec) readCommonSpecToSkill(skill, spec) assert(spec.main_skill == nil or spec.main_skill:isInstanceOf(UsableSkill)) + if type(spec.derived_piles) == "string" then + skill.derived_piles = {spec.derived_piles} + else + skill.derived_piles = spec.derived_piles or {} + end skill.main_skill = spec.main_skill skill.target_num = spec.target_num or skill.target_num skill.min_target_num = spec.min_target_num or skill.min_target_num @@ -191,8 +196,8 @@ function fk.CreateActiveSkill(spec) readUsableSpecToSkill(skill, spec) if spec.can_use then - skill.canUse = function(curSkill, player, card) - return spec.can_use(curSkill, player, card) and curSkill:isEffectable(player) + skill.canUse = function(curSkill, player, card, extra_data) + return spec.can_use(curSkill, player, card, extra_data) and curSkill:isEffectable(player) end end if spec.card_filter then skill.cardFilter = spec.card_filter end @@ -211,9 +216,9 @@ function fk.CreateActiveSkill(spec) if spec.interaction then skill.interaction = setmetatable({}, { - __call = function(self) + __call = function() if type(spec.interaction) == "function" then - return spec.interaction(self) + return spec.interaction(skill) else return spec.interaction end @@ -445,6 +450,7 @@ end ---@field public special_skills? string[] ---@field public is_damage_card? boolean ---@field public multiple_targets? boolean +---@field public is_passive? boolean local defaultCardSkill = fk.CreateActiveSkill{ name = "default_card_skill", @@ -487,6 +493,7 @@ local function readCardSpecToCard(card, spec) card.special_skills = spec.special_skills card.is_damage_card = spec.is_damage_card card.multiple_targets = spec.multiple_targets + card.is_passive = spec.is_passive end ---@param spec CardSpec diff --git a/lua/server/events/gameflow.lua b/lua/server/events/gameflow.lua index fe66b5b8..b44f1041 100644 --- a/lua/server/events/gameflow.lua +++ b/lua/server/events/gameflow.lua @@ -268,7 +268,7 @@ GameEvent.functions[GameEvent.Phase] = function(self) local room = self.room local logic = room.logic - local player = self.data[1] + local player = self.data[1] ---@type Player if not logic:trigger(fk.EventPhaseStart, player) then if player.phase ~= Player.NotActive then logic:trigger(fk.EventPhaseProceeding, player) @@ -285,23 +285,26 @@ GameEvent.functions[GameEvent.Phase] = function(self) end, [Player.Judge] = function() local cards = player:getCardIds(Player.Judge) - for i = #cards, 1, -1 do - local card - card = player:removeVirtualEquip(cards[i]) + while #cards > 0 do + local cid = table.remove(cards) + if not cid then return end + local card = player:removeVirtualEquip(cid) if not card then - card = Fk:getCardById(cards[i]) + card = Fk:getCardById(cid) end - room:moveCardTo(card, Card.Processing, nil, fk.ReasonPut, self.name) + if table.contains(player:getCardIds(Player.Judge), cid) then + room:moveCardTo(card, Card.Processing, nil, fk.ReasonPut, self.name) - ---@type CardEffectEvent - local effect_data = { - card = card, - to = player.id, - tos = { {player.id} }, - } - room:doCardEffect(effect_data) - if effect_data.isCancellOut and card.skill then - card.skill:onNullified(room, effect_data) + ---@type CardEffectEvent + local effect_data = { + card = card, + to = player.id, + tos = { {player.id} }, + } + room:doCardEffect(effect_data) + if effect_data.isCancellOut and card.skill then + card.skill:onNullified(room, effect_data) + end end end end, diff --git a/lua/server/gamelogic.lua b/lua/server/gamelogic.lua index 9e1e340d..71c556d7 100644 --- a/lua/server/gamelogic.lua +++ b/lua/server/gamelogic.lua @@ -599,6 +599,151 @@ function GameLogic:getEventsOfScope(eventType, n, func, scope) return start_event:searchEvents(eventType, n, func) end +-- 在指定历史范围中找符合条件的事件(逆序) +---@param eventType integer @ 要查找的事件类型 +---@param func fun(e: GameEvent): boolean @ 过滤用的函数 +---@param n integer @ 最多找多少个 +---@param end_id integer @ 查询历史范围:从最后的事件开始逆序查找直到id为end_id的事件(不含) +---@return GameEvent[] @ 找到的符合条件的所有事件,最多n个但不保证有n个 +function GameLogic:getEventsByRule(eventType, n, func, end_id) + local ret = {} + local events = self.event_recorder[eventType] or Util.DummyTable + for i = #events, 1, -1 do + local e = events[i] + if e.id <= end_id then break end + if func(e) then + table.insert(ret, e) + if #ret >= n then break end + end + end + return ret +end + + +--- 获取实际的伤害事件 +---@param n integer @ 最多找多少个 +---@param func fun(e: GameEvent): boolean @ 过滤用的函数 +---@param scope? integer @ 查询历史范围,只能是当前阶段/回合/轮次 +---@param end_id? integer @ 查询历史范围:从最后的事件开始逆序查找直到id为end_id的事件(不含) +---@return GameEvent[] @ 找到的符合条件的所有事件,最多n个但不保证有n个 +function GameLogic:getActualDamageEvents(n, func, scope, end_id) + if not end_id then + scope = scope or Player.HistoryTurn + end + + n = n or 1 + func = func or Util.TrueFunc + + local eventType = GameEvent.Damage + local ret = {} + local endIdRecorded + local tempEvents = {} + + local addTempEvents = function(reverse) + if #tempEvents > 0 and #ret < n then + table.sort(tempEvents, function(a, b) + if reverse then + return a.data[1].dealtRecorderId > b.data[1].dealtRecorderId + else + return a.data[1].dealtRecorderId < b.data[1].dealtRecorderId + end + end) + + for _, e in ipairs(tempEvents) do + table.insert(ret, e) + if #ret >= n then return true end + end + end + + endIdRecorded = nil + tempEvents = {} + + return false + end + + if scope then + local event = self:getCurrentEvent() + local start_event ---@type GameEvent + if scope == Player.HistoryGame then + start_event = self.all_game_events[1] + elseif scope == Player.HistoryRound then + start_event = event:findParent(GameEvent.Round, true) + elseif scope == Player.HistoryTurn then + start_event = event:findParent(GameEvent.Turn, true) + elseif scope == Player.HistoryPhase then + start_event = event:findParent(GameEvent.Phase, true) + end + + if not start_event then return {} end + + local events = self.event_recorder[eventType] or Util.DummyTable + local from = start_event.id + local to = start_event.end_id + if math.abs(to) == 1 then to = #self.all_game_events end + + for _, v in ipairs(events) do + local damageStruct = v.data[1] + if damageStruct.dealtRecorderId then + if endIdRecorded and v.id > endIdRecorded then + local result = addTempEvents() + if result then + return ret + end + end + + if v.id >= from and v.id <= to then + if not endIdRecorded and v.end_id > -1 and v.end_id > v.id then + endIdRecorded = v.end_id + end + + if func(v) then + if endIdRecorded then + table.insert(tempEvents, v) + else + table.insert(ret, v) + end + end + end + if #ret >= n then break end + end + end + + addTempEvents() + else + local events = self.event_recorder[eventType] or Util.DummyTable + + for i = #events, 1, -1 do + local e = events[i] + if e.id <= end_id then break end + + local damageStruct = e.data[1] + if damageStruct.dealtRecorderId then + if e.end_id == -1 or (endIdRecorded and endIdRecorded > e.end_id) then + local result = addTempEvents(true) + if result then + return ret + end + + if func(e) then + table.insert(ret, e) + end + else + endIdRecorded = e.end_id + if func(e) then + table.insert(tempEvents, e) + end + end + + if #ret >= n then break end + end + end + + addTempEvents(true) + end + + return ret +end + function GameLogic:dumpEventStack(detailed) local top = self:getCurrentEvent() local i = self.game_event_stack.p diff --git a/lua/server/mark_enum.lua b/lua/server/mark_enum.lua index e9308841..f13cb258 100644 --- a/lua/server/mark_enum.lua +++ b/lua/server/mark_enum.lua @@ -19,23 +19,38 @@ MarkEnum.MinusMaxCards = "MinusMaxCards" ---于本回合内减少标记值数量的手牌上限 MarkEnum.MinusMaxCardsInTurn = "MinusMaxCards-turn" ----使用牌无次数限制,可带清除标记后缀(-tmp为请求专用) +---使用牌无次数限制 MarkEnum.BypassTimesLimit = "BypassTimesLimit" ----使用牌无距离限制,可带清除标记后缀(-tmp为请求专用) +---使用牌无距离限制 MarkEnum.BypassDistancesLimit = "BypassDistancesLimit" ----对其使用牌无次数限制,可带清除标记后缀 +---对其使用牌无次数限制 MarkEnum.BypassTimesLimitTo = "BypassTimesLimitTo" ----对其使用牌无距离限制,可带清除标记后缀 +---对其使用牌无距离限制 MarkEnum.BypassDistancesLimitTo = "BypassDistancesLimitTo" ----非锁定技失效,可带清除标记后缀 +---非锁定技失效 MarkEnum.UncompulsoryInvalidity = "UncompulsoryInvalidity" ----不可明置,可带清除标记后缀(值为表,m - 主将, d - 副将) +---不可明置(值为表,m - 主将, d - 副将) MarkEnum.RevealProhibited = "RevealProhibited" ----不计入距离、座次后缀,可带清除标记后缀 +---不计入距离、座次后缀 MarkEnum.PlayerRemoved = "PlayerRemoved" ---各种清除标记后缀 +--- +---phase:阶段结束后 +--- +---turn:回合结束后 +--- +---round:轮次结束后 MarkEnum.TempMarkSuffix = { "-phase", "-turn", "-round" } ---卡牌标记版本的清除标记后缀 -MarkEnum.CardTempMarkSuffix = { "-phase", "-turn", "-round", "-inhand" } +--- +---phase:阶段结束后 +--- +---turn:回合结束后 +--- +---round:轮次结束后 +--- +---inhand:离开手牌区后 +MarkEnum.CardTempMarkSuffix = { "-phase", "-turn", "-round", + "-inhand" } diff --git a/lua/server/room.lua b/lua/server/room.lua index 47467285..96c2e118 100644 --- a/lua/server/room.lua +++ b/lua/server/room.lua @@ -1726,6 +1726,26 @@ function Room:askForSkillInvoke(player, skill_name, data, prompt) return invoked end +-- 获取使用牌的合法额外目标(【借刀杀人】等带副目标的卡牌除外) +---@param data CardUseStruct @ 使用事件的data +---@param bypass_distances boolean? @ 是否无距离关系的限制 +---@param use_AimGroup boolean? @ 某些场合需要使用AimGroup,by smart Ho-spair +---@return integer[] @ 返回满足条件的player的id列表 +function Room:getUseExtraTargets(data, bypass_distances, use_AimGroup) + if not (data.card.type == Card.TypeBasic or data.card:isCommonTrick()) then return {} end + if data.card.skill:getMinTargetNum() > 1 then return {} end --stupid collateral + local tos = {} + local current_targets = use_AimGroup and AimGroup:getAllTargets(data.tos) or TargetGroup:getRealTargets(data.tos) + for _, p in ipairs(self.alive_players) do + if not table.contains(current_targets, p.id) and not self:getPlayerById(data.from):isProhibited(p, data.card) then + if data.card.skill:modTargetFilter(p.id, {}, data.from, data.card, not bypass_distances) then + table.insert(tos, p.id) + end + end + end + return tos +end + --为使用牌增减目标 ---@param player ServerPlayer @ 执行的玩家 ---@param targets ServerPlayer[] @ 可选的目标范围 @@ -2913,7 +2933,7 @@ end --- 让一名玩家获得一张牌 ---@param player integer|ServerPlayer @ 要拿牌的玩家 ----@param cid integer|Card @ 要拿到的卡牌 +---@param cid integer|Card|integer[] @ 要拿到的卡牌 ---@param unhide? boolean @ 是否明着拿 ---@param reason? CardMoveReason @ 卡牌移动的原因 ---@param proposer? integer @ 移动操作者的id @@ -3114,6 +3134,7 @@ function Room:handleAddLoseSkills(player, skill_names, source_skill, sendlog, no if #skill_names == 0 then return end local losts = {} ---@type boolean[] local triggers = {} ---@type Skill[] + local lost_piles = {} ---@type integer[] for _, skill in ipairs(skill_names) do if string.sub(skill, 1, 1) == "-" then local actual_skill = string.sub(skill, 2, #skill) @@ -3135,12 +3156,17 @@ function Room:handleAddLoseSkills(player, skill_names, source_skill, sendlog, no table.insert(losts, true) table.insert(triggers, s) + if s.derived_piles then + for _, pile_name in ipairs(s.derived_piles) do + table.insertTableIfNeed(lost_piles, player:getPile(pile_name)) + end + end end end else local sk = Fk.skills[skill] if sk and not player:hasSkill(sk, true, true) then - local got_skills = player:addSkill(sk) + local got_skills = player:addSkill(sk, source_skill) for _, s in ipairs(got_skills) do -- TODO: limit skill mark @@ -3171,6 +3197,15 @@ function Room:handleAddLoseSkills(player, skill_names, source_skill, sendlog, no self.logic:trigger(event, player, triggers[i]) end end + + if #lost_piles > 0 then + self:moveCards({ + ids = lost_piles, + from = player.id, + toArea = Card.DiscardPile, + moveReason = fk.ReasonPutIntoDiscardPile, + }) + end end -- 判定 @@ -3236,7 +3271,7 @@ function Room:retrial(card, player, judge, skillName, exchange) end --- 弃置一名角色的牌。 ----@param card_ids integer[] @ 被弃掉的牌 +---@param card_ids integer[]|integer @ 被弃掉的牌 ---@param skillName? string @ 技能名 ---@param who ServerPlayer @ 被弃牌的人 ---@param thrower? ServerPlayer @ 弃别人牌的人 diff --git a/lua/server/system_enum.lua b/lua/server/system_enum.lua index 483de916..e75d679a 100644 --- a/lua/server/system_enum.lua +++ b/lua/server/system_enum.lua @@ -134,7 +134,7 @@ fk.IceDamage = 4 ---@field public additionalEffect? integer ---@class CardEffectEvent ----@field public from integer +---@field public from? integer ---@field public to integer ---@field public subTargets? integer[] ---@field public tos TargetGroup diff --git a/packages/standard_cards/init.lua b/packages/standard_cards/init.lua index a8cc087c..c0559fcf 100644 --- a/packages/standard_cards/init.lua +++ b/packages/standard_cards/init.lua @@ -112,6 +112,7 @@ local jink = fk.CreateBasicCard{ suit = Card.Heart, number = 2, skill = jinkSkill, + is_passive = true, } extension:addCards({ @@ -468,6 +469,7 @@ local nullification = fk.CreateTrickCard{ suit = Card.Spade, number = 11, skill = nullificationSkill, + is_passive = true, } extension:addCards({