diff --git a/lua/client/client_util.lua b/lua/client/client_util.lua
index 9419edf8..082239cc 100644
--- a/lua/client/client_util.lua
+++ b/lua/client/client_util.lua
@@ -160,7 +160,7 @@ end
function GetSkillData(skill_name)
local skill = Fk.skills[skill_name]
local freq = "notactive"
- if skill:isInstanceOf(ActiveSkill) then
+ if skill:isInstanceOf(ActiveSkill) or skill:isInstanceOf(ViewAsSkill) then
freq = "active"
end
return json.encode{
@@ -173,8 +173,12 @@ end
function ActiveCanUse(skill_name)
local skill = Fk.skills[skill_name]
local ret = false
- if skill and skill:isInstanceOf(ActiveSkill) then
- ret = skill:canUse(Self)
+ if skill then
+ if skill:isInstanceOf(ActiveSkill) then
+ ret = skill:canUse(Self)
+ elseif skill:isInstanceOf(ViewAsSkill) then
+ ret = skill:enabledAtPlay(Self)
+ end
end
return json.encode(ret)
end
@@ -182,8 +186,12 @@ end
function ActiveCardFilter(skill_name, to_select, selected, selected_targets)
local skill = Fk.skills[skill_name]
local ret = false
- if skill and skill:isInstanceOf(ActiveSkill) then
- ret = skill:cardFilter(to_select, selected, selected_targets)
+ if skill then
+ if skill:isInstanceOf(ActiveSkill) then
+ ret = skill:cardFilter(to_select, selected, selected_targets)
+ elseif skill:isInstanceOf(ViewAsSkill) then
+ ret = skill:cardFilter(to_select, selected)
+ end
end
return json.encode(ret)
end
@@ -191,8 +199,15 @@ end
function ActiveTargetFilter(skill_name, to_select, selected, selected_cards)
local skill = Fk.skills[skill_name]
local ret = false
- if skill and skill:isInstanceOf(ActiveSkill) then
- ret = skill:targetFilter(to_select, selected, selected_cards)
+ if skill then
+ if skill:isInstanceOf(ActiveSkill) then
+ ret = skill:targetFilter(to_select, selected, selected_cards)
+ elseif skill:isInstanceOf(ViewAsSkill) then
+ local card = skill:viewAs(selected_cards)
+ if card then
+ ret = card.skill:targetFilter(to_select, selected, selected_cards)
+ end
+ end
end
return json.encode(ret)
end
@@ -200,15 +215,46 @@ end
function ActiveFeasible(skill_name, selected, selected_cards)
local skill = Fk.skills[skill_name]
local ret = false
- if skill and skill:isInstanceOf(ActiveSkill) then
- ret = skill:feasible(selected, selected_cards)
+ if skill then
+ if skill:isInstanceOf(ActiveSkill) then
+ ret = skill:feasible(selected, selected_cards)
+ elseif skill:isInstanceOf(ViewAsSkill) then
+ local card = skill:viewAs(selected_cards)
+ if card then
+ ret = card.skill:feasible(selected, selected_cards)
+ end
+ end
end
return json.encode(ret)
end
--- ViewAsSkill (Todo)
function CanViewAs(skill_name, card_ids)
- return "true"
+ local skill = Fk.skills[skill_name]
+ local ret = false
+ if skill then
+ if skill:isInstanceOf(ViewAsSkill) then
+ ret = skill:viewAs(card_ids) ~= nil
+ elseif skill:isInstanceOf(ActiveSkill) then
+ ret = true
+ end
+ end
+ return json.encode(ret)
+end
+
+function CardFitPattern(card_name, pattern)
+ local exp = Exppattern:Parse(pattern)
+ local ret = exp:matchExp(card_name)
+ return json.encode(ret)
+end
+
+function SkillFitPattern(skill_name, pattern)
+ local skill = Fk.skills[skill_name]
+ local ret = false
+ if skill and skill.pattern then
+ local exp = Exppattern:Parse(pattern)
+ ret = exp:matchExp(skill.pattern)
+ end
+ return json.encode(ret)
end
Fk:loadTranslationTable{
@@ -355,10 +401,22 @@ Fk:loadTranslationTable{
-- useCard
["#UseCard"] = "%from 使用了牌 %card",
["#UseCardToTargets"] = "%from 使用了牌 %card,目标是 %to",
- ["#CardUseCollaborator"] = "%from 在此次 %card 中的子目标是 %to",
+ ["#CardUseCollaborator"] = "%from 在此次 %arg 中的子目标是 %to",
["#UseCardToCard"] = "%from 使用了牌 %card,目标是 %arg",
["#ResponsePlayCard"] = "%from 打出了牌 %card",
+ ["#UseVCard"] = "%from 将 %card 当 %arg 使用",
+ ["#UseVCardToTargets"] = "%from 将 %card 当 %arg 使用,目标是 %to",
+ ["#UseVCardToCard"] = "%from 将 %card 当 %arg2 使用,目标是 %arg",
+ ["#ResponsePlayVCard"] = "%from 将 %card 当 %arg 打出",
+ ["#UseV0Card"] = "%from 使用了 %arg",
+ ["#UseV0CardToTargets"] = "%from 使用了 %arg,目标是 %to",
+ ["#UseV0CardToCard"] = "%from 使用了 %arg2,目标是 %arg",
+ ["#ResponsePlayV0Card"] = "%from 打出了 %arg",
+
+ -- skill
+ ["#InvokeSkill"] = "%from 发动了 “%arg”",
+
-- judge
["#StartJudgeReason"] = "%from 开始了 %arg 的判定",
["#InitialJudge"] = "%from 的判定牌为 %card",
diff --git a/lua/core/card.lua b/lua/core/card.lua
index 56052e10..f29e094e 100644
--- a/lua/core/card.lua
+++ b/lua/core/card.lua
@@ -9,6 +9,7 @@
---@field type CardType
---@field sub_type CardSubtype
---@field area CardArea
+---@field subcards integer[]
local Card = class("Card")
---@alias Suit integer
@@ -74,6 +75,74 @@ function Card:initialize(name, suit, number, color)
self.type = 0
self.sub_type = Card.SubTypeNone
self.skill = nil
+ self.subcards = {}
+end
+
+---@param suit Suit
+---@param number integer
+---@return Card
+function Card:clone(suit, number)
+ local newCard = self.class:new(self.name, suit, number)
+ newCard.skill = self.skill
+ return newCard
+end
+
+function Card:isVirtual()
+ return self.id <= 0
+end
+
+function Card:getEffectiveId()
+ if self:isVirtual() then
+ return #self.subcards > 0 and self.subcards[1] or nil
+ end
+ return self.id
+end
+
+local function updateColorAndNumber(card)
+ local color = Card.NoColor
+ local number = 0
+ local different_color = false
+ for _, id in ipairs(card.subcards) do
+ local c = Fk:getCardById(id)
+ number = math.min(number + c.number, 13)
+ if color ~= c.color then
+ if not different_color then
+ if color ~= Card.NoColor then
+ different_color = true
+ end
+ color = c.color
+ else
+ color = Card.NoColor
+ end
+ end
+ end
+
+ card.color = color
+ card.number = number
+end
+
+---@param card integer|Card
+function Card:addSubcard(card)
+ if type(card) == "number" then
+ table.insert(self.subcards, card)
+ else
+ assert(card:isInstanceOf(Card))
+ assert(not card:isVirtual(), "Can not add virtual card as subcard")
+ table.insert(self.subcards, card.id)
+ end
+
+ updateColorAndNumber(self)
+end
+
+function Card:addSubcards(cards)
+ for _, c in ipairs(cards) do
+ self:addSubcard(c)
+ end
+end
+
+function Card:clearSubcards()
+ self.subcards = {}
+ updateColorAndNumber(self)
end
function Card:getSuitString()
@@ -91,6 +160,16 @@ function Card:getSuitString()
end
end
+function Card:getColorString()
+ local color = self.color
+ if color == Card.Black then
+ return "black"
+ elseif color == Card.Red then
+ return "red"
+ end
+ return "nocolor"
+end
+
local function getNumberStr(num)
if num == 1 then
return "A"
@@ -107,9 +186,13 @@ end
-- for sendLog
function Card:toLogString()
local ret = string.format('%s', Fk:translate(self.name) .. "[")
- ret = ret .. Fk:translate("log_" .. self:getSuitString())
- if self.number > 0 then
- ret = ret .. string.format('%s', self.color == Card.Red and "#CC3131" or "black", getNumberStr(self.number))
+ if self:isVirtual() then
+ ret = ret .. Fk:translate(self:getColorString())
+ else
+ ret = ret .. Fk:translate("log_" .. self:getSuitString())
+ if self.number > 0 then
+ ret = ret .. string.format('%s', self.color == Card.Red and "#CC3131" or "black", getNumberStr(self.number))
+ end
end
ret = ret .. ']'
return ret
diff --git a/lua/core/card_type/basic.lua b/lua/core/card_type/basic.lua
index 330811cc..e969713f 100644
--- a/lua/core/card_type/basic.lua
+++ b/lua/core/card_type/basic.lua
@@ -6,13 +6,4 @@ function BasicCard:initialize(name, suit, number)
self.type = Card.TypeBasic
end
----@param suit Suit
----@param number integer
----@return BasicCard
-function BasicCard:clone(suit, number)
- local newCard = BasicCard:new(self.name, suit, number)
- newCard.skill = self.skill
- return newCard
-end
-
return BasicCard
diff --git a/lua/core/card_type/equip.lua b/lua/core/card_type/equip.lua
index 2b072065..37db029e 100644
--- a/lua/core/card_type/equip.lua
+++ b/lua/core/card_type/equip.lua
@@ -17,15 +17,6 @@ function Weapon:initialize(name, suit, number, attackRange)
self.attack_range = attackRange or 1
end
----@param suit Suit
----@param number integer
----@return Weapon
-function Weapon:clone(suit, number)
- local newCard = Weapon:new(self.name, suit, number, self.attack_range)
- newCard.skill = self.skill
- return newCard
-end
-
---@class Armor : EquipCard
local Armor = EquipCard:subclass("armor")
@@ -34,15 +25,6 @@ function Armor:initialize(name, suit, number)
self.sub_type = Card.SubtypeArmor
end
----@param suit Suit
----@param number integer
----@return Armor
-function Armor:clone(suit, number)
- local newCard = Armor:new(self.name, suit, number)
- newCard.skill = self.skill
- return newCard
-end
-
---@class DefensiveRide : EquipCard
local DefensiveRide = EquipCard:subclass("DefensiveRide")
@@ -51,15 +33,6 @@ function DefensiveRide:initialize(name, suit, number)
self.sub_type = Card.SubtypeDefensiveRide
end
----@param suit Suit
----@param number integer
----@return DefensiveRide
-function DefensiveRide:clone(suit, number)
- local newCard = DefensiveRide:new(self.name, suit, number)
- newCard.skill = self.skill
- return newCard
-end
-
---@class OffensiveRide : EquipCard
local OffensiveRide = EquipCard:subclass("OffensiveRide")
@@ -68,15 +41,6 @@ function OffensiveRide:initialize(name, suit, number)
self.sub_type = Card.SubtypeOffensiveRide
end
----@param suit Suit
----@param number integer
----@return OffensiveRide
-function OffensiveRide:clone(suit, number)
- local newCard = OffensiveRide:new(self.name, suit, number)
- newCard.skill = self.skill
- return newCard
-end
-
---@class Treasure : EquipCard
local Treasure = EquipCard:subclass("Treasure")
@@ -85,13 +49,4 @@ function Treasure:initialize(name, suit, number)
self.sub_type = Card.SubtypeTreasure
end
----@param suit Suit
----@param number integer
----@return Treasure
-function Treasure:clone(suit, number)
- local newCard = Treasure:new(self.name, suit, number)
- newCard.skill = self.skill
- return newCard
-end
-
return { EquipCard, Weapon, Armor, DefensiveRide, OffensiveRide, Treasure }
diff --git a/lua/core/card_type/trick.lua b/lua/core/card_type/trick.lua
index 2afc0f72..0c366e68 100644
--- a/lua/core/card_type/trick.lua
+++ b/lua/core/card_type/trick.lua
@@ -6,17 +6,6 @@ function TrickCard:initialize(name, suit, number)
self.type = Card.TypeTrick
end
----@param suit Suit
----@param number integer
----@return TrickCard
-function TrickCard:clone(suit, number)
- local newCard = TrickCard:new(self.name, suit, number)
-
- newCard.skill = self.skill
-
- return newCard
-end
-
---@class DelayedTrickCard : TrickCard
local DelayedTrickCard = TrickCard:subclass("DelayedTrickCard")
@@ -25,13 +14,4 @@ function DelayedTrickCard:initialize(name, suit, number)
self.sub_type = Card.SubtypeDelayedTrick
end
----@param suit Suit
----@param number integer
----@return DelayedTrickCard
-function DelayedTrickCard:clone(suit, number)
- local newCard = DelayedTrickCard:new(self.name, suit, number)
- newCard.skill = self.skill
- return newCard
-end
-
return { TrickCard, DelayedTrickCard }
diff --git a/lua/core/engine.lua b/lua/core/engine.lua
index 6164939a..c2e00002 100644
--- a/lua/core/engine.lua
+++ b/lua/core/engine.lua
@@ -137,12 +137,14 @@ function Engine:addGenerals(generals)
end
local cardId = 1
+local _card_name_table = {}
---@param card Card
function Engine:addCard(card)
assert(card.class:isSubclassOf(Card))
card.id = cardId
cardId = cardId + 1
table.insert(self.cards, card)
+ _card_name_table[card.name] = card
end
---@param cards Card[]
@@ -152,6 +154,16 @@ function Engine:addCards(cards)
end
end
+---@param name string
+---@param suit Suit
+---@param number integer
+---@return Card
+function Engine:cloneCard(name, suit, number)
+ local cd = _card_name_table[name]
+ assert(cd, "Attempt to clone a card that not added to engine")
+ return cd:clone(suit, number)
+end
+
---@param num integer
---@param generalPool General[]
---@param except string[]
diff --git a/lua/core/exppattern.lua b/lua/core/exppattern.lua
new file mode 100644
index 00000000..533e9738
--- /dev/null
+++ b/lua/core/exppattern.lua
@@ -0,0 +1,246 @@
+--[[
+
+ Exppattern is a string that describes cards of a same 'type', e.g. name,
+ suit, etc.
+
+ The string will be parsed and construct a new Exppattern instance.
+ Then we can use this instance to check the card.
+
+ Syntax for the string form:
+ 1. the whole string can be splited by ';'. Every slice stands for a Matcher
+ 2. For the matcher string, it can be splited by '|'.
+ 3. And the arrays in class Match is concated by ',' in string.
+
+ Example:
+ slash,jink|2~4|spade;.|.|.|.|.|trick
+
+]]--
+
+---@class Matcher
+---@field name string[]
+---@field number integer[]
+---@field suit integer[]
+---@field place string[]
+---@field generalName string[]
+---@field cardType integer[]
+---@field id integer[]
+
+local numbertable = {
+ ["A"] = 1,
+ ["J"] = 11,
+ ["Q"] = 12,
+ ["K"] = 13,
+}
+
+local suittable = {
+ [Card.Spade] = "spade",
+ [Card.Club] = "club",
+ [Card.Heart] = "heart",
+ [Card.Diamond] = "diamond",
+}
+
+local typetable = {
+ [Card.TypeBasic] = "basic",
+ [Card.TypeTrick] = "trick",
+ [Card.TypeEquip] = "equip",
+}
+
+---@param matcher Matcher
+---@param card Card
+local function matchCard(matcher, card)
+ if type(card) == "number" then
+ card = Fk:getCardById(card)
+ end
+
+ if matcher.name and not table.contains(matcher.name, card.name) then
+ return false
+ end
+
+ if matcher.number and not table.contains(matcher.number, card.number) then
+ return false
+ end
+
+ if matcher.suit and not table.contains(matcher.suit, card:getSuitString()) then
+ return false
+ end
+
+ -- TODO: place
+ -- TODO: generalName
+
+ if matcher.cardType and not table.contains(matcher.cardType, typetable[card.type]) then
+ return false
+ end
+
+ if matcher.id and not table.contains(matcher.id, card.id) then
+ return false
+ end
+
+ return true
+end
+
+local function hasIntersection(a, b)
+ if a == nil or b == nil then
+ return true
+ end
+
+ local tmp = {}
+ for _, e in ipairs(a) do
+ tmp[e] = true
+ end
+ for _, e in ipairs(b) do
+ if tmp[e] then
+ return true
+ end
+ end
+ return false
+end
+
+---@param a Matcher
+---@param b Matcher
+local function matchMatcher(a, b)
+ local keys = {
+ "name",
+ "number",
+ "suit",
+ "place",
+ "generalName",
+ "cardType",
+ "id",
+ }
+
+ for _, k in ipairs(keys) do
+ if not hasIntersection(a[k], b[k]) then
+ return false
+ end
+ end
+
+ return true
+end
+
+local function parseMatcher(str)
+ local t = str:split("|")
+ if #t < 7 then
+ for i = 1, 7 - #t do
+ table.insert(t, ".")
+ end
+ end
+
+ for i, item in ipairs(t) do
+ t[i] = item:split(",")
+ end
+
+ local ret = {} ---@type Matcher
+ ret.name = not table.contains(t[1], ".") and t[1] or nil
+
+ if not table.contains(t[2], ".") then
+ ret.number = {}
+ for _, num in ipairs(t[2]) do
+ local n = tonumber(num)
+ if not n then
+ n = numbertable[num]
+ end
+ if n then
+ table.insertIfNeed(ret.number, n)
+ else
+ if string.find(n, "~") then
+ local start, _end = table.unpack(n:split("~"))
+ for i = start, _end do
+ table.insertIfNeed(ret.number, n)
+ end
+ end
+ end
+ end
+ end
+
+ if not table.contains(t[3], ".") then
+ ret.suit = {}
+ for _, num in ipairs(t[3]) do
+ local n = suittable[num]
+ if n then
+ table.insertIfNeed(ret.suit, n)
+ end
+ end
+ end
+
+ ret.place = not table.contains(t[4], ".") and t[4] or nil
+ ret.generalName = not table.contains(t[5], ".") and t[5] or nil
+
+ if not table.contains(t[6], ".") then
+ ret.cardType = {}
+ for _, num in ipairs(t[6]) do
+ local n = typetable[num]
+ if n then
+ table.insertIfNeed(ret.cardType, n)
+ end
+ end
+ end
+
+ if not table.contains(t[7], ".") then
+ ret.id = {}
+ for _, num in ipairs(t[6]) do
+ local n = tonumber(num)
+ if n and n > 0 then
+ table.insertIfNeed(ret.id, n)
+ end
+ end
+ end
+
+ return ret
+end
+
+---@class Exppattern: Object
+---@field matchers Matcher[]
+local Exppattern = class("Exppattern")
+
+function Exppattern:initialize(spec)
+ if not spec then
+ self.matchers = {}
+ elseif spec[1] ~= nil then
+ self.matchers = spec
+ else
+ self.matchers = {}
+ self.matchers[1] = spec
+ end
+end
+
+---@param str string
+function Exppattern.static:Parse(str)
+ local ret = Exppattern:new()
+ local t = str:split(";")
+ for i, s in ipairs(t) do
+ ret.matchers[i] = parseMatcher(s)
+ end
+ return ret
+end
+
+---@param card Card
+function Exppattern:match(card)
+ for _, matcher in ipairs(self.matchers) do
+ local result = matchCard(matcher, card)
+ if result then
+ return true
+ end
+ end
+ return false
+end
+
+function Exppattern:matchExp(exp)
+ if type(exp) == "string" then
+ exp = Exppattern:Parse(exp)
+ end
+
+ local a = self.matchers
+ local b = exp.matchers
+
+ for _, m in ipairs(a) do
+ for _, n in ipairs(b) do
+ if matchMatcher(m, n) then
+ return true
+ end
+ end
+ end
+
+ return false
+end
+
+return Exppattern
diff --git a/lua/core/skill_type/view_as.lua b/lua/core/skill_type/view_as.lua
index e69de29b..bbaa3e7c 100644
--- a/lua/core/skill_type/view_as.lua
+++ b/lua/core/skill_type/view_as.lua
@@ -0,0 +1,35 @@
+---@class ViewAsSkill
+---@field pattern string @ cards that can be viewAs'ed by this skill
+local ViewAsSkill = Skill:subclass("ViewAsSkill")
+
+function ViewAsSkill:initialize(name)
+ Skill.initialize(self, name, Skill.NotFrequent)
+ self.pattern = ""
+end
+
+---@param to_select integer @ id of a card not selected
+---@param selected integer[] @ ids of selected cards
+---@return boolean
+function ViewAsSkill:cardFilter(to_select, selected)
+ return false
+end
+
+---@param cards integer[] @ ids of cards
+---@return card
+function ViewAsSkill:viewAs(cards)
+ return nil
+end
+
+-- For extra judgement, like mark or HP
+
+---@param player Player
+function ViewAsSkill:enabledAtPlay(player)
+ return true
+end
+
+---@param player Player
+function ViewAsSkill:enabledAtResponse(player)
+ return true
+end
+
+return ViewAsSkill
diff --git a/lua/fk_ex.lua b/lua/fk_ex.lua
index 9a736110..233aadd2 100644
--- a/lua/fk_ex.lua
+++ b/lua/fk_ex.lua
@@ -4,6 +4,7 @@ dofile "lua/server/event.lua"
dofile "lua/server/system_enum.lua"
TriggerSkill = require "core.skill_type.trigger"
ActiveSkill = require "core.skill_type.active_skill"
+ViewAsSkill = require "core.skill_type.view_as"
DistanceSkill = require "core.skill_type.distance"
StatusSkills = {
DistanceSkill,
@@ -111,6 +112,37 @@ function fk.CreateActiveSkill(spec)
return skill
end
+---@class ViewAsSkillSpec: SkillSpec
+---@field card_filter fun(self: ViewAsSkill, to_select: integer, selected: integer[]): boolean
+---@field view_as fun(self: ViewAsSkill, cards: integer[])
+---@field pattern string
+---@field enabled_at_play fun(self: ViewAsSkill, player: Player): boolean
+---@field enabled_at_response fun(self: ViewAsSkill, player: Player): boolean
+
+---@param spec ViewAsSkillSpec
+---@return ViewAsSkill
+function fk.CreateViewAsSkill(spec)
+ assert(type(spec.name) == "string")
+ assert(type(spec.view_as) == "function")
+
+ local skill = ViewAsSkill:new(spec.name)
+ skill.viewAs = spec.view_as
+ if spec.card_filter then
+ skill.cardFilter = spec.card_filter
+ end
+ if type(spec.pattern) == "string" then
+ skill.pattern = spec.pattern
+ end
+ if type(spec.enabled_at_play) == "function" then
+ skill.enabledAtPlay = spec.enabled_at_play
+ end
+ if type(spec.enabled_at_response) == "function" then
+ skill.enabledAtResponse = spec.enabled_at_response
+ end
+
+ return skill
+end
+
---@class DistanceSpec: SkillSpec
---@field correct_func fun(self: DistanceSkill, from: Player, to: Player)
---@field global boolean
diff --git a/lua/freekill.lua b/lua/freekill.lua
index 9122bc7d..2754f077 100644
--- a/lua/freekill.lua
+++ b/lua/freekill.lua
@@ -20,6 +20,7 @@ Engine = require "core.engine"
Package = require "core.package"
General = require "core.general"
Card = require "core.card"
+Exppattern = require "core.exppattern"
Skill = require "core.skill"
Player = require "core.player"
diff --git a/lua/server/room.lua b/lua/server/room.lua
index bb649f9c..6fc89dcc 100644
--- a/lua/server/room.lua
+++ b/lua/server/room.lua
@@ -55,9 +55,15 @@ function Room:initialize(_room)
end
local co = coroutine.create(co_func)
while not self.game_finished do
- coroutine.resume(co)
+ local ret, err_msg = coroutine.resume(co)
+
+ -- handle error
+ if ret == false then
+ fk.qCritical(err_msg)
+ print(debug.traceback(co))
+ break
+ end
end
- fk.qInfo("Game Finished.")
end
self.players = {}
@@ -426,6 +432,19 @@ function Room:sendLogEvent(type, data, players)
self:doBroadcastNotify("LogEvent", json.encode(data), players)
end
+---@param player ServerPlayer
+---@param skill_name string
+---@param skill_type nil
+function Room:notifySkillInvoked(player, skill_name, skill_type)
+ self:sendLog{
+ type = "#InvokeSkill",
+ from = player.id,
+ arg = skill_name,
+ }
+
+ -- TODO: notifySkill animation
+end
+
------------------------------------------------------------------------
-- interactive functions
------------------------------------------------------------------------
@@ -604,64 +623,101 @@ function Room:askForSkillInvoke(player, skill_name, data)
end
---@param player ServerPlayer
+---@param data string
---@return CardUseStruct
-function Room:askForUseCard(player, card_name, prompt, cancelable, extra_data)
+function Room:handleUseCardReply(player, data)
+ data = json.decode(data)
+ local card = data.card
+ local targets = data.targets
+ if type(card) == "string" then
+ local card_data = json.decode(card)
+ local skill = Fk.skills[card_data.skill]
+ local selected_cards = card_data.subcards
+ if skill:isInstanceOf(ActiveSkill) then
+ self:notifySkillInvoked(player, skill.name)
+ skill:onEffect(self, {
+ from = player.id,
+ cards = selected_cards,
+ tos = targets,
+ })
+ return nil
+ elseif skill:isInstanceOf(ViewAsSkill) then
+ local c = skill:viewAs(selected_cards)
+ if c then
+ self:notifySkillInvoked(player, skill.name)
+ local use = {} ---@type CardUseStruct
+ use.from = player.id
+ use.tos = {}
+ for _, target in ipairs(targets) do
+ table.insert(use.tos, { target })
+ end
+ use.card = c
+ return use
+ end
+ end
+ else
+ local use = {} ---@type CardUseStruct
+ use.from = player.id
+ use.tos = {}
+ for _, target in ipairs(targets) do
+ table.insert(use.tos, { target })
+ end
+ if #use.tos == 0 then
+ use.tos = nil
+ end
+ use.card = Fk:getCardById(card)
+ return use
+ end
+end
+
+-- available extra_data:
+-- * must_targets: integer[]
+---@param player ServerPlayer
+---@param card_name string
+---@param pattern string
+---@param prompt string
+---@return CardUseStruct
+function Room:askForUseCard(player, card_name, pattern, prompt, cancelable, extra_data)
local command = "AskForUseCard"
self:notifyMoveFocus(player, card_name)
cancelable = cancelable or false
extra_data = extra_data or {}
+ pattern = pattern or card_name
prompt = prompt or "#AskForUseCard"
- local data = {card_name, prompt, cancelable, extra_data}
+ local data = {card_name, pattern, prompt, cancelable, extra_data}
local result = self:doRequest(player, command, json.encode(data))
if result ~= "" then
- data = json.decode(result)
- local card = data.card
- local targets = data.targets
- if type(card) == "string" then
- -- TODO: ViewAsSkill
- return nil
- else
- local use = {} ---@type CardUseStruct
- use.from = player.id
- use.tos = {}
- for _, target in ipairs(targets) do
- table.insert(use.tos, { target })
- end
- if #use.tos == 0 then
- use.tos = nil
- end
- use.cardId = card
- return use
- end
+ return self:handleUseCardReply(player, result)
end
return nil
end
-function Room:askForResponse(player, card_name, prompt, cancelable, extra_data)
+---@param player ServerPlayer
+---@param card_name string
+---@param pattern string
+---@param prompt string
+---@param cancelable string
+function Room:askForResponse(player, card_name, pattern, prompt, cancelable, extra_data)
local command = "AskForResponseCard"
self:notifyMoveFocus(player, card_name)
cancelable = cancelable or false
extra_data = extra_data or {}
+ pattern = pattern or card_name
prompt = prompt or "#AskForResponseCard"
- local data = {card_name, prompt, cancelable, extra_data}
+ local data = {card_name, pattern, prompt, cancelable, extra_data}
local result = self:doRequest(player, command, json.encode(data))
if result ~= "" then
- data = json.decode(result)
- local card = data.card
- local targets = data.targets
- if type(card) == "string" then
- -- TODO: ViewAsSkill
- return nil
- else
- return card
+ local use = self:handleUseCardReply(player, result)
+ if use then
+ return use.card
end
end
return nil
end
-function Room:askForNullification(players, card_name, prompt, cancelable, extra_data)
+function Room:askForNullification(players, card_name, pattern, prompt, cancelable, extra_data)
if #players == 0 then
return nil
end
@@ -671,33 +727,16 @@ function Room:askForNullification(players, card_name, prompt, cancelable, extra_
cancelable = cancelable or false
extra_data = extra_data or {}
prompt = prompt or "#AskForUseCard"
+ pattern = pattern or card_name
self:notifyMoveFocus(self.alive_players, card_name)
self:doBroadcastNotify("WaitForNullification", "")
- local data = {card_name, prompt, cancelable, extra_data}
+ local data = {card_name, pattern, prompt, cancelable, extra_data}
local winner = self:doRaceRequest(command, players, json.encode(data))
if winner then
local result = winner.client_reply
- data = json.decode(result)
- local card = data.card
- local targets = data.targets
- if type(card) == "string" then
- -- TODO: ViewAsSkill
- return nil
- else
- local use = {} ---@type CardUseStruct
- use.from = winner.id
- use.tos = {}
- for _, target in ipairs(targets) do
- table.insert(use.tos, { target })
- end
- if #use.tos == 0 then
- use.tos = nil
- end
- use.cardId = card
- return use
- end
+ return self:handleUseCardReply(winner, result)
end
return nil
end
@@ -706,6 +745,114 @@ end
-- use card logic, and wrappers
------------------------------------------------------------------------
+---@param room Room
+---@param cardUseEvent CardUseStruct
+local sendCardEmotionAndLog = function(room, cardUseEvent)
+ local from = cardUseEvent.from
+ local card = cardUseEvent.card
+ room:setEmotion(room:getPlayerById(from), card.name)
+ room:doAnimate("Indicate", {
+ from = from,
+ to = cardUseEvent.tos or {},
+ })
+
+ local useCardIds = card:isVirtual() and card.subcards or { card.id }
+ if cardUseEvent.tos and #cardUseEvent.tos > 0 then
+ local to = {}
+ for _, t in ipairs(cardUseEvent.tos) do
+ table.insert(to, t[1])
+ end
+
+ if card:isVirtual() then
+ if #useCardIds == 0 then
+ room:sendLog{
+ type = "#UseV0CardToTargets",
+ from = from,
+ to = to,
+ arg = card:toLogString(),
+ }
+ else
+ room:sendLog{
+ type = "#UseVCardToTargets",
+ from = from,
+ to = to,
+ card = useCardIds,
+ arg = card:toLogString(),
+ }
+ end
+ else
+ room:sendLog{
+ type = "#UseCardToTargets",
+ from = from,
+ to = to,
+ card = useCardIds
+ }
+ end
+
+ for _, t in ipairs(cardUseEvent.tos) do
+ if t[2] then
+ local temp = {table.unpack(t)}
+ table.remove(temp, 1)
+ room:sendLog{
+ type = "#CardUseCollaborator",
+ from = t[1],
+ to = temp,
+ arg = card.name,
+ }
+ end
+ end
+ elseif cardUseEvent.toCard then
+ if card:isVirtual() then
+ if #useCardIds == 0 then
+ room:sendLog{
+ type = "#UseV0CardToCard",
+ from = from,
+ arg = cardUseEvent.toCard.name,
+ arg2 = card:toLogString(),
+ }
+ else
+ room:sendLog{
+ type = "#UseVCardToCard",
+ from = from,
+ card = useCardIds,
+ arg = cardUseEvent.toCard.name,
+ arg2 = card:toLogString(),
+ }
+ end
+ else
+ room:sendLog{
+ type = "#UseCardToCard",
+ from = from,
+ card = useCardIds,
+ arg = cardUseEvent.toCard.name,
+ }
+ end
+ else
+ if card:isVirtual() then
+ if #useCardIds == 0 then
+ room:sendLog{
+ type = "#UseV0Card",
+ from = from,
+ arg = card:toLogString(),
+ }
+ else
+ room:sendLog{
+ type = "#UseVCard",
+ from = from,
+ card = useCardIds,
+ arg = card:toLogString(),
+ }
+ end
+ else
+ room:sendLog{
+ type = "#UseCard",
+ from = from,
+ card = useCardIds,
+ }
+ end
+ end
+end
+
---@param room Room
---@param cardUseEvent CardUseStruct
---@param aimEventCollaborators table
@@ -732,7 +879,7 @@ local onAim = function(room, cardUseEvent, aimEventCollaborators)
if not aimEventCollaborators[toId] or collaboratorsIndex[toId] > #aimEventCollaborators[toId] then
aimStruct = {
from = cardUseEvent.from,
- cardId = cardUseEvent.cardId,
+ card = cardUseEvent.card,
to = toId,
targetGroup = cardUseEvent.tos,
nullifiedTargets = cardUseEvent.nullifiedTargets or {},
@@ -760,7 +907,7 @@ local onAim = function(room, cardUseEvent, aimEventCollaborators)
else
aimStruct = aimEventCollaborators[toId][collaboratorsIndex[toId]]
aimStruct.from = cardUseEvent.from
- aimStruct.cardId = cardUseEvent.cardId
+ aimStruct.card = cardUseEvent.card
aimStruct.tos = aimGroup
aimStruct.targetGroup = cardUseEvent.tos
aimStruct.nullifiedTargets = cardUseEvent.nullifiedTargets or {}
@@ -820,74 +967,33 @@ end
function Room:useCard(cardUseEvent)
local from = cardUseEvent.from
self:moveCards({
- ids = { cardUseEvent.cardId },
+ ids = self:getSubcardsByRule(cardUseEvent.card),
from = from,
toArea = Card.Processing,
moveReason = fk.ReasonUse,
})
- if Fk:getCardById(cardUseEvent.cardId).skill then
- Fk:getCardById(cardUseEvent.cardId).skill:onUse(self, cardUseEvent)
+ if cardUseEvent.card.skill then
+ cardUseEvent.card.skill:onUse(self, cardUseEvent)
end
- self:setEmotion(self:getPlayerById(from), Fk:getCardById(cardUseEvent.cardId).name)
- self:doAnimate("Indicate", {
- from = from,
- to = cardUseEvent.tos or {},
- })
- if cardUseEvent.tos and #cardUseEvent.tos > 0 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
+ sendCardEmotionAndLog(self, cardUseEvent)
if self.logic:trigger(fk.PreCardUse, self:getPlayerById(cardUseEvent.from), cardUseEvent) then
return false
end
if not cardUseEvent.extraUse then
- self:getPlayerById(cardUseEvent.from):addCardUseHistory(Fk:getCardById(cardUseEvent.cardId).trueName, 1)
+ self:getPlayerById(cardUseEvent.from):addCardUseHistory(cardUseEvent.card.trueName, 1)
end
if cardUseEvent.responseToEvent then
- cardUseEvent.responseToEvent.cardIdsResponded = cardUseEvent.responseToEvent.cardIdsResponded or {}
- table.insert(cardUseEvent.responseToEvent.cardIdsResponded, cardUseEvent.cardId)
+ cardUseEvent.responseToEvent.cardsResponded = cardUseEvent.responseToEvent.cardsResponded or {}
+ table.insert(cardUseEvent.responseToEvent.cardsResponded, cardUseEvent.card)
end
for _, event in ipairs({ fk.AfterCardUseDeclared, fk.AfterCardTargetDeclared, fk.BeforeCardUseEffect, fk.CardUsing }) do
- if not cardUseEvent.toCardId and #TargetGroup:getRealTargets(cardUseEvent.tos) == 0 then
+ if not cardUseEvent.toCard and #TargetGroup:getRealTargets(cardUseEvent.tos) == 0 then
break
end
@@ -899,20 +1005,21 @@ function Room:useCard(cardUseEvent)
break
end
- if Fk:getCardById(cardUseEvent.cardId).type == Card.TypeEquip then
- if self:getCardArea(cardUseEvent.cardId) ~= Card.Processing then
+ local realCardIds = self:getSubcardsByRule(cardUseEvent.card, { Card.Processing })
+ if cardUseEvent.card.type == Card.TypeEquip then
+ if #realCardIds == 0 then
break
end
if self:getPlayerById(TargetGroup:getRealTargets(cardUseEvent.tos)[1]).dead then
self.moveCards({
- ids = { cardUseEvent.cardId },
+ ids = realCardIds,
toArea = Card.DiscardPile,
moveReason = fk.ReasonPutIntoDiscardPile,
})
else
local target = TargetGroup:getRealTargets(cardUseEvent.tos)[1]
- local existingEquipId = self:getPlayerById(target):getEquipment(Fk:getCardById(cardUseEvent.cardId).sub_type)
+ local existingEquipId = self:getPlayerById(target):getEquipment(cardUseEvent.card.sub_type)
if existingEquipId then
self:moveCards(
{
@@ -922,7 +1029,7 @@ function Room:useCard(cardUseEvent)
moveReason = fk.ReasonPutIntoDiscardPile,
},
{
- ids = { cardUseEvent.cardId },
+ ids = realCardIds,
to = target,
toArea = Card.PlayerEquip,
moveReason = fk.ReasonUse,
@@ -930,7 +1037,7 @@ function Room:useCard(cardUseEvent)
)
else
self:moveCards({
- ids = { cardUseEvent.cardId },
+ ids = realCardIds,
to = target,
toArea = Card.PlayerEquip,
moveReason = fk.ReasonUse,
@@ -939,8 +1046,8 @@ function Room:useCard(cardUseEvent)
end
break
- elseif Fk:getCardById(cardUseEvent.cardId).sub_type == Card.SubtypeDelayedTrick then
- if self:getCardArea(cardUseEvent.cardId) ~= Card.Processing then
+ elseif cardUseEvent.card.sub_type == Card.SubtypeDelayedTrick then
+ if #realCardIds == 0 then
break
end
@@ -948,14 +1055,14 @@ function Room:useCard(cardUseEvent)
if not self:getPlayerById(target).dead then
local findSameCard = false
for _, cardId in ipairs(self:getPlayerById(target):getCardIds(Player.Judge)) do
- if Fk:getCardById(cardId).trueName == Fk:getCardById(cardUseEvent.cardId) then
+ if Fk:getCardById(cardId).trueName == cardUseEvent.card.trueName then
findSameCard = true
end
end
if not findSameCard then
self:moveCards({
- ids = { cardUseEvent.cardId },
+ ids = realCardIds,
to = target,
toArea = Card.PlayerJudge,
moveReason = fk.ReasonUse,
@@ -966,7 +1073,7 @@ function Room:useCard(cardUseEvent)
end
self:moveCards({
- ids = { cardUseEvent.cardId },
+ ids = realCardIds,
toArea = Card.DiscardPile,
moveReason = fk.ReasonPutIntoDiscardPile,
})
@@ -974,13 +1081,13 @@ function Room:useCard(cardUseEvent)
break
end
- if Fk:getCardById(cardUseEvent.cardId).skill then
+ if cardUseEvent.card.skill then
---@type CardEffectEvent
local cardEffectEvent = {
from = cardUseEvent.from,
tos = cardUseEvent.tos,
- cardId = cardUseEvent.cardId,
- toCardId = cardUseEvent.toCardId,
+ card = cardUseEvent.card,
+ toCard = cardUseEvent.toCard,
responseToEvent = cardUseEvent.responseToEvent,
nullifiedTargets = cardUseEvent.nullifiedTargets,
disresponsiveList = cardUseEvent.disresponsiveList,
@@ -989,7 +1096,7 @@ function Room:useCard(cardUseEvent)
cardIdsResponded = cardUseEvent.nullifiedTargets,
}
- if cardUseEvent.toCardId ~= nil then
+ if cardUseEvent.toCard ~= nil then
self:doCardEffect(cardEffectEvent)
else
local collaboratorsIndex = {}
@@ -1034,9 +1141,11 @@ function Room:useCard(cardUseEvent)
end
self.logic:trigger(fk.CardUseFinished, self:getPlayerById(cardUseEvent.from), cardUseEvent)
- if self:getCardArea(cardUseEvent.cardId) == Card.Processing then
+
+ local leftRealCardIds = self:getSubcardsByRule(cardUseEvent.card, { Card.Processing })
+ if #leftRealCardIds > 0 then
self:moveCards({
- ids = { cardUseEvent.cardId },
+ ids = leftRealCardIds,
toArea = Card.DiscardPile,
moveReason = fk.ReasonPutIntoDiscardPile,
})
@@ -1053,7 +1162,7 @@ function Room:doCardEffect(cardEffectEvent)
break
end
- if not cardEffectEvent.toCardId and (not (self:getPlayerById(cardEffectEvent.to):isAlive() and cardEffectEvent.to) or #self:deadPlayerFilter(TargetGroup:getRealTargets(cardEffectEvent.tos)) == 0) then
+ if not cardEffectEvent.toCard and (not (self:getPlayerById(cardEffectEvent.to):isAlive() and cardEffectEvent.to) or #self:deadPlayerFilter(TargetGroup:getRealTargets(cardEffectEvent.tos)) == 0) then
break
end
@@ -1066,7 +1175,7 @@ function Room:doCardEffect(cardEffectEvent)
end
if event == fk.PreCardEffect then
- if Fk:getCardById(cardEffectEvent.cardId).name == 'slash' and
+ if cardEffectEvent.card.name == 'slash' and
not (
cardEffectEvent.disresponsive or
cardEffectEvent.unoffsetable or
@@ -1076,11 +1185,11 @@ function Room:doCardEffect(cardEffectEvent)
local to = self:getPlayerById(cardEffectEvent.to)
local use = self:askForUseCard(to, "jink")
if use then
- use.toCardId = cardEffectEvent.cardId
+ use.toCard = cardEffectEvent.card
use.responseToEvent = cardEffectEvent
self:useCard(use)
end
- elseif Fk:getCardById(cardEffectEvent.cardId).type == Card.TypeTrick then
+ elseif cardEffectEvent.card.type == Card.TypeTrick then
local players = {}
for _, p in ipairs(self.alive_players) do
local cards = p.player_cards[Player.Hand]
@@ -1094,7 +1203,7 @@ function Room:doCardEffect(cardEffectEvent)
local use = self:askForNullification(players)
if use then
- use.toCardId = cardEffectEvent.cardId
+ use.toCard = cardEffectEvent.card
use.responseToEvent = cardEffectEvent
self:useCard(use)
end
@@ -1102,9 +1211,8 @@ function Room:doCardEffect(cardEffectEvent)
end
if event == fk.CardEffecting then
- local cardEffecting = Fk:getCardById(cardEffectEvent.cardId)
- if cardEffecting.skill then
- cardEffecting.skill:onEffect(self, cardEffectEvent)
+ if cardEffectEvent.card.skill then
+ cardEffectEvent.card.skill:onEffect(self, cardEffectEvent)
end
end
end
@@ -1113,22 +1221,48 @@ end
---@param cardResponseEvent CardResponseEvent
function Room:responseCard(cardResponseEvent)
local from = cardResponseEvent.customFrom or cardResponseEvent.from
+ local card = cardResponseEvent.card
+ local cardIds = self:getSubcardsByRule(card)
+
+ if card:isVirtual() then
+ if #cardIds == 0 then
+ self:sendLog{
+ type = "#ResponsePlayV0Card",
+ from = from,
+ arg = card:toLogString(),
+ }
+ else
+ self:sendLog{
+ type = "#ResponsePlayVCard",
+ from = from,
+ card = cardIds,
+ arg = card:toLogString(),
+ }
+ end
+ else
+ self:sendLog{
+ type = "#ResponsePlayCard",
+ from = from,
+ card = cardIds,
+ }
+ end
self:moveCards({
- ids = { cardResponseEvent.cardId },
+ ids = cardIds,
from = from,
toArea = Card.Processing,
moveReason = fk.ReasonResonpse,
})
- self:setEmotion(self:getPlayerById(from), Fk:getCardById(cardResponseEvent.cardId).name)
+ self:setEmotion(self:getPlayerById(from), card.name)
for _, event in ipairs({ fk.PreCardRespond, fk.CardResponding, fk.CardRespondFinished }) do
self.logic:trigger(event, self:getPlayerById(cardResponseEvent.from), cardResponseEvent)
end
- if self:getCardArea(cardResponseEvent.cardId) == Card.Processing or cardResponseEvent.skipDrop then
+ local realCardIds = self:getSubcardsByRule(cardResponseEvent.card, { Card.Processing })
+ if #realCardIds > 0 and not cardResponseEvent.skipDrop then
self:moveCards({
- ids = { cardResponseEvent.cardId },
+ ids = realCardIds,
toArea = Card.DiscardPile,
moveReason = fk.ReasonPutIntoDiscardPile,
})
@@ -1780,6 +1914,25 @@ function Room:gameOver(winner)
coroutine.yield()
end
+---@param card Card
+---@param fromAreas CardArea[]|null
+---@return integer[]
+function Room:getSubcardsByRule(card, fromAreas)
+ if card:isVirtual() and #card.subcards == 0 then
+ return {}
+ end
+
+ local cardIds = {}
+ fromAreas = fromAreas or {}
+ for _, cardId in ipairs(card:isVirtual() and card.subcards or { card.id }) do
+ if #fromAreas == 0 or table.contains(fromAreas, self:getCardArea(cardId)) then
+ table.insert(cardIds, cardId)
+ end
+ end
+
+ return cardIds
+end
+
function CreateRoom(_room)
RoomInstance = Room:new(_room)
end
diff --git a/lua/server/system_enum.lua b/lua/server/system_enum.lua
index cd0a06c9..0a95196c 100644
--- a/lua/server/system_enum.lua
+++ b/lua/server/system_enum.lua
@@ -10,13 +10,13 @@
---@alias DyingStruct { who: integer, damage: DamageStruct }
---@alias DeathStruct { who: integer, damage: DamageStruct }
----@alias CardUseStruct { from: integer, tos: TargetGroup, cardId: integer, toCardId: integer|null, responseToEvent: CardUseStruct|null, nullifiedTargets: interger[]|null, extraUse: boolean|null, disresponsiveList: integer[]|null, unoffsetableList: integer[]|null, addtionalDamage: integer|null, customFrom: integer|null, cardIdsResponded: integer[]|null }
----@alias AimStruct { from: integer, cardId: integer, tos: AimGroup, to: integer, subTargets: integer[]|null, targetGroup: TargetGroup|null, nullifiedTargets: integer[]|null, firstTarget: boolean, additionalDamage: integer|null, disresponsive: boolean|null, unoffsetableList: boolean|null }
----@alias CardEffectEvent { from: integer, to: integer, subTargets: integer[]|null, tos: TargetGroup, cardId: integer, toCardId: integer|null, responseToEvent: CardUseStruct|null, nullifiedTargets: interger[]|null, extraUse: boolean|null, disresponsiveList: integer[]|null, unoffsetableList: integer[]|null, addtionalDamage: integer|null, customFrom: integer|null, cardIdsResponded: integer[]|null, disresponsive: boolean|null, unoffsetable: boolean|null }
+---@alias CardUseStruct { from: integer, tos: TargetGroup, card: Card, toCard: Card|null, responseToEvent: CardUseStruct|null, nullifiedTargets: interger[]|null, extraUse: boolean|null, disresponsiveList: integer[]|null, unoffsetableList: integer[]|null, addtionalDamage: integer|null, customFrom: integer|null, cardsResponded: Card[]|null }
+---@alias AimStruct { from: integer, card: Card, tos: AimGroup, to: integer, subTargets: integer[]|null, targetGroup: TargetGroup|null, nullifiedTargets: integer[]|null, firstTarget: boolean, additionalDamage: integer|null, disresponsive: boolean|null, unoffsetableList: boolean|null }
+---@alias CardEffectEvent { from: integer, to: integer, subTargets: integer[]|null, tos: TargetGroup, card: Card, toCard: Card|null, responseToEvent: CardUseStruct|null, nullifiedTargets: interger[]|null, extraUse: boolean|null, disresponsiveList: integer[]|null, unoffsetableList: integer[]|null, addtionalDamage: integer|null, customFrom: integer|null, cardsResponded: Card[]|null, disresponsive: boolean|null, unoffsetable: boolean|null }
---@alias SkillEffectEvent { from: integer, tos: integer[], cards: integer[] }
---@alias JudgeStruct { who: ServerPlayer, card: Card, reason: string }
----@alias CardResponseEvent { from: integer, cardId: integer, responseToEvent: CardEffectEvent|null, skipDrop: boolean|null, customFrom: integer|null }
+---@alias CardResponseEvent { from: integer, card: Card, responseToEvent: CardEffectEvent|null, skipDrop: boolean|null, customFrom: integer|null }
---@alias CardMoveReason integer
diff --git a/packages/standard/game_rule.lua b/packages/standard/game_rule.lua
index 49bee7fc..9d26a5af 100644
--- a/packages/standard/game_rule.lua
+++ b/packages/standard/game_rule.lua
@@ -125,7 +125,7 @@ GameRule = fk.CreateTriggerSkill{
---@type CardEffectEvent
local effect_data = {
- cardId = cards[i],
+ card = card,
to = player.id,
tos = { {player.id} },
}
@@ -144,26 +144,8 @@ GameRule = fk.CreateTriggerSkill{
local result = room:doRequest(player, "PlayCard", player.id)
if result == "" then break end
- local data = json.decode(result)
- local card = data.card
- local targets = data.targets
- if type(card) == "string" then
- local card_data = json.decode(card)
- local skill = Fk.skills[card_data.skill]
- local selected_cards = card_data.subcards
- skill:onEffect(room, {
- from = player.id,
- cards = selected_cards,
- tos = targets,
- })
- else
- local use = {} ---@type CardUseStruct
- use.from = player.id
- use.tos = {}
- for _, target in ipairs(targets) do
- table.insert(use.tos, { target })
- end
- use.cardId = card
+ local use = room:handleUseCardReply(player, result)
+ if use then
room:useCard(use)
end
end
diff --git a/packages/standard/init.lua b/packages/standard/init.lua
index 3de6a6a2..9df6a710 100644
--- a/packages/standard/init.lua
+++ b/packages/standard/init.lua
@@ -11,6 +11,12 @@ Fk:loadTranslationTable{
["qun"] = "群",
}
+Fk:loadTranslationTable{
+ ["black"] = "黑色",
+ ["red"] = '红色',
+ ["nocolor"] = '无色',
+}
+
local caocao = General:new(extension, "caocao", "wei", 4)
Fk:loadTranslationTable{
["caocao"] = "曹操",
@@ -74,9 +80,27 @@ Fk:loadTranslationTable{
["liubei"] = "刘备",
}
+local wusheng = fk.CreateViewAsSkill{
+ name = "wusheng",
+ pattern = "slash",
+ card_filter = function(self, to_select, selected)
+ if #selected == 1 then return false end
+ return Fk:getCardById(to_select).color == Card.Red
+ end,
+ view_as = function(self, cards)
+ if #cards ~= 1 then
+ return nil
+ end
+ local c = Fk:cloneCard("slash")
+ c:addSubcard(cards[1])
+ return c
+ end,
+}
local guanyu = General:new(extension, "guanyu", "shu", 4)
+guanyu:addSkill(wusheng)
Fk:loadTranslationTable{
["guanyu"] = "关羽",
+ ["wusheng"] = "武圣",
}
local zhangfei = General:new(extension, "zhangfei", "shu", 4)
diff --git a/packages/standard_cards/init.lua b/packages/standard_cards/init.lua
index 341ebf9d..ee45383a 100644
--- a/packages/standard_cards/init.lua
+++ b/packages/standard_cards/init.lua
@@ -195,6 +195,7 @@ local dismantlement = fk.CreateTrickCard{
}
Fk:loadTranslationTable{
["dismantlement"] = "过河拆桥",
+ ["dismantlement_skill"] = "过河拆桥",
}
extension:addCards({
@@ -241,6 +242,7 @@ local snatch = fk.CreateTrickCard{
}
Fk:loadTranslationTable{
["snatch"] = "顺手牵羊",
+ ["snatch_skill"] = "顺手牵羊",
}
extension:addCards({
@@ -275,11 +277,11 @@ local duelSkill = fk.CreateActiveSkill{
break
end
- local cardIdResponded = room:askForResponse(currentResponser, 'slash')
- if cardIdResponded then
+ local cardResponded = room:askForResponse(currentResponser, 'slash')
+ if cardResponded then
room:responseCard({
from = currentResponser.id,
- cardId = cardIdResponded,
+ card = cardResponded,
responseToEvent = effect,
})
else
@@ -336,17 +338,13 @@ local collateralSkill = fk.CreateActiveSkill{
cardUseEvent.tos = { { cardUseEvent.tos[1][1], cardUseEvent.tos[2][1] } }
end,
on_effect = function(self, room, effect)
- local cardIdResponded = nil
- if not (effect.disresponsive or table.contains(effect.disresponsiveList or {}, effect.to)) then
- cardIdResponded = room:askForResponse(room:getPlayerById(effect.to), 'slash')
- end
+ local use = room:askForUseCard(
+ room:getPlayerById(effect.to),
+ "slash", nil, nil, nil, { must_targets = effect.subTargets }
+ )
- if cardIdResponded then
- room:useCard({
- from = effect.to,
- tos = { { effect.subTargets[1] } },
- cardId = cardIdResponded,
- })
+ if use then
+ room:useCard(use)
else
room:obtainCard(effect.from, room:getPlayerById(effect.to):getEquipment(Card.SubtypeWeapon), true, fk.ReasonGive)
end
@@ -436,15 +434,15 @@ local savageAssaultSkill = fk.CreateActiveSkill{
end
end,
on_effect = function(self, room, effect)
- local cardIdResponded = nil
+ local cardResponded = nil
if not (effect.disresponsive or table.contains(effect.disresponsiveList or {}, effect.to)) then
- cardIdResponded = room:askForResponse(room:getPlayerById(effect.to), 'slash')
+ cardResponded = room:askForResponse(room:getPlayerById(effect.to), 'slash')
end
- if cardIdResponded then
+ if cardResponded then
room:responseCard({
from = effect.to,
- cardId = cardIdResponded,
+ card = cardResponded,
responseToEvent = effect,
})
else
@@ -485,15 +483,15 @@ local archeryAttackSkill = fk.CreateActiveSkill{
end
end,
on_effect = function(self, room, effect)
- local cardIdResponded = nil
+ local cardResponded = nil
if not (effect.disresponsive or table.contains(effect.disresponsiveList or {}, effect.to)) then
- cardIdResponded = room:askForResponse(room:getPlayerById(effect.to), 'jink')
+ cardResponded = room:askForResponse(room:getPlayerById(effect.to), 'jink')
end
- if cardIdResponded then
+ if cardResponded then
room:responseCard({
from = effect.to,
- cardId = cardIdResponded,
+ card = cardResponded,
responseToEvent = effect,
})
else
@@ -628,7 +626,7 @@ local lightningSkill = fk.CreateActiveSkill{
local to = room:getPlayerById(effect.to)
local nextp = to:getNextAlive()
room:moveCards{
- ids = { effect.cardId },
+ ids = room:getSubcardsByRule(effect.card, { Card.Processing }),
to = nextp.id,
toArea = Card.PlayerJudge,
moveReason = fk.ReasonPut
@@ -686,7 +684,7 @@ local indulgenceSkill = fk.CreateActiveSkill{
end,
on_nullified = function(self, room, effect)
room:moveCards{
- ids = { effect.cardId },
+ ids = room:getSubcardsByRule(effect.card, { Card.Processing }),
toArea = Card.DiscardPile,
moveReason = fk.ReasonPutIntoDiscardPile
}
diff --git a/qml/Pages/Room.qml b/qml/Pages/Room.qml
index a8bae4ce..57cd5d88 100644
--- a/qml/Pages/Room.qml
+++ b/qml/Pages/Room.qml
@@ -26,6 +26,7 @@ Item {
property var selected_targets: []
property string responding_card
property bool respond_play: false
+ property var extra_data: ({})
Image {
source: AppPath + "/image/gamebg"
diff --git a/qml/Pages/RoomElement/Dashboard.qml b/qml/Pages/RoomElement/Dashboard.qml
index 6f334875..62958d99 100644
--- a/qml/Pages/RoomElement/Dashboard.qml
+++ b/qml/Pages/RoomElement/Dashboard.qml
@@ -13,7 +13,6 @@ RowLayout {
property bool selected: selfPhoto.selected
- property bool is_pending: false
property string pending_skill: ""
property var pending_card
property var pendings: [] // int[], store cid
@@ -116,7 +115,7 @@ RowLayout {
if (cname) {
let ids = [], cards = handcardAreaItem.cards;
for (let i = 0; i < cards.length; i++) {
- if (cards[i].name === cname)
+ if (JSON.parse(Backend.callLuaFunction("CardFitPattern", [cards[i].name, cname])))
ids.push(cards[i].cid);
}
handcardAreaItem.enableCards(ids);
@@ -242,7 +241,10 @@ RowLayout {
function enableSkills(cname) {
if (cname) {
- // TODO: vs skill
+ for (let i = 0; i < skillButtons.count; i++) {
+ let item = skillButtons.itemAt(i);
+ item.enabled = JSON.parse(Backend.callLuaFunction("SkillFitPattern", [item.orig, cname]));
+ }
return;
}
for (let i = 0; i < skillButtons.count; i++) {
diff --git a/qml/Pages/RoomLogic.js b/qml/Pages/RoomLogic.js
index a1c8f3e4..f346f596 100644
--- a/qml/Pages/RoomLogic.js
+++ b/qml/Pages/RoomLogic.js
@@ -298,6 +298,14 @@ function enableTargets(card) { // card: int | { skill: string, subcards: int[] }
okButton.enabled = JSON.parse(Backend.callLuaFunction(
"CardFeasible", [card, selected_targets]
));
+ if (okButton.enabled) {
+ if (roomScene.extra_data instanceof Object) {
+ let must = roomScene.extra_data.must_targets;
+ if (must instanceof Array) {
+ okButton.enabled = (must.length === 0);
+ }
+ }
+ }
} else {
all_photos.forEach(photo => {
photo.state = "normal";
@@ -337,6 +345,16 @@ function updateSelectedTargets(playerid, selected) {
okButton.enabled = JSON.parse(Backend.callLuaFunction(
"CardFeasible", [card, selected_targets]
));
+ if (okButton.enabled) {
+ if (roomScene.extra_data instanceof Object) {
+ let must = roomScene.extra_data.must_targets;
+ if (must instanceof Array) {
+ okButton.enabled = (must.filter((val) => {
+ return selected_targets.indexOf(val) === -1;
+ }).length === 0);
+ }
+ }
+ }
} else {
all_photos.forEach(photo => {
photo.state = "normal";
@@ -628,14 +646,19 @@ callbacks["GameLog"] = function(jsonData) {
}
callbacks["AskForUseCard"] = function(jsonData) {
- // jsonData: card, prompt, cancelable, {}
+ // jsonData: card, pattern, prompt, cancelable, {}
let data = JSON.parse(jsonData);
let cardname = data[0];
- let prompt = data[1];
+ let pattern = data[1];
+ let prompt = data[2];
+ let extra_data = data[4];
+ if (extra_data != null) {
+ roomScene.extra_data = extra_data;
+ }
roomScene.promptText = Backend.translate(prompt)
.arg(Backend.translate(cardname));
- roomScene.responding_card = cardname;
+ roomScene.responding_card = pattern;
roomScene.respond_play = false;
roomScene.state = "responding";
okButton.enabled = false;
@@ -643,14 +666,15 @@ callbacks["AskForUseCard"] = function(jsonData) {
}
callbacks["AskForResponseCard"] = function(jsonData) {
- // jsonData: card, prompt, cancelable, {}
+ // jsonData: card_name, pattern, prompt, cancelable, {}
let data = JSON.parse(jsonData);
let cardname = data[0];
- let prompt = data[1];
+ let pattern = data[1];
+ let prompt = data[2];
roomScene.promptText = Backend.translate(prompt)
.arg(Backend.translate(cardname));
- roomScene.responding_card = cardname;
+ roomScene.responding_card = pattern;
roomScene.respond_play = true;
roomScene.state = "responding";
okButton.enabled = false;