Changelog: v0.4.16

This commit is contained in:
notify 2024-06-10 15:19:47 +08:00
parent 5090bdba77
commit 524a028ce3
48 changed files with 1276 additions and 687 deletions

View File

@ -85,8 +85,10 @@ jobs:
cd assets/res cd assets/res
cp -r /etc/ssl/certs . cp -r /etc/ssl/certs .
cp /usr/share/ca-certificates/mozilla/* certs/ cp /usr/share/ca-certificates/mozilla/* certs/
echo ${FKVER%)} > fk_ver cd ../..
./genfkver.sh echo ${FKVER%)} > ../fk_ver
../genfkver.sh
cp ../fk_ver assets/res
- name: Configure CMake Project - name: Configure CMake Project
working-directory: ${{github.workspace}} working-directory: ${{github.workspace}}

View File

@ -1,5 +1,36 @@
# ChangeLog # ChangeLog
## v0.4.16
在引入freekill-core之后的第一次版本更新甚至无法保证这次更新是否正常
1. 改进processPrompt支持双将和暗将
2. 副将长名旋转
3. 国战体力上限优化,包括一览和选将框
4. 空格添加结束出牌阶段Escape键呼出菜单
5. 武将一览左栏文本换行
6. 同名替换影响已选择的武将
7. 再次排序手牌时按照点数排序
8. Logic.js翻译
9. 进入房间翻译删去句号跟房间内其他toast风格统一
10. 常见疑问最后一张“下一条”改为“OK!”
11. 录像回放“从文件打开”翻译
12. interaction自动弹出和关闭comboBox补技能名
13. 卡牌音效添加装备效果音效和使用音效,小小重构
14. activeSkill的prompt的selected_targets实装
15. 禁用扩展包文本ui限制长度
16. 右键技能呼出气泡
17. 搬运了ArrangeCards。
18. 优化了GuanxingBox的操作
19. 修复了不能及时更新技能prompt的bug
20. 取消目标后会刷新目标选择
21. 完备了借刀的牌名
22. CPP代码进行大的重构配有少量文档
23. (底层)调度机制大改
24. 大厅UI开始调整但是仍未完工
___
## v0.4.13 & 14 & 15 ## v0.4.13 & 14 & 15
- 优化重连逻辑 - 优化重连逻辑

View File

@ -6,7 +6,7 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(FreeKill VERSION 0.4.15) project(FreeKill VERSION 0.4.16)
add_definitions(-DFK_VERSION=\"${CMAKE_PROJECT_VERSION}\") add_definitions(-DFK_VERSION=\"${CMAKE_PROJECT_VERSION}\")
find_package(Qt6 REQUIRED COMPONENTS find_package(Qt6 REQUIRED COMPONENTS

View File

@ -3,8 +3,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.notify.FreeKill" package="org.notify.FreeKill"
android:installLocation="preferExternal" android:installLocation="preferExternal"
android:versionCode="415" android:versionCode="416"
android:versionName="0.4.15"> android:versionName="0.4.16">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

View File

@ -9,6 +9,7 @@
---@field public discard_pile integer[] @ 弃牌堆 ---@field public discard_pile integer[] @ 弃牌堆
---@field public observing boolean ---@field public observing boolean
---@field public record any ---@field public record any
---@field public last_update_ui integer @ 上次刷新状态技UI的时间
Client = AbstractRoom:subclass('Client') Client = AbstractRoom:subclass('Client')
-- load client classes -- load client classes
@ -65,6 +66,23 @@ function Client:initialize()
else else
self:notifyUI(command, data) self:notifyUI(command, data)
end end
if self.recording and command == "GameLog" then
--and os.getms() - self.last_update_ui > 60000 then
-- self.last_update_ui = os.getms()
-- TODO: create a function
-- 刷所有人手牌上限
for _, p in ipairs(self.alive_players) do
self:notifyUI("MaxCard", {
pcardMax = p:getMaxCards(),
id = p.id,
})
end
-- 刷自己的手牌
for _, cid in ipairs(Self:getCardIds("h")) do
self:notifyUI("UpdateCard", cid)
end
end
end end
self.discard_pile = {} self.discard_pile = {}
@ -72,6 +90,7 @@ function Client:initialize()
self.disabled_packs = {} self.disabled_packs = {}
self.disabled_generals = {} self.disabled_generals = {}
-- self.last_update_ui = os.getms()
self.recording = false self.recording = false
end end
@ -114,10 +133,6 @@ function Client:moveCards(moves)
for _, move in ipairs(moves) do for _, move in ipairs(moves) do
if move.from and move.fromArea then if move.from and move.fromArea then
local from = self:getPlayerById(move.from) local from = self:getPlayerById(move.from)
self:notifyUI("MaxCard", {
pcardMax = from:getMaxCards(),
id = move.from,
})
if move.fromArea == Card.PlayerHand and not Self:isBuddy(self:getPlayerById(move.from)) then if move.fromArea == Card.PlayerHand and not Self:isBuddy(self:getPlayerById(move.from)) then
for _ = 1, #move.ids do for _ = 1, #move.ids do
table.remove(from.player_cards[Player.Hand]) table.remove(from.player_cards[Player.Hand])
@ -133,13 +148,11 @@ function Client:moveCards(moves)
if move.to and move.toArea then if move.to and move.toArea then
local ids = move.ids local ids = move.ids
self:notifyUI("MaxCard", { if (move.toArea == Card.PlayerHand and not Self:isBuddy(self:getPlayerById(move.to))) or
pcardMax = self:getPlayerById(move.to):getMaxCards(), (move.toArea == Card.PlayerSpecial and not move.moveVisible) then
id = move.to, ids = {-1}
})
if (not Self:isBuddy(self:getPlayerById(move.to)) and move.toArea == Card.PlayerHand) or table.contains(ids, -1) then
ids = table.map(ids, function() return -1 end)
end end
self:getPlayerById(move.to):addCards(move.toArea, ids, move.specialName) self:getPlayerById(move.to):addCards(move.toArea, ids, move.specialName)
elseif move.toArea == Card.DiscardPile then elseif move.toArea == Card.DiscardPile then
table.insert(self.discard_pile, move.ids[1]) table.insert(self.discard_pile, move.ids[1])
@ -340,6 +353,7 @@ fk.client_callback["AddObserver"] = function(data)
} }
local p = ClientPlayer:new(player) local p = ClientPlayer:new(player)
table.insert(ClientInstance.observers, p) table.insert(ClientInstance.observers, p)
-- ClientInstance:notifyUI("ServerMessage", string.format(Fk:translate("$AddObserver"), name))
end end
fk.client_callback["RemoveObserver"] = function(data) fk.client_callback["RemoveObserver"] = function(data)
@ -347,6 +361,7 @@ fk.client_callback["RemoveObserver"] = function(data)
for _, p in ipairs(ClientInstance.observers) do for _, p in ipairs(ClientInstance.observers) do
if p.player:getId() == id then if p.player:getId() == id then
table.removeOne(ClientInstance.observers, p) table.removeOne(ClientInstance.observers, p)
-- ClientInstance:notifyUI("ServerMessage", string.format(Fk:translate("$RemoveObserver"), p.player:getScreenName()))
break break
end end
end end
@ -387,10 +402,6 @@ fk.client_callback["PropertyUpdate"] = function(data)
end end
ClientInstance:notifyUI("PropertyUpdate", data) ClientInstance:notifyUI("PropertyUpdate", data)
ClientInstance:notifyUI("MaxCard", {
pcardMax = ClientInstance:getPlayerById(id):getMaxCards(),
id = id,
})
end end
fk.client_callback["AskForCardChosen"] = function(data) fk.client_callback["AskForCardChosen"] = function(data)
@ -474,7 +485,38 @@ end
---@param moves CardsMoveStruct[] ---@param moves CardsMoveStruct[]
local function separateMoves(moves) local function separateMoves(moves)
local ret = {} ---@type CardsMoveInfo[] local ret = {} ---@type CardsMoveInfo[]
local function containArea(area, relevant, defaultVisible) --处理区的处理?
local areas = relevant
and {Card.PlayerEquip, Card.PlayerJudge, Card.PlayerHand}
or {Card.PlayerEquip, Card.PlayerJudge}
return table.contains(areas, area) or (defaultVisible and table.contains({Card.Processing, Card.DiscardPile}, area))
end
for _, move in ipairs(moves) do for _, move in ipairs(moves) do
local singleVisible = move.moveVisible
if not singleVisible then
if move.visiblePlayers then
local visiblePlayers = move.visiblePlayers
if type(visiblePlayers) == "number" then
if Self:isBuddy(visiblePlayers) then
singleVisible = true
end
elseif type(visiblePlayers) == "table" then
if table.find(visiblePlayers, function(pid) return Self:isBuddy(pid) end) then
singleVisible = true
end
end
else
if move.to and move.toArea == Card.PlayerSpecial and Self:isBuddy(move.to) then
singleVisible = true
end
end
end
if not singleVisible then
singleVisible = containArea(move.toArea, move.to and Self:isBuddy(move.to), move.moveVisible == nil)
end
for _, info in ipairs(move.moveInfo) do for _, info in ipairs(move.moveInfo) do
table.insert(ret, { table.insert(ret, {
ids = {info.cardId}, ids = {info.cardId},
@ -486,6 +528,7 @@ local function separateMoves(moves)
specialName = move.specialName, specialName = move.specialName,
fromSpecialName = info.fromSpecialName, fromSpecialName = info.fromSpecialName,
proposer = move.proposer, proposer = move.proposer,
moveVisible = singleVisible or containArea(info.fromArea, move.from and Self:isBuddy(move.from), move.moveVisible == nil)
}) })
end end
end end
@ -513,7 +556,7 @@ local function mergeMoves(moves)
proposer = move.proposer, proposer = move.proposer,
} }
end end
table.insert(temp[info].ids, move.ids[1]) table.insert(temp[info].ids, move.moveVisible and move.ids[1] or -1)
end end
for _, v in pairs(temp) do for _, v in pairs(temp) do
table.insert(ret, v) table.insert(ret, v)
@ -609,8 +652,27 @@ local function sendMoveCardLog(move)
from = move.from, from = move.from,
card = move.ids, card = move.ids,
} }
-- elseif move.toArea == Card.Processing then elseif move.toArea == Card.Processing then
-- nop if move.fromArea == Card.DrawPile and (move.moveReason == fk.ReasonPut or move.moveReason == fk.ReasonJustMove) then
if hidden then
client:appendLog{
type = "$ViewCardFromDrawPile",
from = move.proposer,
arg = #move.ids,
}
else
client:appendLog{
type = "$TurnOverCardFromDrawPile",
from = move.proposer,
card = move.ids,
arg = #move.ids,
}
client:setCardNote(move.ids, {
type = "$$TurnOverCard",
from = move.proposer,
})
end
end
elseif move.from and move.toArea == Card.DrawPile then elseif move.from and move.toArea == Card.DrawPile then
msgtype = hidden and "$PutCard" or "$PutKnownCard" msgtype = hidden and "$PutCard" or "$PutKnownCard"
client:appendLog{ client:appendLog{
@ -1029,7 +1091,7 @@ end
fk.client_callback["EnterLobby"] = function(jsonData) fk.client_callback["EnterLobby"] = function(jsonData)
local c = ClientInstance local c = ClientInstance
--[[ ---[[
if c.recording and not c.observing then if c.recording and not c.observing then
c.recording = false c.recording = false
c.record[2] = table.concat({ c.record[2] = table.concat({
@ -1120,7 +1182,7 @@ local function loadPlayerSummary(pdata)
to = id, to = id,
toArea = Card.PlayerSpecial, toArea = Card.PlayerSpecial,
specialName = k, specialName = k,
specialVisible = Self.id == id, moveVisible = true,
} }
table.insert(card_moves, move) table.insert(card_moves, move)
end end

View File

@ -16,6 +16,8 @@ function GetGeneralData(name)
subkingdom = general.subkingdom, subkingdom = general.subkingdom,
hp = general.hp, hp = general.hp,
maxHp = general.maxHp, maxHp = general.maxHp,
mainMaxHpAdjustedValue = general.mainMaxHpAdjustedValue,
deputyMaxHpAdjustedValue = general.deputyMaxHpAdjustedValue,
shield = general.shield, shield = general.shield,
hidden = general.hidden, hidden = general.hidden,
total_hidden = general.total_hidden, total_hidden = general.total_hidden,
@ -31,6 +33,8 @@ function GetGeneralDetail(name)
kingdom = general.kingdom, kingdom = general.kingdom,
hp = general.hp, hp = general.hp,
maxHp = general.maxHp, maxHp = general.maxHp,
mainMaxHp = general.mainMaxHpAdjustedValue,
deputyMaxHp = general.deputyMaxHpAdjustedValue,
gender = general.gender, gender = general.gender,
skill = {}, skill = {},
related_skill = {}, related_skill = {},
@ -382,6 +386,22 @@ function CardFeasible(card, selected_targets)
return ret return ret
end end
---@param card string | integer
---@param selected_targets integer[] @ ids of selected players
function CardPrompt(card, selected_targets)
local c ---@type Card
local selected_cards
if type(card) == "number" then
c = Fk:getCardById(card)
selected_cards = {card}
else
local t = json.decode(card)
return ActiveSkillPrompt(t.skill, t.subcards, selected_targets)
end
return ActiveSkillPrompt(c.skill, selected_cards, selected_targets)
end
-- Handle skills -- Handle skills
function GetSkillData(skill_name) function GetSkillData(skill_name)
@ -621,6 +641,7 @@ end
function GetInteractionOfSkill(skill_name) function GetInteractionOfSkill(skill_name)
local skill = Fk.skills[skill_name] local skill = Fk.skills[skill_name]
if skill and skill.interaction then if skill and skill.interaction then
skill.interaction.data = nil
return skill:interaction() return skill:interaction()
end end
return nil return nil
@ -738,10 +759,9 @@ function GetCardProhibitReason(cid, method, pattern)
local skillName = s.name local skillName = s.name
local ret = Fk:translate(skillName) local ret = Fk:translate(skillName)
if ret ~= skillName then if ret ~= skillName then
-- TODO: translate return ret .. Fk:translate("prohibit") .. Fk:translate(method == "use" and "method_use" or "method_response_play")
return ret .. "" .. (method == "use" and "使用" or "打出")
elseif skillName:endsWith("_prohibit") and skillName:startsWith("#") then elseif skillName:endsWith("_prohibit") and skillName:startsWith("#") then
return Fk:translate(skillName:sub(2, -10)) .. "" .. (method == "use" and "使用" or "打出") return Fk:translate(skillName:sub(2, -10)) .. Fk:translate("prohibit") .. Fk:translate(method == "use" and "method_use" or "method_response_play")
else else
return ret return ret
end end

View File

@ -12,7 +12,10 @@ Fk:loadTranslationTable({
-- ["Old Password"] = "旧密码", -- ["Old Password"] = "旧密码",
-- ["New Password"] = "新密码", -- ["New Password"] = "新密码",
-- ["Update Avatar"] = "更新头像", -- ["Update Avatar"] = "更新头像",
-- ["Update avatar done."] = "头像已更新",
-- ["Update Password"] = "更新密码", -- ["Update Password"] = "更新密码",
-- ["Update password done."] = "密码已更新",
-- ["Old password wrong!"] = "旧密码错误!",
-- ["Lobby BG"] = "大厅壁纸", -- ["Lobby BG"] = "大厅壁纸",
-- ["Room BG"] = "房间背景", -- ["Room BG"] = "房间背景",
-- ["Game BGM"] = "游戏BGM", -- ["Game BGM"] = "游戏BGM",
@ -90,9 +93,14 @@ Fk:loadTranslationTable({
["Cards Overview"] = "Cards", ["Cards Overview"] = "Cards",
["Special card skills:"] = "<b>Special use method:</b>", ["Special card skills:"] = "<b>Special use method:</b>",
["Every suit & number:"] = "<b>All suit and number:</b>", ["Every suit & number:"] = "<b>All suit and number:</b>",
-- ["Male Audio"] = "男性音效",
-- ["Female Audio"] = "女性音效",
-- ["Equip Effect Audio"] = "效果音效",
-- ["Equip Use Audio"] = "使用音效",
["Scenarios Overview"] = "Game modes", ["Scenarios Overview"] = "Game modes",
-- ["Replay"] = "录像", -- ["Replay"] = "录像",
-- ["Replay Manager"] = "来欣赏潇洒的录像吧!", -- ["Replay Manager"] = "来欣赏潇洒的录像吧!",
-- ["Replay from File"] = "从文件打开",
["Game Win"] = "Win", ["Game Win"] = "Win",
["Game Lose"] = "Lose", ["Game Lose"] = "Lose",
["Play the Replay"] = "Play", ["Play the Replay"] = "Play",
@ -156,7 +164,7 @@ Fk:loadTranslationTable({
["IncludeDeputy"] = "<font color=\"red\">Deputy character enabled</font>", ["IncludeDeputy"] = "<font color=\"red\">Deputy character enabled</font>",
-- Room -- Room
["$EnterRoom"] = "Successfully entered the room.", ["$EnterRoom"] = "Successfully entered the room",
["#currentRoundNum"] = "Round #%1", ["#currentRoundNum"] = "Round #%1",
["$Choice"] = "%1: Please choose", ["$Choice"] = "%1: Please choose",
["$ChooseGeneral"] = "Please choose %1 character(s)", ["$ChooseGeneral"] = "Please choose %1 character(s)",
@ -347,7 +355,7 @@ Fk:loadTranslationTable({
-- get/lose skill -- get/lose skill
["#AcquireSkill"] = '%from acquired the skill "%arg"', ["#AcquireSkill"] = '%from acquired the skill "%arg"',
["#LoseSkill"] = '%from lost the skill "%arg"', ["#LoseSkill"] = '%from lost the skill "%arg"',
-- moveCards (they are sent by notifyMoveCards) -- moveCards (they are sent by notifyMoveCards)
["$PutCard"] = "%arg card(s) of %from were put into draw pile", ["$PutCard"] = "%arg card(s) of %from were put into draw pile",
@ -405,8 +413,8 @@ Fk:loadTranslationTable({
-- turnOver -- turnOver
["#TurnOver"] = "%from turned over character card, now his status is %arg", ["#TurnOver"] = "%from turned over character card, now his status is %arg",
["face_up"] = "face up", ["face_up"] = "face up",
["face_down"] = "face down", ["face_down"] = "face down",
-- damage, heal and lose HP -- damage, heal and lose HP
["#Damage"] = "%to dealt %arg %arg2 DMG to %from", ["#Damage"] = "%to dealt %arg %arg2 DMG to %from",
@ -443,4 +451,6 @@ Fk:loadTranslationTable({
["##ResponsePlayCard"] = "%from plays", ["##ResponsePlayCard"] = "%from plays",
["##ShowCard"] = "%from shows", ["##ShowCard"] = "%from shows",
["##JudgeCard"] = "%arg judge", ["##JudgeCard"] = "%arg judge",
["##PindianCard"] = "%from point fights",
["##RecastCard"] = "%from recasts",
}, "en_US") }, "en_US")

View File

@ -12,7 +12,10 @@ Fk:loadTranslationTable{
["Old Password"] = "旧密码", ["Old Password"] = "旧密码",
["New Password"] = "新密码", ["New Password"] = "新密码",
["Update Avatar"] = "更新头像", ["Update Avatar"] = "更新头像",
["Update avatar done."] = "头像已更新",
["Update Password"] = "更新密码", ["Update Password"] = "更新密码",
["Update password done."] = "密码已更新",
["Old password wrong!"] = "旧密码错误!",
["Lobby BG"] = "大厅壁纸", ["Lobby BG"] = "大厅壁纸",
["Room BG"] = "房间背景", ["Room BG"] = "房间背景",
["Game BGM"] = "游戏BGM", ["Game BGM"] = "游戏BGM",
@ -31,7 +34,7 @@ Fk:loadTranslationTable{
["Search"] = "搜索", ["Search"] = "搜索",
["Back"] = "返回", ["Back"] = "返回",
["Refresh Room List"] = "刷新房间列表", ["Refresh Room List"] = "刷新房间列表 (%1个房间)",
["Disable Extension"] = "禁用Lua拓展 (重启后生效)", ["Disable Extension"] = "禁用Lua拓展 (重启后生效)",
["Create Room"] = "创建房间", ["Create Room"] = "创建房间",
@ -100,9 +103,12 @@ Fk:loadTranslationTable{
["Every suit & number:"] = "<b>所有的花色和点数:</b>", ["Every suit & number:"] = "<b>所有的花色和点数:</b>",
["Male Audio"] = "男性音效", ["Male Audio"] = "男性音效",
["Female Audio"] = "女性音效", ["Female Audio"] = "女性音效",
["Equip Effect Audio"] = "效果音效",
["Equip Use Audio"] = "使用音效",
["Scenarios Overview"] = "玩法一览", ["Scenarios Overview"] = "玩法一览",
["Replay"] = "录像", ["Replay"] = "录像",
["Replay Manager"] = "来欣赏潇洒的录像吧!", ["Replay Manager"] = "来欣赏潇洒的录像吧!",
["Replay from File"] = "从文件打开",
["Game Win"] = "胜利", ["Game Win"] = "胜利",
["Game Lose"] = "失败", ["Game Lose"] = "失败",
["Play the Replay"] = "重放", ["Play the Replay"] = "重放",
@ -212,7 +218,7 @@ FreeKill使用的是libgit2的C API与此同时使用Git完成拓展包的下
["IncludeDeputy"] = "<font color=\"red\">启用副将机制</font>", ["IncludeDeputy"] = "<font color=\"red\">启用副将机制</font>",
-- Room -- Room
["$EnterRoom"] = "成功加入房间", ["$EnterRoom"] = "成功加入房间",
["#currentRoundNum"] = "第 %1 轮", ["#currentRoundNum"] = "第 %1 轮",
["$Choice"] = "%1请选择", ["$Choice"] = "%1请选择",
["$ChooseGeneral"] = "请选择 %1 名武将", ["$ChooseGeneral"] = "请选择 %1 名武将",
@ -222,7 +228,7 @@ FreeKill使用的是libgit2的C API与此同时使用Git完成拓展包的下
["#PlayCard"] = "出牌阶段,请使用一张牌", ["#PlayCard"] = "出牌阶段,请使用一张牌",
["#AskForGeneral"] = "请选择 1 名武将", ["#AskForGeneral"] = "请选择 1 名武将",
["#AskForSkillInvoke"] = "你想发动技能“%1”吗?", ["#AskForSkillInvoke"] = "你想发动〖%1〗吗?",
["#AskForLuckCard"] = "你想使用手气卡吗?还可以使用 %1 次,剩余手气卡∞张", ["#AskForLuckCard"] = "你想使用手气卡吗?还可以使用 %1 次,剩余手气卡∞张",
["AskForLuckCard"] = "手气卡", ["AskForLuckCard"] = "手气卡",
["#AskForChoice"] = "%1请选择", ["#AskForChoice"] = "%1请选择",
@ -254,13 +260,13 @@ FreeKill使用的是libgit2的C API与此同时使用Git完成拓展包的下
["$Equip"] = "装备区", ["$Equip"] = "装备区",
["$Judge"] = "判定区", ["$Judge"] = "判定区",
['$Selected'] = "已选择", ['$Selected'] = "已选择",
["#AskForUseActiveSkill"] = "使用技能 %1", ["#AskForUseActiveSkill"] = "发动〖%1〗",
["#AskForUseCard"] = "请使用卡牌 %1", ["#AskForUseCard"] = "请使用【%1】",
["#AskForResponseCard"] = "请打出卡牌 %1", ["#AskForResponseCard"] = "请打出【%1】",
["#AskForNullification"] = "是否为目标为 %dest 的 %arg 使用无懈可击", ["#AskForNullification"] = "是否为目标为 %dest 的【%arg】使用【无懈可击】",
["#AskForNullificationWithoutTo"] = "是否对 %src 使用的 %arg 使用无懈可击", ["#AskForNullificationWithoutTo"] = "是否对 %src 使用的【%arg】使用【无懈可击】",
["#AskForPeaches"] = "%src 生命危急,需要 %arg 个", ["#AskForPeaches"] = "%src 生命危急,需要 %arg 个",
["#AskForPeachesSelf"] = "你生命危急,需要 %arg 个桃或酒", ["#AskForPeachesSelf"] = "你生命危急,需要 %arg 个",
["#AskForDiscard"] = "请弃置 %arg 张牌,最少 %arg2 张", ["#AskForDiscard"] = "请弃置 %arg 张牌,最少 %arg2 张",
["#AskForCard"] = "请选择 %arg 张牌,最少 %arg2 张", ["#AskForCard"] = "请选择 %arg 张牌,最少 %arg2 张",
@ -322,6 +328,9 @@ FreeKill使用的是libgit2的C API与此同时使用Git完成拓展包的下
["Back To Lobby"] = "返回大厅", ["Back To Lobby"] = "返回大厅",
["Save Replay"] = "保存录像", ["Save Replay"] = "保存录像",
["$AddObserver"] = '玩家 <b>%s</b> 开始旁观',
["$RemoveObserver"] = '旁观者 <b>%s</b> 离开了房间',
["Speed Resume"] = "匀速", ["Speed Resume"] = "匀速",
["Speed Up"] = "加速", ["Speed Up"] = "加速",
["Speed Down"] = "减速", ["Speed Down"] = "减速",
@ -373,6 +382,7 @@ Fk:loadTranslationTable{
["pile_discard"] = "弃牌堆", ["pile_discard"] = "弃牌堆",
["processing_area"] = "处理区", ["processing_area"] = "处理区",
["Pile"] = "牌堆", ["Pile"] = "牌堆",
["toObtain"] = "获得的牌",
["Top"] = "牌堆顶", ["Top"] = "牌堆顶",
["Bottom"] = "牌堆底", ["Bottom"] = "牌堆底",
["Shuffle"] = "洗牌", ["Shuffle"] = "洗牌",
@ -408,8 +418,8 @@ Fk:loadTranslationTable{
["$GameEnd"] = "== 游戏结束 ==", ["$GameEnd"] = "== 游戏结束 ==",
-- get/lose skill -- get/lose skill
["#AcquireSkill"] = "%from 获得了技能 “%arg”", ["#AcquireSkill"] = "%from 获得了〖%arg〗",
["#LoseSkill"] = "%from 失去了技能 “%arg”", ["#LoseSkill"] = "%from 失去了〖%arg〗",
-- moveCards (they are sent by notifyMoveCards) -- moveCards (they are sent by notifyMoveCards)
["$GetCardsFromPile"] = "%from 从 %arg 中获得了 %arg2 张牌 %card", ["$GetCardsFromPile"] = "%from 从 %arg 中获得了 %arg2 张牌 %card",
@ -432,6 +442,8 @@ Fk:loadTranslationTable{
["$DiscardCards"] = "%from 弃置了 %arg 张牌 %card", ["$DiscardCards"] = "%from 弃置了 %arg 张牌 %card",
["$DiscardOther"] = "%to 弃置了 %from 的 %arg 张牌 %card", ["$DiscardOther"] = "%to 弃置了 %from 的 %arg 张牌 %card",
["$PutToDiscard"] = "%arg 张牌 %card 被置入弃牌堆", ["$PutToDiscard"] = "%arg 张牌 %card 被置入弃牌堆",
["$ViewCardFromDrawPile"] = "%from 观看了 %arg 张牌",
["$TurnOverCardFromDrawPile"] = "%from 亮出了 %arg 张牌 %card",
["#AbortArea"] = "%from 的 %arg 被废除", ["#AbortArea"] = "%from 的 %arg 被废除",
["#ResumeArea"] = "%from 的 %arg 被恢复", ["#ResumeArea"] = "%from 的 %arg 被恢复",
@ -464,30 +476,30 @@ Fk:loadTranslationTable{
["#FilterCard"] = "由于 %arg 的效果,与 %from 相关的 %arg2 被视为了 %arg3", ["#FilterCard"] = "由于 %arg 的效果,与 %from 相关的 %arg2 被视为了 %arg3",
-- skill -- skill
["#InvokeSkill"] = "%from 发动了 “%arg”", ["#InvokeSkill"] = "%from 发动了〖%arg〗",
-- judge -- judge
["#StartJudgeReason"] = "%from 开始了 %arg 的判定", ["#StartJudgeReason"] = "%from 开始了 %arg 的判定",
["#InitialJudge"] = "%from 的判定牌为 %arg", ["#InitialJudge"] = "%from 的判定牌为 %arg",
["#ChangedJudge"] = "%from 发动“%arg”把 %to 的判定牌改为 %arg2", ["#ChangedJudge"] = "%from 发动了〖%arg〗把 %to 的判定牌改为 %arg2",
["#JudgeResult"] = "%from 的判定结果为 %arg", ["#JudgeResult"] = "%from 的判定结果为 %arg",
-- turnOver -- turnOver
["#TurnOver"] = "%from 将武将牌翻面,现在是 %arg", ["#TurnOver"] = "%from 将武将牌翻面,现在是 %arg",
["face_up"] = "正面朝上", ["face_up"] = "正面朝上",
["face_down"] = "背面朝上", ["face_down"] = "背面朝上",
-- damage, heal and lose HP -- damage, heal and lose HP
["#Damage"] = "%to 对 %from 造成了 %arg 点 %arg2 伤害", ["#Damage"] = "%to 对 %from 造成了 %arg 点 %arg2 伤害",
["#DamageWithNoFrom"] = "%from 受到了 %arg 点 %arg2 伤害", ["#DamageWithNoFrom"] = "%from 受到了 %arg 点 %arg2 伤害",
["#LoseHP"] = "%from 失去了 %arg 点体力", ["#LoseHP"] = "%from 失去了 %arg 点体力",
["#HealHP"] = "%from 回复了 %arg 点体力", ["#HealHP"] = "%from 回复了 %arg 点体力",
["#ShowHPAndMaxHP"] = "%from 现在的体力值为 %arg体力上限为 %arg2", ["#ShowHPAndMaxHP"] = "%from 的体力值为 %arg体力上限为 %arg2",
["#LoseMaxHP"] = "%from 减了 %arg 点体力上限", ["#LoseMaxHP"] = "%from 减了 %arg 点体力上限",
["#HealMaxHP"] = "%from 加了 %arg 点体力上限", ["#HealMaxHP"] = "%from 加了 %arg 点体力上限",
-- dying and death -- dying and death
["#EnterDying"] = "%from 进入了濒死阶段", ["#EnterDying"] = "%from 进入了濒死状态",
["#KillPlayer"] = "%from [%arg] 阵亡,凶手是 %to", ["#KillPlayer"] = "%from [%arg] 阵亡,凶手是 %to",
["#KillPlayerWithNoKiller"] = "%from [%arg] 阵亡,无伤害来源", ["#KillPlayerWithNoKiller"] = "%from [%arg] 阵亡,无伤害来源",
["#Revive"] = "%from 竟然复活了", ["#Revive"] = "%from 竟然复活了",
@ -499,7 +511,7 @@ Fk:loadTranslationTable{
["#GuanxingResult"] = "%from 的观星结果为 %arg 上 %arg2 下", ["#GuanxingResult"] = "%from 的观星结果为 %arg 上 %arg2 下",
["#ChainStateChange"] = "%from %arg 了武将牌", ["#ChainStateChange"] = "%from %arg 了武将牌",
["#ChainDamage"] = "%from 处于连环状态,将受到传导的伤害", ["#ChainDamage"] = "%from 处于连环状态,将受到传导的伤害",
["#ChangeKingdom"] = "%from 的国籍从 %arg 变成了 %arg2", ["#ChangeKingdom"] = "%from 的势力从 %arg 变成了 %arg2",
["#RoomOutdated"] = "服务器更新完毕!该房间已过期,将无法再次游玩", ["#RoomOutdated"] = "服务器更新完毕!该房间已过期,将无法再次游玩",
} }
@ -507,10 +519,13 @@ Fk:loadTranslationTable{
Fk:loadTranslationTable{ Fk:loadTranslationTable{
["$$DiscardCards"] = "%from弃置", ["$$DiscardCards"] = "%from弃置",
["$$PutCard"] = "%from置于", ["$$PutCard"] = "%from置于",
["$$TurnOverCard"] = "%from亮出",
["##UseCard"] = "%from使用", ["##UseCard"] = "%from使用",
["##UseCardTo"] = "%from对%to", ["##UseCardTo"] = "%from对%to",
["##ResponsePlayCard"] = "%from打出", ["##ResponsePlayCard"] = "%from打出",
["##ShowCard"] = "%from展示", ["##ShowCard"] = "%from展示",
["##JudgeCard"] = "%arg判定", ["##JudgeCard"] = "%arg判定",
["##PindianCard"] = "%from拼点",
["##RecastCard"] = "%from重铸",
} }

View File

@ -2,33 +2,80 @@
---@class EquipCard : Card ---@class EquipCard : Card
---@field public equip_skill Skill ---@field public equip_skill Skill
---@field public equip_skills Skill[]
---@field public dynamicEquipSkills fun(player: Player): Skill[]
local EquipCard = Card:subclass("EquipCard") local EquipCard = Card:subclass("EquipCard")
function EquipCard:initialize(name, suit, number) function EquipCard:initialize(name, suit, number)
Card.initialize(self, name, suit, number) Card.initialize(self, name, suit, number)
self.type = Card.TypeEquip self.type = Card.TypeEquip
self.equip_skill = nil self.equip_skill = nil
self.equip_skills = nil
self.dynamicEquipSkills = nil
end end
---@param room Room ---@param room Room
---@param player Player ---@param player Player
function EquipCard:onInstall(room, player) function EquipCard:onInstall(room, player)
if self.equip_skill then local equipSkills = self:getEquipSkills(player)
room:handleAddLoseSkills(player, self.equip_skill.name, nil, false, true) if #equipSkills > 0 then
local noTrigger = table.filter(equipSkills, function(skill) return skill.attached_equip end)
if #noTrigger > 0 then
noTrigger = table.map(noTrigger, function(skill) return skill.name end)
room:handleAddLoseSkills(player, table.concat(noTrigger, "|"), nil, false, true)
end
local toTrigger = table.filter(equipSkills, function(skill) return not skill.attached_equip end)
if #toTrigger > 0 then
toTrigger = table.map(toTrigger, function(skill) return skill.name end)
room:handleAddLoseSkills(player, table.concat(toTrigger, "|"), nil, false)
end
end end
end end
---@param room Room ---@param room Room
---@param player Player ---@param player Player
function EquipCard:onUninstall(room, player) function EquipCard:onUninstall(room, player)
if self.equip_skill then local equipSkills = self:getEquipSkills(player)
room:handleAddLoseSkills(player, "-" .. self.equip_skill.name, nil, false, true) if #equipSkills > 0 then
local noTrigger = table.filter(equipSkills, function(skill) return skill.attached_equip end)
if #noTrigger > 0 then
noTrigger = table.map(noTrigger, function(skill) return '-' .. skill.name end)
room:handleAddLoseSkills(player, table.concat(noTrigger, "|"), nil, false, true)
end
local toTrigger = table.filter(equipSkills, function(skill) return not skill.attached_equip end)
if #toTrigger > 0 then
toTrigger = table.map(toTrigger, function(skill) return '-' .. skill.name end)
room:handleAddLoseSkills(player, table.concat(toTrigger, "|"), nil, false)
end
end end
end end
---@param player Player
---@return Skill[]
function EquipCard:getEquipSkills(player)
if self.dynamicEquipSkills then
local equipSkills = self:dynamicEquipSkills(player)
if equipSkills and #equipSkills > 0 then
return equipSkills
end
end
if self.equip_skills then
return self.equip_skills
elseif self.equip_skill then
return { self.equip_skill }
end
return {}
end
function EquipCard:clone(suit, number) function EquipCard:clone(suit, number)
local ret = Card.clone(self, suit, number) local ret = Card.clone(self, suit, number)
ret.equip_skill = self.equip_skill ret.equip_skill = self.equip_skill
ret.equip_skills = self.equip_skills
ret.dynamicEquipSkills = self.dynamicEquipSkills
ret.onInstall = self.onInstall ret.onInstall = self.onInstall
ret.onUninstall = self.onUninstall ret.onUninstall = self.onUninstall
return ret return ret
@ -36,6 +83,7 @@ end
---@class Weapon : EquipCard ---@class Weapon : EquipCard
---@field public attack_range integer ---@field public attack_range integer
---@field public dynamicAttackRange? fun(player: Player): int
local Weapon = EquipCard:subclass("Weapon") local Weapon = EquipCard:subclass("Weapon")
function Weapon:initialize(name, suit, number, attackRange) function Weapon:initialize(name, suit, number, attackRange)
@ -47,9 +95,21 @@ end
function Weapon:clone(suit, number) function Weapon:clone(suit, number)
local ret = EquipCard.clone(self, suit, number) local ret = EquipCard.clone(self, suit, number)
ret.attack_range = self.attack_range ret.attack_range = self.attack_range
ret.dynamicAttackRange = self.dynamicAttackRange
return ret return ret
end end
function Weapon:getAttackRange(player)
if type(self.dynamicAttackRange) == "function" then
local currentAttackRange = self:dynamicAttackRange(player)
if currentAttackRange then
return currentAttackRange
end
end
return self.attack_range
end
---@class Armor : EquipCard ---@class Armor : EquipCard
local Armor = EquipCard:subclass("armor") local Armor = EquipCard:subclass("armor")

View File

@ -676,9 +676,10 @@ end
--- ---
--- 其实就是翻译了 ":" .. name 罢了 --- 其实就是翻译了 ":" .. name 罢了
---@param name string @ 要获得描述的名字 ---@param name string @ 要获得描述的名字
---@param lang? string @ 要使用的语言默认读取config
---@return string @ 描述 ---@return string @ 描述
function Engine:getDescription(name) function Engine:getDescription(name, lang)
return self:translate(":" .. name) return self:translate(":" .. name, lang)
end end
return Engine return Engine

View File

@ -71,4 +71,16 @@ function GameMode:countInFunc(room)
return true return true
end end
-- 修改角色的属性
---@param player ServerPlayer
---@return table @ 返回表,键为调整的角色属性,值为调整后的属性
function GameMode:getAdjustedProperty (player)
local list = {}
if player.role == "lord" and player.role_shown and #player.room.players > 4 then
list.hp = player.hp + 1
list.maxHp = player.maxHp + 1
end
return list
end
return GameMode return GameMode

View File

@ -260,13 +260,7 @@ function Player:removeCards(playerArea, cardIds, specialName)
if #fromAreaIds == 0 then if #fromAreaIds == 0 then
break break
end end
if not table.removeOne(fromAreaIds, id) and not table.removeOne(fromAreaIds, -1) then
if table.contains(fromAreaIds, id) then
table.removeOne(fromAreaIds, id)
-- FIXME: 为客户端移动id为-1的牌考虑但总感觉有地方需要商讨啊
elseif table.every(fromAreaIds, function(e) return e == -1 end) then
table.remove(fromAreaIds, 1)
elseif id == -1 then
table.remove(fromAreaIds, 1) table.remove(fromAreaIds, 1)
end end
end end
@ -455,7 +449,7 @@ function Player:getAttackRange()
baseValue = 0 baseValue = 0
for _, id in ipairs(weapons) do for _, id in ipairs(weapons) do
local weapon = Fk:getCardById(id) local weapon = Fk:getCardById(id)
baseValue = math.max(baseValue, weapon.attack_range or 1) baseValue = math.max(baseValue, weapon:getAttackRange(self) or 1)
end end
end end
@ -563,7 +557,7 @@ end
--- 比较距离 --- 比较距离
---@param other Player @ 终点角色 ---@param other Player @ 终点角色
---@param num integer @ 比较基准 ---@param num integer @ 比较基准
---@param operator string @ 运算符,有 ``"<"`` ``">"`` ``"<="`` ``">="`` ``"=="`` ``"~="`` ---@param operator "<"|">"|"<="|">="|"=="|"~=" @ 运算符
---@return boolean @ 返回比较结果不计入距离结果永远为false ---@return boolean @ 返回比较结果不计入距离结果永远为false
function Player:compareDistance(other, num, operator) function Player:compareDistance(other, num, operator)
local distance = self:distanceTo(other) local distance = self:distanceTo(other)
@ -596,6 +590,11 @@ function Player:inMyAttackRange(other, fixLimit)
fixLimit = fixLimit or 0 fixLimit = fixLimit or 0
local status_skills = Fk:currentRoom().status_skills[AttackRangeSkill] or Util.DummyTable local status_skills = Fk:currentRoom().status_skills[AttackRangeSkill] or Util.DummyTable
for _, skill in ipairs(status_skills) do
if skill:withoutAttackRange(self, other) then
return false
end
end
for _, skill in ipairs(status_skills) do for _, skill in ipairs(status_skills) do
if skill:withinAttackRange(self, other) then if skill:withinAttackRange(self, other) then
return true return true
@ -609,7 +608,8 @@ end
--- 获取下家。 --- 获取下家。
---@param ignoreRemoved? boolean @ 忽略被移除 ---@param ignoreRemoved? boolean @ 忽略被移除
---@param num? integer @ 第几个默认1 ---@param num? integer @ 第几个默认1
---@return ServerPlayer ---@param ignoreRest? boolean @ 是否忽略休整
---@return Player
function Player:getNextAlive(ignoreRemoved, num, ignoreRest) function Player:getNextAlive(ignoreRemoved, num, ignoreRest)
if #Fk:currentRoom().alive_players == 0 then if #Fk:currentRoom().alive_players == 0 then
return self.rest > 0 and self.next.rest > 0 and self.next or self return self.rest > 0 and self.next.rest > 0 and self.next or self
@ -631,9 +631,9 @@ function Player:getNextAlive(ignoreRemoved, num, ignoreRest)
end end
--- 获取上家。 --- 获取上家。
---@param ignoreRemoved boolean @ 忽略被移除 ---@param ignoreRemoved? boolean @ 忽略被移除
---@param num? integer @ 第几个默认1 ---@param num? integer @ 第几个默认1
---@return ServerPlayer ---@return Player
function Player:getLastAlive(ignoreRemoved, num) function Player:getLastAlive(ignoreRemoved, num)
num = num or 1 num = num or 1
local index = (ignoreRemoved and #Fk:currentRoom().alive_players or #table.filter(Fk:currentRoom().alive_players, function(p) return not p:isRemoved() end)) - num local index = (ignoreRemoved and #Fk:currentRoom().alive_players or #table.filter(Fk:currentRoom().alive_players, function(p) return not p:isRemoved() end)) - num
@ -1019,9 +1019,11 @@ function Player:prohibitReveal(isDeputy)
return false return false
end end
---@param to Player --- 判断能否拼点
---@param ignoreFromKong? boolean ---@param to Player @ 拼点对象
---@param ignoreToKong? boolean ---@param ignoreFromKong? boolean @ 忽略发起者没有手牌
---@param ignoreToKong? boolean @ 忽略对象没有手牌
---@return boolean
function Player:canPindian(to, ignoreFromKong, ignoreToKong) function Player:canPindian(to, ignoreFromKong, ignoreToKong)
if self == to then return false end if self == to then return false end
@ -1073,6 +1075,10 @@ function Player:getSwitchSkillState(skillName, afterUse, inWord)
end end
end end
--- 是否能移动特定牌至特定角色
---@param to Player @ 移动至的角色
---@param id integer @ 移动的牌
---@return boolean
function Player:canMoveCardInBoardTo(to, id) function Player:canMoveCardInBoardTo(to, id)
if self == to then if self == to then
return false return false
@ -1094,6 +1100,11 @@ function Player:canMoveCardInBoardTo(to, id)
end end
end end
--- 是否能移动特定牌至特定角色
--- @param to Player @ 移动至的角色
--- @param flag? string @ 移动的区域,`e`为装备区,`j`为判定区,`nil`为装备区和判定区
--- @param excludeIds? integer[] @ 排除的牌
---@return boolean
function Player:canMoveCardsInBoardTo(to, flag, excludeIds) function Player:canMoveCardsInBoardTo(to, flag, excludeIds)
if self == to then if self == to then
return false return false
@ -1120,6 +1131,9 @@ function Player:canMoveCardsInBoardTo(to, flag, excludeIds)
return false return false
end end
--- 获取使命技状态
---@param skillName string
---@return string? @ 存在返回`failed` or `succeed`,不存在返回`nil`
function Player:getQuestSkillState(skillName) function Player:getQuestSkillState(skillName)
local questSkillState = self:getMark(MarkEnum.QuestSkillPreName .. skillName) local questSkillState = self:getMark(MarkEnum.QuestSkillPreName .. skillName)
return type(questSkillState) == "string" and questSkillState or nil return type(questSkillState) == "string" and questSkillState or nil

View File

@ -90,8 +90,19 @@ function Skill:addRelatedSkill(skill)
end end
--- 确认本技能是否为装备技能。 --- 确认本技能是否为装备技能。
---@param player Player
---@return boolean ---@return boolean
function Skill:isEquipmentSkill() function Skill:isEquipmentSkill(player)
if player then
local filterSkills = Fk:currentRoom().status_skills[FilterSkill]
for _, filter in ipairs(filterSkills) do
local result = filter:equipSkillFilter(self, player)
if result then
return true
end
end
end
return self.attached_equip and type(self.attached_equip) == 'string' and self.attached_equip ~= "" return self.attached_equip and type(self.attached_equip) == 'string' and self.attached_equip ~= ""
end end
@ -126,4 +137,11 @@ function Skill:isSwitchSkill()
return self.switchSkillName and type(self.switchSkillName) == 'string' and self.switchSkillName ~= "" return self.switchSkillName and type(self.switchSkillName) == 'string' and self.switchSkillName ~= ""
end end
--判断技能是否为角色技能
---@param player Player
---@return boolean
function Skill:isPlayerSkill(player)
return not (self:isEquipmentSkill(player) or self.name:endsWith("&"))
end
return Skill return Skill

View File

@ -21,47 +21,51 @@ function ActiveSkill:initialize(name, frequency)
end end
--------- ---------
-- Note: these functions are used both client and ai -- 客户端函数AI也会调用以作主动技判断
------- { ------- {
--- Determine whether the skill can be used in playing phase -- 判断该技能是否可主动发动
---@param player Player ---@param player Player @ 使用者
---@param card Card @ helper ---@param card Card @ 牌
---@param extra_data UseExtraData @ 额外数据
---@return bool
function ActiveSkill:canUse(player, card, extra_data) function ActiveSkill:canUse(player, card, extra_data)
return self:isEffectable(player) return self:isEffectable(player)
end end
--- Determine whether a card can be selected by this skill -- 判断一张牌是否可被此技能选中
--- only used in skill of players ---@param to_select integer @ 待选牌
---@param to_select integer @ id of a card not selected ---@param selected integer[] @ 已选牌
---@param selected integer[] @ ids of selected cards ---@param selected_targets integer[] @ 已选目标
---@param selected_targets integer[] @ ids of selected players ---@return bool
function ActiveSkill:cardFilter(to_select, selected, selected_targets) function ActiveSkill:cardFilter(to_select, selected, selected_targets)
return true return true
end end
--- Determine whether a target can be selected by this skill -- 判断一名角色是否可被此技能选中
--- only used in skill of players ---@param to_select integer @ 待选目标
---@param to_select integer @ id of the target ---@param selected integer[] @ 已选目标
---@param selected integer[] @ ids of selected targets ---@param selected_cards integer[] @ 已选牌
---@param selected_cards integer[] @ ids of selected cards ---@param card Card @ 牌
---@param card Card @ helper ---@param extra_data UseExtraData @ 额外数据
---@param extra_data? any @ extra_data ---@return bool
function ActiveSkill:targetFilter(to_select, selected, selected_cards, card, extra_data) function ActiveSkill:targetFilter(to_select, selected, selected_cards, card, extra_data)
return false return false
end end
--- Determine whether a target can be selected by this skill(in modifying targets) -- 判断一名角色是否可成为此技能的目标
--- only used in skill of players ---@param to_select integer @ 待选目标
---@param to_select integer @ id of the target ---@param selected integer[] @ 已选目标
---@param selected? integer[] @ ids of selected targets ---@param user? integer @ 使用者
---@param user? integer @ id of the userdata ---@param card? Card @ 牌
---@param card? Card @ helper ---@param distance_limited? boolean @ 是否受距离限制
---@param distance_limited? boolean @ is limited by distance ---@return bool
function ActiveSkill:modTargetFilter(to_select, selected, user, card, distance_limited) function ActiveSkill:modTargetFilter(to_select, selected, user, card, distance_limited)
return false return false
end end
-- 获得技能的最小目标数
---@return number @ 最小目标数
function ActiveSkill:getMinTargetNum() function ActiveSkill:getMinTargetNum()
local ret local ret
if self.target_num then ret = self.target_num if self.target_num then ret = self.target_num
@ -78,6 +82,10 @@ function ActiveSkill:getMinTargetNum()
end end
end end
-- 获得技能的最大目标数
---@param player Player @ 使用者
---@param card Card @ 牌
---@return number @ 最大目标数
function ActiveSkill:getMaxTargetNum(player, card) function ActiveSkill:getMaxTargetNum(player, card)
local ret local ret
if self.target_num then ret = self.target_num if self.target_num then ret = self.target_num
@ -100,6 +108,8 @@ function ActiveSkill:getMaxTargetNum(player, card)
return ret return ret
end end
-- 获得技能的最小卡牌数
---@return number @ 最小卡牌数
function ActiveSkill:getMinCardNum() function ActiveSkill:getMinCardNum()
local ret local ret
if self.card_num then ret = self.card_num if self.card_num then ret = self.card_num
@ -116,6 +126,8 @@ function ActiveSkill:getMinCardNum()
end end
end end
-- 获得技能的最大卡牌数
---@return number @ 最大卡牌数
function ActiveSkill:getMaxCardNum() function ActiveSkill:getMaxCardNum()
local ret local ret
if self.card_num then ret = self.card_num if self.card_num then ret = self.card_num
@ -132,6 +144,11 @@ function ActiveSkill:getMaxCardNum()
end end
end end
-- 获得技能的距离限制
---@param player Player @ 使用者
---@param card Card @ 使用卡牌
---@param to Player @ 目标
---@return number @ 距离限制
function ActiveSkill:getDistanceLimit(player, card, to) function ActiveSkill:getDistanceLimit(player, card, to)
local ret = self.distance_limit or 0 local ret = self.distance_limit or 0
local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or Util.DummyTable local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or Util.DummyTable
@ -143,8 +160,14 @@ function ActiveSkill:getDistanceLimit(player, card, to)
return ret return ret
end end
-- 判断一个角色是否在技能的距离限制内
---@param player Player @ 使用者
---@param isattack bool @ 是否使用攻击距离
---@param card Card @ 使用卡牌
---@param to Player @ 目标
---@return bool
function ActiveSkill:withinDistanceLimit(player, isattack, card, to) function ActiveSkill:withinDistanceLimit(player, isattack, card, to)
if to and to.dead then return false end if not to or player:distanceTo(to) < 1 then return false end
local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or Util.DummyTable local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or Util.DummyTable
if not card and self.name:endsWith("_skill") then if not card and self.name:endsWith("_skill") then
card = Fk:cloneCard(self.name:sub(1, #self.name - 6)) card = Fk:cloneCard(self.name:sub(1, #self.name - 6))
@ -174,7 +197,7 @@ function ActiveSkill:withinDistanceLimit(player, isattack, card, to)
end end
return (isattack and player:inMyAttackRange(to)) or return (isattack and player:inMyAttackRange(to)) or
(player:distanceTo(to) > 0 and player:distanceTo(to) <= self:getDistanceLimit(player, card, to)) or (player:distanceTo(to) <= self:getDistanceLimit(player, card, to)) or
hasMark(card, MarkEnum.BypassDistancesLimit, card_temp_suf) or hasMark(card, MarkEnum.BypassDistancesLimit, card_temp_suf) or
hasMark(player, MarkEnum.BypassDistancesLimit, temp_suf) or hasMark(player, MarkEnum.BypassDistancesLimit, temp_suf) or
hasMark(to, MarkEnum.BypassDistancesLimitTo, temp_suf) hasMark(to, MarkEnum.BypassDistancesLimitTo, temp_suf)
@ -189,26 +212,32 @@ function ActiveSkill:withinDistanceLimit(player, isattack, card, to)
-- end))) -- end)))
end end
--- Determine if selected cards and targets are valid for this skill -- 判断一个技能是否可发动(也就是确认键是否可点击)
--- If returns true, the OK button should be enabled -- 警告:没啥事别改
--- only used in skill of players ---@param selected integer[] @ 已选目标
---@param selected_cards integer[] @ 已选牌
-- NOTE: don't reclaim it ---@param player Player @ 使用者
---@param selected integer[] @ ids of selected players ---@param card Card @ 牌
---@param selected_cards integer[] @ ids of selected cards ---@return bool
function ActiveSkill:feasible(selected, selected_cards, player, card) function ActiveSkill:feasible(selected, selected_cards, player, card)
return #selected >= self:getMinTargetNum() and #selected <= self:getMaxTargetNum(player, card) return #selected >= self:getMinTargetNum() and #selected <= self:getMaxTargetNum(player, card)
and #selected_cards >= self:getMinCardNum() and #selected_cards <= self:getMaxCardNum() and #selected_cards >= self:getMinCardNum() and #selected_cards <= self:getMaxCardNum()
end end
-- 使用技能时默认的烧条提示(一般会在主动使用时出现)
---@param selected_cards integer[] @ 已选牌
---@param selected_targets integer[] @ 已选目标
---@return string?
function ActiveSkill:prompt(selected_cards, selected_targets) return "" end
------- } ------- }
---@param room Room ---@param room Room
---@param cardUseEvent CardUseStruct ---@param cardUseEvent CardUseStruct | SkillEffectEvent
function ActiveSkill:onUse(room, cardUseEvent) end function ActiveSkill:onUse(room, cardUseEvent) end
---@param room Room ---@param room Room
---@param cardUseEvent CardUseStruct ---@param cardUseEvent CardUseStruct | SkillEffectEvent
---@param finished? bool ---@param finished? bool
function ActiveSkill:onAction(room, cardUseEvent, finished) end function ActiveSkill:onAction(room, cardUseEvent, finished) end
@ -225,8 +254,4 @@ function ActiveSkill:onEffect(room, cardEffectEvent) end
---@param cardEffectEvent CardEffectEvent | SkillEffectEvent ---@param cardEffectEvent CardEffectEvent | SkillEffectEvent
function ActiveSkill:onNullified(room, cardEffectEvent) end function ActiveSkill:onNullified(room, cardEffectEvent) end
---@param selected_cards integer[] @ ids of selected cards
---@param selected_targets integer[] @ ids of selected players
function ActiveSkill:prompt(selected_cards, selected_targets) return "" end
return ActiveSkill return ActiveSkill

View File

@ -15,8 +15,18 @@ function AttackRangeSkill:getFixed(from)
return nil return nil
end end
---@param from Player
---@param to Player
---@return boolean
function AttackRangeSkill:withinAttackRange(from, to) function AttackRangeSkill:withinAttackRange(from, to)
return false return false
end end
---@param from Player
---@param to Player
---@return boolean
function AttackRangeSkill:withoutAttackRange(from, to)
return false
end
return AttackRangeSkill return AttackRangeSkill

View File

@ -17,4 +17,11 @@ function FilterSkill:viewAs(card, player)
return nil return nil
end end
---@param skill Skill
---@param player Player
---@return string
function FilterSkill:equipSkillFilter(skill, player)
return nil
end
return FilterSkill return FilterSkill

View File

@ -14,6 +14,12 @@ function UsableSkill:initialize(name, frequency)
self.max_use_time = {9999, 9999, 9999, 9999} self.max_use_time = {9999, 9999, 9999, 9999}
end end
-- 获得技能的最大使用次数
---@param player Player @ 使用者
---@param scope integer @ 考察时机(默认为回合)
---@param card Card @ 卡牌
---@param to Player @ 目标
---@return number @ 最大使用次数
function UsableSkill:getMaxUseTime(player, scope, card, to) function UsableSkill:getMaxUseTime(player, scope, card, to)
scope = scope or Player.HistoryTurn scope = scope or Player.HistoryTurn
local ret = self.max_use_time[scope] local ret = self.max_use_time[scope]
@ -26,18 +32,31 @@ function UsableSkill:getMaxUseTime(player, scope, card, to)
return ret return ret
end end
-- 判断一个角色是否在技能的次数限制内
---@param player Player @ 使用者
---@param scope integer @ 考察时机(默认为回合)
---@param card? Card @ 牌,若没有牌,则尝试制造一张虚拟牌
---@param card_name? string @ 牌名
---@param to any @ 目标
---@return bool
function UsableSkill:withinTimesLimit(player, scope, card, card_name, to) function UsableSkill:withinTimesLimit(player, scope, card, card_name, to)
if to and to.dead then return false end if to and to.dead then return false end
scope = scope or Player.HistoryTurn scope = scope or Player.HistoryTurn
local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or Util.DummyTable local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or Util.DummyTable
if not card and self.name:endsWith("_skill") then if not card then
card = Fk:cloneCard(self.name:sub(1, #self.name - 6)) if card_name then
card = Fk:cloneCard(card_name)
elseif self.name:endsWith("_skill") then
card = Fk:cloneCard(self.name:sub(1, #self.name - 6))
end
end
if not card_name and card then
card_name = card.trueName
end end
for _, skill in ipairs(status_skills) do for _, skill in ipairs(status_skills) do
if skill:bypassTimesCheck(player, self, scope, card, to) then return true end if skill:bypassTimesCheck(player, self, scope, card, to) then return true end
end end
card_name = card_name or card.trueName
local temp_suf = table.simpleClone(MarkEnum.TempMarkSuffix) local temp_suf = table.simpleClone(MarkEnum.TempMarkSuffix)
local card_temp_suf = table.simpleClone(MarkEnum.CardTempMarkSuffix) local card_temp_suf = table.simpleClone(MarkEnum.CardTempMarkSuffix)

View File

@ -180,11 +180,11 @@ end
---@field public card_filter? fun(self: ActiveSkill, to_select: integer, selected: integer[], selected_targets: integer[]): boolean? ---@field public card_filter? fun(self: ActiveSkill, to_select: integer, selected: integer[], selected_targets: integer[]): boolean?
---@field public target_filter? fun(self: ActiveSkill, to_select: integer, selected: integer[], selected_cards: integer[], card: Card, extra_data: any): boolean? ---@field public target_filter? fun(self: ActiveSkill, to_select: integer, selected: integer[], selected_cards: integer[], card: Card, extra_data: any): boolean?
---@field public feasible? fun(self: ActiveSkill, selected: integer[], selected_cards: integer[]): boolean? ---@field public feasible? fun(self: ActiveSkill, selected: integer[], selected_cards: integer[]): boolean?
---@field public on_use? fun(self: ActiveSkill, room: Room, cardUseEvent: CardUseStruct): boolean? ---@field public on_use? fun(self: ActiveSkill, room: Room, cardUseEvent: CardUseStruct | SkillEffectEvent): boolean?
---@field public on_action? fun(self: ActiveSkill, room: Room, cardUseEvent: CardUseStruct, finished: boolean): boolean? ---@field public on_action? fun(self: ActiveSkill, room: Room, cardUseEvent: CardUseStruct | SkillEffectEvent, finished: boolean): boolean?
---@field public about_to_effect? fun(self: ActiveSkill, room: Room, cardEffectEvent: CardEffectEvent): boolean? ---@field public about_to_effect? fun(self: ActiveSkill, room: Room, cardEffectEvent: CardEffectEvent | SkillEffectEvent): boolean?
---@field public on_effect? fun(self: ActiveSkill, room: Room, cardEffectEvent: CardEffectEvent): boolean? ---@field public on_effect? fun(self: ActiveSkill, room: Room, cardEffectEvent: CardEffectEvent | SkillEffectEvent): boolean?
---@field public on_nullified? fun(self: ActiveSkill, room: Room, cardEffectEvent: CardEffectEvent): boolean? ---@field public on_nullified? fun(self: ActiveSkill, room: Room, cardEffectEvent: CardEffectEvent | SkillEffectEvent): boolean?
---@field public mod_target_filter? fun(self: ActiveSkill, to_select: integer, selected: integer[], user: integer, card: Card, distance_limited: boolean): boolean? ---@field public mod_target_filter? fun(self: ActiveSkill, to_select: integer, selected: integer[], user: integer, card: Card, distance_limited: boolean): boolean?
---@field public prompt? string|fun(self: ActiveSkill, selected_cards: integer[], selected_targets: integer[]): string ---@field public prompt? string|fun(self: ActiveSkill, selected_cards: integer[], selected_targets: integer[]): string
---@field public interaction any ---@field public interaction any
@ -335,12 +335,14 @@ end
---@field public correct_func? fun(self: AttackRangeSkill, from: Player, to: Player): number? ---@field public correct_func? fun(self: AttackRangeSkill, from: Player, to: Player): number?
---@field public fixed_func? fun(self: AttackRangeSkill, player: Player): number? ---@field public fixed_func? fun(self: AttackRangeSkill, player: Player): number?
---@field public within_func? fun(self: AttackRangeSkill, from: Player, to: Player): boolean? ---@field public within_func? fun(self: AttackRangeSkill, from: Player, to: Player): boolean?
---@field public without_func? fun(self: AttackRangeSkill, from: Player, to: Player): boolean?
---@param spec AttackRangeSpec ---@param spec AttackRangeSpec
---@return AttackRangeSkill ---@return AttackRangeSkill
function fk.CreateAttackRangeSkill(spec) function fk.CreateAttackRangeSkill(spec)
assert(type(spec.name) == "string") assert(type(spec.name) == "string")
assert(type(spec.correct_func) == "function" or type(spec.fixed_func) == "function" or type(spec.within_func) == "function") assert(type(spec.correct_func) == "function" or type(spec.fixed_func) == "function" or
type(spec.within_func) == "function" or type(spec.without_func) == "function")
local skill = AttackRangeSkill:new(spec.name) local skill = AttackRangeSkill:new(spec.name)
readStatusSpecToSkill(skill, spec) readStatusSpecToSkill(skill, spec)
@ -353,6 +355,9 @@ function fk.CreateAttackRangeSkill(spec)
if spec.within_func then if spec.within_func then
skill.withinAttackRange = spec.within_func skill.withinAttackRange = spec.within_func
end end
if spec.without_func then
skill.withoutAttackRange = spec.without_func
end
return skill return skill
end end
@ -417,6 +422,7 @@ end
---@class FilterSpec: StatusSkillSpec ---@class FilterSpec: StatusSkillSpec
---@field public card_filter? fun(self: FilterSkill, card: Card, player: Player, isJudgeEvent: boolean): boolean? ---@field public card_filter? fun(self: FilterSkill, card: Card, player: Player, isJudgeEvent: boolean): boolean?
---@field public view_as? fun(self: FilterSkill, card: Card, player: Player): Card? ---@field public view_as? fun(self: FilterSkill, card: Card, player: Player): Card?
---@field public equip_skill_filter? fun(self: FilterSkill, skill: Skill, player: Player): string?
---@param spec FilterSpec ---@param spec FilterSpec
---@return FilterSkill ---@return FilterSkill
@ -427,6 +433,7 @@ function fk.CreateFilterSkill(spec)
readStatusSpecToSkill(skill, spec) readStatusSpecToSkill(skill, spec)
skill.cardFilter = spec.card_filter skill.cardFilter = spec.card_filter
skill.viewAs = spec.view_as skill.viewAs = spec.view_as
skill.equipSkillFilter = spec.equip_skill_filter
return skill return skill
end end
@ -526,7 +533,20 @@ function fk.CreateDelayedTrickCard(spec)
end end
local function readCardSpecToEquip(card, spec) local function readCardSpecToEquip(card, spec)
card.equip_skill = spec.equip_skill if spec.equip_skill then
if spec.equip_skill.class and spec.equip_skill:isInstanceOf(Skill) then
card.equip_skill = spec.equip_skill
card.equip_skills = { spec.equip_skill }
else
card.equip_skill = spec.equip_skill[1]
card.equip_skills = spec.equip_skill
end
end
if spec.dynamic_equip_skills then
assert(type(spec.dynamic_equip_skills) == "function")
card.dynamicEquipSkills = spec.dynamic_equip_skills
end
if spec.on_install then card.onInstall = spec.on_install end if spec.on_install then card.onInstall = spec.on_install end
if spec.on_uninstall then card.onUninstall = spec.on_uninstall end if spec.on_uninstall then card.onUninstall = spec.on_uninstall end
@ -543,6 +563,11 @@ function fk.CreateWeapon(spec)
local card = Weapon:new(spec.name, spec.suit, spec.number, spec.attack_range) local card = Weapon:new(spec.name, spec.suit, spec.number, spec.attack_range)
readCardSpecToCard(card, spec) readCardSpecToCard(card, spec)
readCardSpecToEquip(card, spec) readCardSpecToEquip(card, spec)
if spec.dynamic_attack_range then
assert(type(spec.dynamic_attack_range) == "function")
card.dynamicAttackRange = spec.dynamic_attack_range
end
return card return card
end end
@ -610,6 +635,10 @@ function fk.CreateGameMode(spec)
assert(type(spec.is_counted) == "function") assert(type(spec.is_counted) == "function")
ret.countInFunc = spec.is_counted ret.countInFunc = spec.is_counted
end end
if spec.get_adjusted then
assert(type(spec.get_adjusted) == "function")
ret.getAdjustedProperty = spec.get_adjusted
end
return ret return ret
end end

View File

@ -4,8 +4,7 @@
-- 向Lua虚拟机中加载库、游戏中的类以及加载Mod等等。 -- 向Lua虚拟机中加载库、游戏中的类以及加载Mod等等。
-- 加载第三方库 -- 加载第三方库
package.path = package.path .. ";./lua/lib/?.lua" package.path = "./?.lua;./?/init.lua;./lua/lib/?.lua;./lua/?.lua"
.. ";./lua/?.lua"
-- middleclass: 轻量级的面向对象库 -- middleclass: 轻量级的面向对象库
class = require "middleclass" class = require "middleclass"
@ -62,7 +61,9 @@ UI = require "ui-util"
-- 读取配置文件。 -- 读取配置文件。
-- 因为io马上就要被禁用了所以赶紧先在这里读取配置文件。 -- 因为io马上就要被禁用了所以赶紧先在这里读取配置文件。
local function loadConf() local function loadConf()
local cfg = io.open("freekill.client.config.json") local new_core = FileIO.pwd():endsWith("packages/freekill-core")
local cfg = io.open((new_core and "../../" or "") .. "freekill.client.config.json")
local ret local ret
if cfg == nil then if cfg == nil then
ret = { ret = {

View File

@ -1,6 +1,8 @@
-- SPDX-License-Identifier: GPL-3.0-or-later -- SPDX-License-Identifier: GPL-3.0-or-later
GameEvent.functions[GameEvent.Dying] = function(self) ---@class GameEvent.Dying : GameEvent
local Dying = GameEvent:subclass("GameEvent.Dying")
function Dying:main()
local dyingStruct = table.unpack(self.data) local dyingStruct = table.unpack(self.data)
local room = self.room local room = self.room
local logic = room.logic local logic = room.logic
@ -27,7 +29,7 @@ GameEvent.functions[GameEvent.Dying] = function(self)
end end
end end
GameEvent.exit_funcs[GameEvent.Dying] = function(self) function Dying:exit()
local room = self.room local room = self.room
local logic = room.logic local logic = room.logic
local dyingStruct = self.data[1] local dyingStruct = self.data[1]
@ -41,7 +43,9 @@ GameEvent.exit_funcs[GameEvent.Dying] = function(self)
logic:trigger(fk.AfterDying, dyingPlayer, dyingStruct, self.interrupted) logic:trigger(fk.AfterDying, dyingPlayer, dyingStruct, self.interrupted)
end end
GameEvent.prepare_funcs[GameEvent.Death] = function(self) ---@class GameEvent.Death : GameEvent
local Death = GameEvent:subclass("GameEvent.Death")
function Death:prepare()
local deathStruct = table.unpack(self.data) local deathStruct = table.unpack(self.data)
local room = self.room local room = self.room
local victim = room:getPlayerById(deathStruct.who) local victim = room:getPlayerById(deathStruct.who)
@ -50,7 +54,7 @@ GameEvent.prepare_funcs[GameEvent.Death] = function(self)
end end
end end
GameEvent.functions[GameEvent.Death] = function(self) function Death:main()
local deathStruct = table.unpack(self.data) local deathStruct = table.unpack(self.data)
local room = self.room local room = self.room
local victim = room:getPlayerById(deathStruct.who) local victim = room:getPlayerById(deathStruct.who)
@ -99,7 +103,9 @@ GameEvent.functions[GameEvent.Death] = function(self)
logic:trigger(fk.Deathed, victim, deathStruct) logic:trigger(fk.Deathed, victim, deathStruct)
end end
GameEvent.functions[GameEvent.Revive] = function(self) ---@class GameEvent.Revive : GameEvent
local Revive = GameEvent:subclass("GameEvent.Revive")
function Revive:main()
local room = self.room local room = self.room
local player, sendLog, reason = table.unpack(self.data) local player, sendLog, reason = table.unpack(self.data)
@ -118,3 +124,5 @@ GameEvent.functions[GameEvent.Revive] = function(self)
reason = reason or "" reason = reason or ""
room.logic:trigger(fk.AfterPlayerRevived, player, { reason = reason }) room.logic:trigger(fk.AfterPlayerRevived, player, { reason = reason })
end end
return { Dying, Death, Revive }

View File

@ -47,7 +47,9 @@ local function discardInit(room, player)
end end
end end
GameEvent.functions[GameEvent.DrawInitial] = function(self) ---@class GameEvent.DrawInitial : GameEvent
local DrawInitial = GameEvent:subclass("GameEvent.DrawInitial")
function DrawInitial:main()
local room = self.room local room = self.room
local luck_data = { local luck_data = {
@ -81,6 +83,7 @@ GameEvent.functions[GameEvent.DrawInitial] = function(self)
room:setTag("LuckCardData", luck_data) room:setTag("LuckCardData", luck_data)
room:notifyMoveFocus(room.alive_players, "AskForLuckCard") room:notifyMoveFocus(room.alive_players, "AskForLuckCard")
room:doBroadcastNotify("AskForLuckCard", room.settings.luckTime or 4) room:doBroadcastNotify("AskForLuckCard", room.settings.luckTime or 4)
room.room:setRequestTimer(room.timeout * 1000 + 1000)
local remainTime = room.timeout + 1 local remainTime = room.timeout + 1
local currentTime = os.time() local currentTime = os.time()
@ -123,6 +126,8 @@ GameEvent.functions[GameEvent.DrawInitial] = function(self)
coroutine.yield("__handleRequest", (remainTime - elapsed) * 1000) coroutine.yield("__handleRequest", (remainTime - elapsed) * 1000)
end end
room.room:destroyRequestTimer()
for _, player in ipairs(room.alive_players) do for _, player in ipairs(room.alive_players) do
local draw_data = luck_data[player.id] local draw_data = luck_data[player.id]
draw_data.luckTime = nil draw_data.luckTime = nil
@ -132,10 +137,23 @@ GameEvent.functions[GameEvent.DrawInitial] = function(self)
room:removeTag("LuckCardData") room:removeTag("LuckCardData")
end end
GameEvent.functions[GameEvent.Round] = function(self) ---@class GameEvent.Round : GameEvent
local Round = GameEvent:subclass("GameEvent.Round")
function Round:action()
local room = self.room
local p
repeat
p = room.current
GameEvent.Turn:create(p):exec()
if room.game_finished then break end
room.current = room.current:getNextAlive(true, nil, true)
until p.seat >= p:getNextAlive(true, nil, true).seat
end
function Round:main()
local room = self.room local room = self.room
local logic = room.logic local logic = room.logic
local p
local isFirstRound = room:getTag("FirstRound") local isFirstRound = room:getTag("FirstRound")
if isFirstRound then if isFirstRound then
@ -160,18 +178,11 @@ GameEvent.functions[GameEvent.Round] = function(self)
end end
logic:trigger(fk.RoundStart, room.current) logic:trigger(fk.RoundStart, room.current)
self:action()
repeat
p = room.current
GameEvent(GameEvent.Turn, p):exec()
if room.game_finished then break end
room.current = room.current:getNextAlive(true, nil, true)
until p.seat >= p:getNextAlive(true, nil, true).seat
logic:trigger(fk.RoundEnd, p) logic:trigger(fk.RoundEnd, p)
end end
GameEvent.cleaners[GameEvent.Round] = function(self) function Round:clear()
local room = self.room local room = self.room
for _, p in ipairs(room.players) do for _, p in ipairs(room.players) do
@ -198,7 +209,9 @@ GameEvent.cleaners[GameEvent.Round] = function(self)
end end
end end
GameEvent.prepare_funcs[GameEvent.Turn] = function(self) ---@class GameEvent.Turn : GameEvent
local Turn = GameEvent:subclass("GameEvent.Turn")
function Turn:prepare()
local room = self.room local room = self.room
local logic = room.logic local logic = room.logic
local player = room.current local player = room.current
@ -224,7 +237,7 @@ GameEvent.prepare_funcs[GameEvent.Turn] = function(self)
return logic:trigger(fk.BeforeTurnStart, player) return logic:trigger(fk.BeforeTurnStart, player)
end end
GameEvent.functions[GameEvent.Turn] = function(self) function Turn:main()
local room = self.room local room = self.room
room.current.phase = Player.PhaseNone room.current.phase = Player.PhaseNone
room.logic:trigger(fk.TurnStart, room.current) room.logic:trigger(fk.TurnStart, room.current)
@ -232,7 +245,7 @@ GameEvent.functions[GameEvent.Turn] = function(self)
room.current:play() room.current:play()
end end
GameEvent.cleaners[GameEvent.Turn] = function(self) function Turn:clear()
local room = self.room local room = self.room
local current = room.current local current = room.current
@ -280,7 +293,9 @@ GameEvent.cleaners[GameEvent.Turn] = function(self)
end end
end end
GameEvent.functions[GameEvent.Phase] = function(self) ---@class GameEvent.Phase : GameEvent
local Phase = GameEvent:subclass("GameEvent.Phase")
function Phase:main()
local room = self.room local room = self.room
local logic = room.logic local logic = room.logic
@ -373,7 +388,7 @@ GameEvent.functions[GameEvent.Phase] = function(self)
end end
end end
GameEvent.cleaners[GameEvent.Phase] = function(self) function Phase:clear()
local room = self.room local room = self.room
local player = self.data[1] local player = self.data[1]
local logic = room.logic local logic = room.logic
@ -408,3 +423,5 @@ GameEvent.cleaners[GameEvent.Phase] = function(self)
room:broadcastProperty(p, "MaxCards") room:broadcastProperty(p, "MaxCards")
end end
end end
return { DrawInitial, Round, Turn, Phase }

View File

@ -31,7 +31,9 @@ local function sendDamageLog(room, damageStruct)
}) })
end end
GameEvent.functions[GameEvent.ChangeHp] = function(self) ---@class GameEvent.ChangeHp : GameEvent
local ChangeHp = GameEvent:subclass("GameEvent.ChangeHp")
function ChangeHp:main()
local player, num, reason, skillName, damageStruct = table.unpack(self.data) local player, num, reason, skillName, damageStruct = table.unpack(self.data)
local room = self.room local room = self.room
local logic = room.logic local logic = room.logic
@ -112,17 +114,21 @@ GameEvent.functions[GameEvent.ChangeHp] = function(self)
return true return true
end end
GameEvent.functions[GameEvent.Damage] = function(self) ---@class GameEvent.Damage : GameEvent
local Damage = GameEvent:subclass("GameEvent.Damage")
function Damage:main()
local damageStruct = table.unpack(self.data) local damageStruct = table.unpack(self.data)
local room = self.room local room = self.room
local logic = room.logic local logic = room.logic
if not damageStruct.chain and logic:damageByCardEffect(not not damageStruct.from) then if not damageStruct.chain and logic:damageByCardEffect(false) then
local cardEffectData = logic:getCurrentEvent():findParent(GameEvent.CardEffect) local cardEffectData = logic:getCurrentEvent():findParent(GameEvent.CardEffect)
if cardEffectData then if cardEffectData then
local cardEffectEvent = cardEffectData.data[1] local cardEffectEvent = cardEffectData.data[1]
damageStruct.damage = damageStruct.damage + (cardEffectEvent.additionalDamage or 0) damageStruct.damage = damageStruct.damage + (cardEffectEvent.additionalDamage or 0)
damageStruct.by_user = true if damageStruct.from and cardEffectEvent.from == damageStruct.from.id then
damageStruct.by_user = true
end
end end
end end
@ -137,12 +143,14 @@ GameEvent.functions[GameEvent.Damage] = function(self)
assert(damageStruct.to:isInstanceOf(ServerPlayer)) assert(damageStruct.to:isInstanceOf(ServerPlayer))
local stages = { local stages = {}
{fk.PreDamage, "from"},
}
if not damageStruct.isVirtualDMG then if not damageStruct.isVirtualDMG then
table.insertTable(stages, { { fk.DamageCaused, "from" }, { fk.DamageInflicted, "to" } }) stages = {
{ fk.PreDamage, "from"},
{ fk.DamageCaused, "from" },
{ fk.DamageInflicted, "to" },
}
end end
for _, struct in ipairs(stages) do for _, struct in ipairs(stages) do
@ -198,7 +206,7 @@ GameEvent.functions[GameEvent.Damage] = function(self)
return true return true
end end
GameEvent.exit_funcs[GameEvent.Damage] = function(self) function Damage:exit()
local room = self.room local room = self.room
local logic = room.logic local logic = room.logic
local damageStruct = self.data[1] local damageStruct = self.data[1]
@ -230,7 +238,9 @@ GameEvent.exit_funcs[GameEvent.Damage] = function(self)
end end
end end
GameEvent.functions[GameEvent.LoseHp] = function(self) ---@class GameEvent.LoseHp : GameEvent
local LoseHp = GameEvent:subclass("GameEvent.LoseHp")
function LoseHp:main()
local player, num, skillName = table.unpack(self.data) local player, num, skillName = table.unpack(self.data)
local room = self.room local room = self.room
local logic = room.logic local logic = room.logic
@ -258,7 +268,9 @@ GameEvent.functions[GameEvent.LoseHp] = function(self)
return true return true
end end
GameEvent.functions[GameEvent.Recover] = function(self) ---@class GameEvent.Recover : GameEvent
local Recover = GameEvent:subclass("GameEvent.Recover")
function Recover:main()
local recoverStruct = table.unpack(self.data) local recoverStruct = table.unpack(self.data)
local room = self.room local room = self.room
local logic = room.logic local logic = room.logic
@ -289,7 +301,9 @@ GameEvent.functions[GameEvent.Recover] = function(self)
return true return true
end end
GameEvent.functions[GameEvent.ChangeMaxHp] = function(self) ---@class GameEvent.ChangeMaxHp : GameEvent
local ChangeMaxHp = GameEvent:subclass("GameEvent.ChangeMaxHp")
function ChangeMaxHp:main()
local player, num = table.unpack(self.data) local player, num = table.unpack(self.data)
local room = self.room local room = self.room
@ -344,3 +358,5 @@ GameEvent.functions[GameEvent.ChangeMaxHp] = function(self)
room.logic:trigger(fk.MaxHpChanged, player, { num = num }) room.logic:trigger(fk.MaxHpChanged, player, { num = num })
return true return true
end end
return { ChangeHp, Damage, LoseHp, Recover, ChangeMaxHp }

View File

@ -5,51 +5,46 @@
-- 某类事件对应的结束事件其id刚好就是那个事件的相反数 -- 某类事件对应的结束事件其id刚好就是那个事件的相反数
-- GameEvent.EventFinish = -1 -- GameEvent.EventFinish = -1
GameEvent.Game = 0 local tmp
tmp = require "server.events.misc"
GameEvent.Game = tmp[1]
GameEvent.ChangeProperty = tmp[2]
GameEvent.ClearEvent = tmp[3]
GameEvent.ChangeHp = 1 tmp = require "server.events.hp"
GameEvent.Damage = 2 GameEvent.ChangeHp = tmp[1]
GameEvent.LoseHp = 3 GameEvent.Damage = tmp[2]
GameEvent.Recover = 4 GameEvent.LoseHp = tmp[3]
GameEvent.ChangeMaxHp = 5 GameEvent.Recover = tmp[4]
dofile "lua/server/events/hp.lua" GameEvent.ChangeMaxHp = tmp[5]
GameEvent.Dying = 6 tmp = require "server.events.death"
GameEvent.Death = 7 GameEvent.Dying = tmp[1]
GameEvent.Revive = 22 GameEvent.Death = tmp[2]
dofile "lua/server/events/death.lua" GameEvent.Revive = tmp[3]
GameEvent.MoveCards = 8 tmp = require "server.events.movecard"
dofile "lua/server/events/movecard.lua" GameEvent.MoveCards = tmp
GameEvent.UseCard = 9 tmp = require "server.events.usecard"
GameEvent.RespondCard = 10 GameEvent.UseCard = tmp[1]
GameEvent.CardEffect = 20 GameEvent.RespondCard = tmp[2]
dofile "lua/server/events/usecard.lua" GameEvent.CardEffect = tmp[3]
GameEvent.SkillEffect = 11 tmp = require "server.events.skill"
-- GameEvent.AddSkill = 12 GameEvent.SkillEffect = tmp
-- GameEvent.LoseSkill = 13
dofile "lua/server/events/skill.lua"
GameEvent.Judge = 14 tmp = require "server.events.judge"
dofile "lua/server/events/judge.lua" GameEvent.Judge = tmp
GameEvent.DrawInitial = 15 tmp = require "server.events.gameflow"
GameEvent.Round = 16 GameEvent.DrawInitial = tmp[1]
GameEvent.Turn = 17 GameEvent.Round = tmp[2]
GameEvent.Phase = 18 GameEvent.Turn = tmp[3]
dofile "lua/server/events/gameflow.lua" GameEvent.Phase = tmp[4]
GameEvent.Pindian = 19 tmp = require "server.events.pindian"
dofile "lua/server/events/pindian.lua" GameEvent.Pindian = tmp
-- 20 = CardEffect
GameEvent.ChangeProperty = 21
-- 新的clear函数专用
GameEvent.ClearEvent = 9999
dofile "lua/server/events/misc.lua"
for _, l in ipairs(Fk._custom_events) do for _, l in ipairs(Fk._custom_events) do
local name, p, m, c, e = l.name, l.p, l.m, l.c, l.e local name, p, m, c, e = l.name, l.p, l.m, l.c, l.e
@ -58,37 +53,3 @@ for _, l in ipairs(Fk._custom_events) do
GameEvent.cleaners[name] = c GameEvent.cleaners[name] = c
GameEvent.exit_funcs[name] = e GameEvent.exit_funcs[name] = e
end end
local eventTranslations = {
[GameEvent.Game] = "GameEvent.Game",
[GameEvent.ChangeHp] = "GameEvent.ChangeHp",
[GameEvent.Damage] = "GameEvent.Damage",
[GameEvent.LoseHp] = "GameEvent.LoseHp",
[GameEvent.Recover] = "GameEvent.Recover",
[GameEvent.ChangeMaxHp] = "GameEvent.ChangeMaxHp",
[GameEvent.Dying] = "GameEvent.Dying",
[GameEvent.Death] = "GameEvent.Death",
[GameEvent.Revive] = "GameEvent.Revive",
[GameEvent.MoveCards] = "GameEvent.MoveCards",
[GameEvent.UseCard] = "GameEvent.UseCard",
[GameEvent.RespondCard] = "GameEvent.RespondCard",
[GameEvent.CardEffect] = "GameEvent.CardEffect",
[GameEvent.SkillEffect] = "GameEvent.SkillEffect",
[GameEvent.Judge] = "GameEvent.Judge",
[GameEvent.DrawInitial] = "GameEvent.DrawInitial",
[GameEvent.Round] = "GameEvent.Round",
[GameEvent.Turn] = "GameEvent.Turn",
[GameEvent.Phase] = "GameEvent.Phase",
[GameEvent.Pindian] = "GameEvent.Pindian",
[GameEvent.ChangeProperty] = "GameEvent.ChangeProperty",
[GameEvent.ClearEvent] = "GameEvent.ClearEvent",
}
function GameEvent.static:translate(id)
local ret = eventTranslations[id]
if not ret then ret = id end
return ret
end

View File

@ -1,6 +1,8 @@
-- SPDX-License-Identifier: GPL-3.0-or-later -- SPDX-License-Identifier: GPL-3.0-or-later
GameEvent.functions[GameEvent.Judge] = function(self) ---@class GameEvent.Judge : GameEvent
local Judge = GameEvent:subclass("GameEvent.Judge")
function Judge:main()
local data = table.unpack(self.data) local data = table.unpack(self.data)
local room = self.room local room = self.room
local logic = room.logic local logic = room.logic
@ -53,7 +55,7 @@ GameEvent.functions[GameEvent.Judge] = function(self)
end end
end end
GameEvent.cleaners[GameEvent.Judge] = function(self) function Judge:clear()
local data = table.unpack(self.data) local data = table.unpack(self.data)
local room = self.room local room = self.room
if (self.interrupted or not data.skipDrop) and room:getCardArea(data.card.id) == Card.Processing then if (self.interrupted or not data.skipDrop) and room:getCardArea(data.card.id) == Card.Processing then
@ -71,3 +73,5 @@ GameEvent.cleaners[GameEvent.Judge] = function(self)
end end
}) })
end end
return Judge

View File

@ -1,10 +1,14 @@
-- SPDX-License-Identifier: GPL-3.0-or-later -- SPDX-License-Identifier: GPL-3.0-or-later
GameEvent.functions[GameEvent.Game] = function(self) ---@class GameEvent.Game : GameEvent
local Game = GameEvent:subclass("GameEvent.Game")
function Game:main()
self.room.logic:run() self.room.logic:run()
end end
GameEvent.functions[GameEvent.ChangeProperty] = function(self) ---@class GameEvent.ChangeProperty : GameEvent
local ChangeProperty = GameEvent:subclass("GameEvent.Game")
function ChangeProperty:main()
local data = table.unpack(self.data) local data = table.unpack(self.data)
local room = self.room local room = self.room
local player = data.from local player = data.from
@ -125,12 +129,14 @@ GameEvent.functions[GameEvent.ChangeProperty] = function(self)
logic:trigger(fk.AfterPropertyChange, player, data) logic:trigger(fk.AfterPropertyChange, player, data)
end end
GameEvent.functions[GameEvent.ClearEvent] = function(self) ---@class GameEvent.ClearEvent : GameEvent
local ClearEvent = GameEvent:subclass("GameEvent.ClearEvent")
function ClearEvent:main()
local event = self.data[1] local event = self.data[1]
local logic = self.room.logic local logic = self.room.logic
-- 不可中断 -- 不可中断
Pcall(event.clear_func, event) Pcall(event.clear, event)
for _, f in ipairs(event.extra_clear_funcs) do for _, f in ipairs(event.extra_clear) do
if type(f) == "function" then Pcall(f, event) end if type(f) == "function" then Pcall(f, event) end
end end
@ -147,3 +153,5 @@ GameEvent.functions[GameEvent.ClearEvent] = function(self)
logic.game_event_stack:pop() logic.game_event_stack:pop()
logic.cleaner_stack:pop() logic.cleaner_stack:pop()
end end
return { Game, ChangeProperty, ClearEvent }

View File

@ -1,6 +1,8 @@
-- SPDX-License-Identifier: GPL-3.0-or-later -- SPDX-License-Identifier: GPL-3.0-or-later
GameEvent.functions[GameEvent.MoveCards] = function(self) ---@class GameEvent.MoveCards : GameEvent
local MoveCards = GameEvent:subclass("GameEvent.MoveCards")
function MoveCards:main()
local args = self.data local args = self.data
local room = self.room local room = self.room
---@type CardsMoveStruct[] ---@type CardsMoveStruct[]
@ -57,6 +59,7 @@ GameEvent.functions[GameEvent.MoveCards] = function(self)
specialVisible = cardsMoveInfo.specialVisible, specialVisible = cardsMoveInfo.specialVisible,
drawPilePosition = cardsMoveInfo.drawPilePosition, drawPilePosition = cardsMoveInfo.drawPilePosition,
moveMark = cardsMoveInfo.moveMark, moveMark = cardsMoveInfo.moveMark,
visiblePlayers = cardsMoveInfo.visiblePlayers,
} }
table.insert(cardsMoveStructs, cardsMoveStruct) table.insert(cardsMoveStructs, cardsMoveStruct)
@ -69,10 +72,11 @@ GameEvent.functions[GameEvent.MoveCards] = function(self)
from = cardsMoveInfo.from, from = cardsMoveInfo.from,
toArea = Card.DiscardPile, toArea = Card.DiscardPile,
moveReason = fk.ReasonPutIntoDiscardPile, moveReason = fk.ReasonPutIntoDiscardPile,
specialName = cardsMoveInfo.specialName, moveVisible = true,
specialVisible = cardsMoveInfo.specialVisible, --specialName = cardsMoveInfo.specialName,
drawPilePosition = cardsMoveInfo.drawPilePosition, --specialVisible = cardsMoveInfo.specialVisible,
moveMark = cardsMoveInfo.moveMark, --drawPilePosition = cardsMoveInfo.drawPilePosition,
--moveMark = cardsMoveInfo.moveMark,
} }
table.insert(cardsMoveStructs, cardsMoveStruct) table.insert(cardsMoveStructs, cardsMoveStruct)
@ -159,7 +163,7 @@ GameEvent.functions[GameEvent.MoveCards] = function(self)
realFromArea == Player.Equip and realFromArea == Player.Equip and
beforeCard.type == Card.TypeEquip and beforeCard.type == Card.TypeEquip and
data.from ~= nil and data.from ~= nil and
beforeCard.equip_skill #beforeCard:getEquipSkills(room:getPlayerById(data.from)) > 0
then then
beforeCard:onUninstall(room, room:getPlayerById(data.from)) beforeCard:onUninstall(room, room:getPlayerById(data.from))
end end
@ -183,15 +187,20 @@ GameEvent.functions[GameEvent.MoveCards] = function(self)
end end
end end
if data.moveMark then if data.moveMark then
local mark = table.clone(data.moveMark) or {"", 0} local mark = data.moveMark
room:setCardMark(currentCard, mark[1], mark[2]) if type(mark) == "string" then
room:setCardMark(currentCard, mark, 1)
elseif type(mark) == "table" then
mark = table.clone(data.moveMark) or {"", 0}
room:setCardMark(currentCard, mark[1], mark[2])
end
end end
if if
data.toArea == Player.Equip and data.toArea == Player.Equip and
currentCard.type == Card.TypeEquip and currentCard.type == Card.TypeEquip and
data.to ~= nil and data.to ~= nil and
room:getPlayerById(data.to):isAlive() and room:getPlayerById(data.to):isAlive() and
currentCard.equip_skill #currentCard:getEquipSkills(room:getPlayerById(data.to)) > 0
then then
currentCard:onInstall(room, room:getPlayerById(data.to)) currentCard:onInstall(room, room:getPlayerById(data.to))
end end
@ -202,3 +211,5 @@ GameEvent.functions[GameEvent.MoveCards] = function(self)
room.logic:trigger(fk.AfterCardsMove, nil, cardsMoveStructs) room.logic:trigger(fk.AfterCardsMove, nil, cardsMoveStructs)
return true return true
end end
return MoveCards

View File

@ -1,6 +1,8 @@
-- SPDX-License-Identifier: GPL-3.0-or-later -- SPDX-License-Identifier: GPL-3.0-or-later
GameEvent.functions[GameEvent.Pindian] = function(self) ---@class GameEvent.Pindian : GameEvent
local Pindian = GameEvent:subclass("GameEvent.Pindian")
function Pindian:main()
local pindianData = table.unpack(self.data) local pindianData = table.unpack(self.data)
local room = self.room local room = self.room
local logic = room.logic local logic = room.logic
@ -35,6 +37,7 @@ GameEvent.functions[GameEvent.Pindian] = function(self)
pindianCard:addSubcard(_pindianCard.id) pindianCard:addSubcard(_pindianCard.id)
pindianData.fromCard = pindianCard pindianData.fromCard = pindianCard
pindianData._fromCard = _pindianCard
table.insert(moveInfos, { table.insert(moveInfos, {
ids = { _pindianCard.id }, ids = { _pindianCard.id },
@ -53,6 +56,7 @@ GameEvent.functions[GameEvent.Pindian] = function(self)
pindianCard:addSubcard(_pindianCard.id) pindianCard:addSubcard(_pindianCard.id)
pindianData.results[to.id].toCard = pindianCard pindianData.results[to.id].toCard = pindianCard
pindianData.results[to.id]._toCard = _pindianCard
table.insert(moveInfos, { table.insert(moveInfos, {
ids = { _pindianCard.id }, ids = { _pindianCard.id },
@ -86,9 +90,11 @@ GameEvent.functions[GameEvent.Pindian] = function(self)
if p == pindianData.from then if p == pindianData.from then
pindianData.fromCard = pindianCard pindianData.fromCard = pindianCard
pindianData._fromCard = _pindianCard
else else
pindianData.results[p.id] = pindianData.results[p.id] or {} pindianData.results[p.id] = pindianData.results[p.id] or {}
pindianData.results[p.id].toCard = pindianCard pindianData.results[p.id].toCard = pindianCard
pindianData.results[p.id]._toCard = _pindianCard
end end
table.insert(moveInfos, { table.insert(moveInfos, {
@ -109,10 +115,21 @@ GameEvent.functions[GameEvent.Pindian] = function(self)
room:moveCards(table.unpack(moveInfos)) room:moveCards(table.unpack(moveInfos))
room:sendFootnote({ pindianData._fromCard.id }, {
type = "##PindianCard",
from = pindianData.from.id,
})
for _, to in ipairs(pindianData.tos) do
room:sendFootnote({ pindianData.results[to.id]._toCard.id }, {
type = "##PindianCard",
from = to.id,
})
end
logic:trigger(fk.PindianCardsDisplayed, nil, pindianData) logic:trigger(fk.PindianCardsDisplayed, nil, pindianData)
for toId, result in pairs(pindianData.results) do for _, to in ipairs(pindianData.tos) do
local to = room:getPlayerById(toId) local result = pindianData.results[to.id]
if pindianData.fromCard.number > result.toCard.number then if pindianData.fromCard.number > result.toCard.number then
result.winner = pindianData.from result.winner = pindianData.from
elseif pindianData.fromCard.number < result.toCard.number then elseif pindianData.fromCard.number < result.toCard.number then
@ -131,9 +148,13 @@ GameEvent.functions[GameEvent.Pindian] = function(self)
room:sendLog{ room:sendLog{
type = "#ShowPindianResult", type = "#ShowPindianResult",
from = pindianData.from.id, from = pindianData.from.id,
to = { toId }, to = { to.id },
arg = result.winner == pindianData.from and "pindianwin" or "pindiannotwin" arg = result.winner == pindianData.from and "pindianwin" or "pindiannotwin"
} }
-- room:setCardEmotion(pindianData._fromCard.id, result.winner == pindianData.from and "pindianwin" or "pindiannotwin")
-- room:setCardEmotion(pindianData.results[to.id]._toCard.id, result.winner == to and "pindianwin" or "pindiannotwin")
logic:trigger(fk.PindianResultConfirmed, nil, singlePindianData) logic:trigger(fk.PindianResultConfirmed, nil, singlePindianData)
end end
@ -142,7 +163,7 @@ GameEvent.functions[GameEvent.Pindian] = function(self)
end end
end end
GameEvent.cleaners[GameEvent.Pindian] = function(self) function Pindian:clear()
local pindianData = table.unpack(self.data) local pindianData = table.unpack(self.data)
local room = self.room local room = self.room
@ -168,3 +189,5 @@ GameEvent.cleaners[GameEvent.Pindian] = function(self)
end end
if not self.interrupted then return end if not self.interrupted then return end
end end
return Pindian

View File

@ -1,6 +1,8 @@
-- SPDX-License-Identifier: GPL-3.0-or-later -- SPDX-License-Identifier: GPL-3.0-or-later
GameEvent.functions[GameEvent.SkillEffect] = function(self) ---@class GameEvent.SkillEffect : GameEvent
local SkillEffect = GameEvent:subclass("GameEvent.SkillEffect")
function SkillEffect:main()
local effect_cb, player, _skill = table.unpack(self.data) local effect_cb, player, _skill = table.unpack(self.data)
local room = self.room local room = self.room
local logic = room.logic local logic = room.logic
@ -19,3 +21,5 @@ GameEvent.functions[GameEvent.SkillEffect] = function(self)
logic:trigger(fk.AfterSkillEffect, player, skill) logic:trigger(fk.AfterSkillEffect, player, skill)
return ret return ret
end end
return SkillEffect

View File

@ -162,7 +162,9 @@ local sendCardEmotionAndLog = function(room, cardUseEvent)
return _card return _card
end end
GameEvent.functions[GameEvent.UseCard] = function(self) ---@class GameEvent.UseCard : GameEvent
local UseCard = GameEvent:subclass("GameEvent.UseCard")
function UseCard:main()
local cardUseEvent = table.unpack(self.data) local cardUseEvent = table.unpack(self.data)
local room = self.room local room = self.room
local logic = room.logic local logic = room.logic
@ -185,6 +187,27 @@ GameEvent.functions[GameEvent.UseCard] = function(self)
cardUseEvent.card.skill:onUse(room, cardUseEvent) cardUseEvent.card.skill:onUse(room, cardUseEvent)
end end
if cardUseEvent.card.type == Card.TypeEquip then
local targets = TargetGroup:getRealTargets(cardUseEvent.tos)
if #targets == 1 then
local target = room:getPlayerById(targets[1])
local subType = cardUseEvent.card.sub_type
local equipsExist = target:getEquipments(subType)
if #equipsExist > 0 and not target:hasEmptyEquipSlot(subType) then
local choices = table.map(
equipsExist,
function(id, index)
return "#EquipmentChoice:" .. index .. "::" .. Fk:translate(Fk:getCardById(id).name) end
)
if target:hasEmptyEquipSlot(subType) then
table.insert(choices, target:getAvailableEquipSlots(subType)[1])
end
cardUseEvent.toPutSlot = room:askForChoice(target, choices, "replace_equip", "#GameRuleReplaceEquipment")
end
end
end
if logic:trigger(fk.PreCardUse, room:getPlayerById(cardUseEvent.from), cardUseEvent) then if logic:trigger(fk.PreCardUse, room:getPlayerById(cardUseEvent.from), cardUseEvent) then
logic:breakEvent() logic:breakEvent()
end end
@ -238,7 +261,7 @@ GameEvent.functions[GameEvent.UseCard] = function(self)
end end
end end
GameEvent.cleaners[GameEvent.UseCard] = function(self) function UseCard:clear()
local cardUseEvent = table.unpack(self.data) local cardUseEvent = table.unpack(self.data)
local room = self.room local room = self.room
@ -254,7 +277,9 @@ GameEvent.cleaners[GameEvent.UseCard] = function(self)
end end
end end
GameEvent.functions[GameEvent.RespondCard] = function(self) ---@class GameEvent.RespondCard : GameEvent
local RespondCard = GameEvent:subclass("GameEvent.RespondCard")
function RespondCard:main()
local cardResponseEvent = table.unpack(self.data) local cardResponseEvent = table.unpack(self.data)
local room = self.room local room = self.room
local logic = room.logic local logic = room.logic
@ -306,7 +331,7 @@ GameEvent.functions[GameEvent.RespondCard] = function(self)
logic:trigger(fk.CardResponding, room:getPlayerById(cardResponseEvent.from), cardResponseEvent) logic:trigger(fk.CardResponding, room:getPlayerById(cardResponseEvent.from), cardResponseEvent)
end end
GameEvent.cleaners[GameEvent.RespondCard] = function(self) function RespondCard:clear()
local cardResponseEvent = table.unpack(self.data) local cardResponseEvent = table.unpack(self.data)
local room = self.room local room = self.room
@ -322,7 +347,9 @@ GameEvent.cleaners[GameEvent.RespondCard] = function(self)
end end
end end
GameEvent.functions[GameEvent.CardEffect] = function(self) ---@class GameEvent.CardEffect : GameEvent
local CardEffect = GameEvent:subclass("GameEvent.CardEffect")
function CardEffect:main()
local cardEffectEvent = table.unpack(self.data) local cardEffectEvent = table.unpack(self.data)
local room = self.room local room = self.room
local logic = room.logic local logic = room.logic
@ -359,13 +386,16 @@ GameEvent.functions[GameEvent.CardEffect] = function(self)
end end
logic:breakEvent() logic:breakEvent()
end end
elseif cardEffectEvent.to and logic:trigger(event, room:getPlayerById(cardEffectEvent.to), cardEffectEvent) then elseif logic:trigger(event, room:getPlayerById(cardEffectEvent.to), cardEffectEvent) then
cardEffectEvent.nullifiedTargets = cardEffectEvent.nullifiedTargets or {} if cardEffectEvent.to then
table.insert(cardEffectEvent.nullifiedTargets, cardEffectEvent.to) cardEffectEvent.nullifiedTargets = cardEffectEvent.nullifiedTargets or {}
table.insert(cardEffectEvent.nullifiedTargets, cardEffectEvent.to)
end
logic:breakEvent() logic:breakEvent()
end end
room:handleCardEffect(event, cardEffectEvent) room:handleCardEffect(event, cardEffectEvent)
end end
end end
return { UseCard, RespondCard, CardEffect }

View File

@ -4,15 +4,11 @@
---@field public id integer @ 事件的id随着时间推移自动增加并分配给新事件 ---@field public id integer @ 事件的id随着时间推移自动增加并分配给新事件
---@field public end_id integer @ 事件的对应结束id如果整个事件中未插入事件那么end_id就是自己的id ---@field public end_id integer @ 事件的对应结束id如果整个事件中未插入事件那么end_id就是自己的id
---@field public room Room @ room实例 ---@field public room Room @ room实例
---@field public event integer @ 该事件对应的EventType ---@field public event GameEvent @ 该事件对应的EventType现已改为对应的class
---@field public data any @ 事件的附加数据,视类型而定 ---@field public data any @ 事件的附加数据,视类型而定
---@field public parent GameEvent @ 事件的父事件(栈中的上一层事件) ---@field public parent GameEvent @ 事件的父事件(栈中的上一层事件)
---@field public prepare_func fun(self: GameEvent) @ 事件即将开始时执行的函数 ---@field public extra_clear fun(self:GameEvent)[] @ 事件结束时执行的自定义函数列表
---@field public main_func fun(self: GameEvent) @ 事件的主函数 ---@field public extra_exit fun(self:GameEvent)[] @ 事件结束后执行的自定义函数
---@field public clear_func fun(self: GameEvent) @ 事件结束时执行的函数
---@field public extra_clear_funcs fun(self:GameEvent)[] @ 事件结束时执行的自定义函数列表
---@field public exit_func fun(self: GameEvent) @ 事件结束后执行的函数
---@field public extra_exit_funcs fun(self:GameEvent)[] @ 事件结束后执行的自定义函数
---@field public exec_ret boolean? @ exec函数的返回值可能不存在 ---@field public exec_ret boolean? @ exec函数的返回值可能不存在
---@field public status string @ ready, running, exiting, dead ---@field public status string @ ready, running, exiting, dead
---@field public interrupted boolean @ 事件是否是因为被中断而结束的,可能是防止事件或者被杀 ---@field public interrupted boolean @ 事件是否是因为被中断而结束的,可能是防止事件或者被杀
@ -31,61 +27,96 @@ GameEvent.cleaners = {}
---@type (fun(self: GameEvent): bool)[] ---@type (fun(self: GameEvent): bool)[]
GameEvent.exit_funcs = {} GameEvent.exit_funcs = {}
local function wrapCoFunc(f, ...)
if not f then return nil end
local args = {...}
return function() return f(table.unpack(args)) end
end
local dummyFunc = Util.DummyFunc local dummyFunc = Util.DummyFunc
function GameEvent:initialize(event, ...) function GameEvent:initialize(event, ...)
self.id = -1 self.id = -1
self.end_id = -1 self.end_id = -1
self.room = RoomInstance self.room = RoomInstance
-- for compat
self.event = event self.event = event
---@diagnostic disable-next-line
-- self.event = self.class
self.data = { ... } self.data = { ... }
self.prepare_func = GameEvent.prepare_funcs[event] or dummyFunc
self.main_func = wrapCoFunc(GameEvent.functions[event], self) or dummyFunc
self.clear_func = GameEvent.cleaners[event] or dummyFunc
self.extra_clear_funcs = Util.DummyTable
self.exit_func = GameEvent.exit_funcs[event] or dummyFunc
self.extra_exit_funcs = Util.DummyTable
self.status = "ready" self.status = "ready"
self.interrupted = false self.interrupted = false
self.extra_clear = Util.DummyTable
self.extra_exit = Util.DummyTable
end end
-- 静态函数实际定义在events/init.lua ---@generic T
function GameEvent:translate(id) ---@param self T
error('static') ---@return T
function GameEvent.create(self, ...)
if self.class then error('cannot use "create()" by event instances') end
return self:new(self, ...)
end
-- 获取最接近GameEvent的基类
---@return GameEvent
function GameEvent.getBaseClass(self, ...)
if self.class then error('cannot use "getBaseClass()" by event instances') end
if self.super == GameEvent or self == GameEvent then
return self
end
return self.super:getBaseClass()
end
function GameEvent.static:subclassed(subclass)
local mt = getmetatable(subclass)
-- 适配老代码event == GameEvent.Turn之类的奇技淫巧危险性待评估
-- 这样若某个模式启用派生类修改逻辑那么findParent之类的基于父类也能找
mt.__eq = function(a, b)
if not a.super or not b.super then return false end
return rawequal(a, b) or a:isSubclassOf(b) or b:isSubclassOf(a)
end
end end
function GameEvent:__tostring() function GameEvent:__tostring()
return string.format("<%s #%d>", GameEvent:translate(self.event), self.id) return string.format("<%s #%d>",
type(self.event == "string") and self.event or self.class.name, self.id)
end
function GameEvent:prepare()
return (GameEvent.prepare_funcs[self.event] or dummyFunc)(self)
end
function GameEvent:main()
return (GameEvent.functions[self.event] or dummyFunc)(self)
end
function GameEvent:clear()
return (GameEvent.cleaners[self.event] or dummyFunc)(self)
end
function GameEvent:exit()
return (GameEvent.exit_funcs[self.event] or dummyFunc)(self)
end end
function GameEvent:addCleaner(f) function GameEvent:addCleaner(f)
if self.extra_clear_funcs == Util.DummyTable then if self.extra_clear == Util.DummyTable then
self.extra_clear_funcs = {} self.extra_clear= {}
end end
table.insert(self.extra_clear_funcs, f) table.insert(self.extra_clear, f)
end end
function GameEvent:addExitFunc(f) function GameEvent:addExitFunc(f)
if self.extra_exit_funcs == Util.DummyTable then if self.extra_exit== Util.DummyTable then
self.extra_exit_funcs = {} self.extra_exit= {}
end end
table.insert(self.extra_exit_funcs, f) table.insert(self.extra_exit, f)
end end
function GameEvent:prependExitFunc(f) function GameEvent:prependExitFunc(f)
if self.extra_exit_funcs == Util.DummyTable then if self.extra_exit== Util.DummyTable then
self.extra_exit_funcs = {} self.extra_exit= {}
end end
table.insert(self.extra_exit_funcs, 1, f) table.insert(self.extra_exit, 1, f)
end end
-- 找第一个与当前事件有继承关系的特定事件 -- 找第一个与当前事件有继承关系的特定事件
---@param eventType integer @ 事件类型 ---@param eventType GameEvent @ 事件类型
---@param includeSelf bool @ 是否包括本事件 ---@param includeSelf bool @ 是否包括本事件
---@param depth? integer @ 搜索深度 ---@param depth? integer @ 搜索深度
---@return GameEvent? ---@return GameEvent?
@ -187,18 +218,18 @@ function GameEvent:exec()
self.parent = logic:getCurrentEvent() self.parent = logic:getCurrentEvent()
if self:prepare_func() then return true end if self:prepare() then return true end
logic:pushEvent(self) logic:pushEvent(self)
local co = coroutine.create(self.main_func) local co = coroutine.create(function() return self:main() end)
self._co = co self._co = co
self.status = "running" self.status = "running"
coroutine.yield(self, "__newEvent") coroutine.yield(self, "__newEvent")
Pcall(self.exit_func, self) Pcall(self.exit, self)
for _, f in ipairs(self.extra_exit_funcs) do for _, f in ipairs(self.extra_exit) do
if type(f) == "function" then if type(f) == "function" then
Pcall(f, self) Pcall(f, self)
end end

View File

@ -10,7 +10,7 @@
---@field public cleaner_stack Stack ---@field public cleaner_stack Stack
---@field public role_table string[][] ---@field public role_table string[][]
---@field public all_game_events GameEvent[] ---@field public all_game_events GameEvent[]
---@field public event_recorder table<integer, GameEvent> ---@field public event_recorder table<GameEvent, GameEvent>
---@field public current_event_id integer ---@field public current_event_id integer
local GameLogic = class("GameLogic") local GameLogic = class("GameLogic")
@ -23,7 +23,21 @@ function GameLogic:initialize(room)
self.game_event_stack = Stack:new() self.game_event_stack = Stack:new()
self.cleaner_stack = Stack:new() self.cleaner_stack = Stack:new()
self.all_game_events = {} self.all_game_events = {}
self.event_recorder = {} self.event_recorder = setmetatable({}, {
-- 对派生事件而言 共用一个键 键取决于最接近GameEvent类的基类
__newindex = function(t, k, v)
if type(k) == "table" and k:isSubclassOf(GameEvent) then
k = k:getBaseClass()
end
rawset(t, k, v)
end,
__index = function(t, k)
if type(k) == "table" and k:isSubclassOf(GameEvent) then
k = k:getBaseClass()
end
return rawget(t, k)
end,
})
self.current_event_id = 0 self.current_event_id = 0
self.specific_events_id = { self.specific_events_id = {
[GameEvent.Damage] = 1, [GameEvent.Damage] = 1,
@ -65,13 +79,13 @@ function GameLogic:run()
self:action() self:action()
end end
local function execGameEvent(type, ...) ---@return boolean
local event = GameEvent:new(type, ...) local function execGameEvent(tp, ...)
local event = tp:create(...)
local _, ret = event:exec() local _, ret = event:exec()
return ret return ret
end end
function GameLogic:assignRoles() function GameLogic:assignRoles()
local room = self.room local room = self.room
local n = #room.players local n = #room.players
@ -113,17 +127,13 @@ function GameLogic:chooseGenerals()
generals = table.filter(generals, function(g) return not table.contains(lord_generals, g) end) generals = table.filter(generals, function(g) return not table.contains(lord_generals, g) end)
room:returnToGeneralPile(generals) room:returnToGeneralPile(generals)
room:setPlayerGeneral(lord, lord_general, true) room:prepareGeneral(lord, lord_general, deputy, true)
room:askForChooseKingdom({lord}) room:askForChooseKingdom({lord})
room:broadcastProperty(lord, "general")
room:broadcastProperty(lord, "kingdom")
room:setDeputyGeneral(lord, deputy)
room:broadcastProperty(lord, "deputyGeneral")
end end
local nonlord = room:getOtherPlayers(lord, true) local nonlord = room:getOtherPlayers(lord, true)
local generals = room:getNGenerals(#nonlord * generalNum) local generals = table.random(room.general_pile, #nonlord * generalNum)
table.shuffle(generals)
for i, p in ipairs(nonlord) do for i, p in ipairs(nonlord) do
local arg = table.slice(generals, (i - 1) * generalNum + 1, i * generalNum + 1) local arg = table.slice(generals, (i - 1) * generalNum + 1, i * generalNum + 1)
p.request_data = json.encode{ arg, n } p.request_data = json.encode{ arg, n }
@ -133,25 +143,22 @@ function GameLogic:chooseGenerals()
room:notifyMoveFocus(nonlord, "AskForGeneral") room:notifyMoveFocus(nonlord, "AskForGeneral")
room:doBroadcastRequest("AskForGeneral", nonlord) room:doBroadcastRequest("AskForGeneral", nonlord)
local selected = {}
for _, p in ipairs(nonlord) do for _, p in ipairs(nonlord) do
local general, deputy
if p.general == "" and p.reply_ready then if p.general == "" and p.reply_ready then
local general_ret = json.decode(p.client_reply) local general_ret = json.decode(p.client_reply)
local general = general_ret[1] general = general_ret[1]
local deputy = general_ret[2] deputy = general_ret[2]
table.insertTableIfNeed(selected, general_ret)
room:setPlayerGeneral(p, general, true, true)
room:setDeputyGeneral(p, deputy)
else else
room:setPlayerGeneral(p, p.default_reply[1], true, true) general = p.default_reply[1]
room:setDeputyGeneral(p, p.default_reply[2]) deputy = p.default_reply[2]
end end
room:findGeneral(general)
room:findGeneral(deputy)
room:prepareGeneral(p, general, deputy)
p.default_reply = "" p.default_reply = ""
end end
generals = table.filter(generals, function(g) return not table.contains(selected, g) end)
room:returnToGeneralPile(generals)
room:askForChooseKingdom(nonlord) room:askForChooseKingdom(nonlord)
end end
@ -178,14 +185,25 @@ function GameLogic:broadcastGeneral()
p.shield = math.min(general.shield + (deputy and deputy.shield or 0), 5) p.shield = math.min(general.shield + (deputy and deputy.shield or 0), 5)
-- TODO: setup AI here -- TODO: setup AI here
if p.role ~= "lord" then local changer = Fk.game_modes[room.settings.gameMode]:getAdjustedProperty(p)
room:broadcastProperty(p, "general") if changer then
room:broadcastProperty(p, "kingdom") for key, value in pairs(changer) do
room:broadcastProperty(p, "deputyGeneral") p[key] = value
elseif #players >= 5 then end
p.maxHp = p.maxHp + 1
p.hp = p.hp + 1
end end
local fixMaxHp = Fk.generals[p.general].fixMaxHp
local deputyFix = Fk.generals[p.deputyGeneral] and Fk.generals[p.deputyGeneral].fixMaxHp
if deputyFix then
fixMaxHp = fixMaxHp and math.min(fixMaxHp, deputyFix) or deputyFix
end
if fixMaxHp then
p.maxHp = fixMaxHp
end
p.hp = math.min(p.maxHp, p.hp)
room:broadcastProperty(p, "general")
room:broadcastProperty(p, "deputyGeneral")
room:broadcastProperty(p, "kingdom")
room:broadcastProperty(p, "maxHp") room:broadcastProperty(p, "maxHp")
room:broadcastProperty(p, "hp") room:broadcastProperty(p, "hp")
room:broadcastProperty(p, "shield") room:broadcastProperty(p, "shield")
@ -416,7 +434,7 @@ end
-- 此为启动事件管理器并启动第一个事件的初始函数 -- 此为启动事件管理器并启动第一个事件的初始函数
function GameLogic:start() function GameLogic:start()
local root_event = GameEvent:new(GameEvent.Game) local root_event = GameEvent.Game:create()
self:pushEvent(root_event) self:pushEvent(root_event)
@ -424,25 +442,20 @@ function GameLogic:start()
-- 事件管理器协程同时也是Game事件 -- 事件管理器协程同时也是Game事件
-- 当新事件想要exec时就切回此处由这里负责调度协程 -- 当新事件想要exec时就切回此处由这里负责调度协程
-- 一个事件结束后也切回此处然后resume -- 一个事件结束后也切回此处然后resume
local co = coroutine.create(root_event.main_func) local co = coroutine.create(function() return root_event:main() end)
root_event._co = co root_event._co = co
local jump_to -- shutdown函数用
while true do while true do
-- 对于cleaner和正常事件处理更后面来的 -- 对于cleaner和正常事件处理更后面来的
local ne = self:getCurrentEvent() local ne = self:getCurrentEvent()
local ce = self:getCurrentCleaner() local ce = self:getCurrentCleaner()
local e = ce and (ce.id >= ne.id and ce or ne) or ne local e = ce and (ce.id >= ne.id and ce or ne) or ne
-- 如果正在jump的话判断是否需要继续clean否则正常继续 if e == ne and e.killed then
if e == ne and jump_to ~= nil then
e.interrupted = true e.interrupted = true
e.killed = e ~= jump_to
self:clearEvent(e) self:clearEvent(e)
coroutine.close(e._co) coroutine.close(e._co)
e.status = "dead" e.status = "dead"
if e == jump_to then jump_to = nil end -- shutdown结束了
e = self:getCurrentCleaner() e = self:getCurrentCleaner()
end end
@ -467,11 +480,12 @@ function GameLogic:start()
coroutine.close(e._co) coroutine.close(e._co)
e.status = "dead" e.status = "dead"
elseif ret == true then elseif ret == true then
-- 跳到越早发生的事件越好 -- 遍历栈将shutdown图中的事件全标记上killed
if not jump_to then -- 被标记killed的事件之后会自动结束并清理
jump_to = evt for i = self.game_event_stack.p, 1, -1 do
else local event = self.game_event_stack.t[i]
jump_to = jump_to.id < evt.id and jump_to or evt event.killed = true
if event == evt then break end
end end
end end
end end
@ -551,9 +565,9 @@ function GameLogic:clearEvent(event)
if event.event == GameEvent.ClearEvent then return end if event.event == GameEvent.ClearEvent then return end
if event.status == "exiting" then return end if event.status == "exiting" then return end
event.status = "exiting" event.status = "exiting"
local ce = GameEvent(GameEvent.ClearEvent, event) local ce = GameEvent.ClearEvent:create(event)
ce.id = self.current_event_id ce.id = self.current_event_id
local co = coroutine.create(ce.main_func) local co = coroutine.create(function() return ce:main() end)
ce._co = co ce._co = co
self.cleaner_stack:push(ce) self.cleaner_stack:push(ce)
end end
@ -563,7 +577,7 @@ function GameLogic:getCurrentEvent()
return self.game_event_stack.t[self.game_event_stack.p] return self.game_event_stack.t[self.game_event_stack.p]
end end
---@param eventType integer ---@param eventType GameEvent
function GameLogic:getMostRecentEvent(eventType) function GameLogic:getMostRecentEvent(eventType)
return self:getCurrentEvent():findParent(eventType, true) return self:getCurrentEvent():findParent(eventType, true)
end end
@ -581,7 +595,7 @@ function GameLogic:getCurrentSkillName()
end end
-- 在指定历史范围中找至多n个符合条件的事件 -- 在指定历史范围中找至多n个符合条件的事件
---@param eventType integer @ 要查找的事件类型 ---@param eventType GameEvent @ 要查找的事件类型
---@param n integer @ 最多找多少个 ---@param n integer @ 最多找多少个
---@param func fun(e: GameEvent): boolean @ 过滤用的函数 ---@param func fun(e: GameEvent): boolean @ 过滤用的函数
---@param scope integer @ 查询历史范围,只能是当前阶段/回合/轮次 ---@param scope integer @ 查询历史范围,只能是当前阶段/回合/轮次

View File

@ -2,9 +2,13 @@
local function tellRoomToObserver(self, player) local function tellRoomToObserver(self, player)
local observee = self.players[1] local observee = self.players[1]
local start_time = os.getms()
local summary = self:getSummary(observee, true) local summary = self:getSummary(observee, true)
player:doNotify("Observe", json.encode(summary)) player:doNotify("Observe", json.encode(summary))
fk.qInfo(string.format("[Observe] %d, %s, in %.3fms",
self.id, player:getScreenName(), (os.getms() - start_time) / 1000))
table.insert(self.observers, {observee.id, player, player:getId()}) table.insert(self.observers, {observee.id, player, player:getId()})
end end
@ -76,6 +80,7 @@ request_handlers["luckcard"] = function(room, id, reqlist)
p:doNotify("AskForLuckCard", pdata.luckTime) p:doNotify("AskForLuckCard", pdata.luckTime)
else else
p.serverplayer:setThinking(false) p.serverplayer:setThinking(false)
ResumeRoom(room.id)
end end
room:setTag("LuckCardData", luck_data) room:setTag("LuckCardData", luck_data)
@ -111,6 +116,7 @@ request_handlers["surrender"] = function(room, id, reqlist)
room.hasSurrendered = true room.hasSurrendered = true
player.surrendered = true player.surrendered = true
room:doBroadcastNotify("CancelRequest", "") room:doBroadcastNotify("CancelRequest", "")
ResumeRoom(room.id)
end end
request_handlers["updatemini"] = function(room, pid, reqlist) request_handlers["updatemini"] = function(room, pid, reqlist)
@ -127,6 +133,7 @@ end
request_handlers["newroom"] = function(s, id) request_handlers["newroom"] = function(s, id)
s:registerRoom(id) s:registerRoom(id)
ResumeRoom(id)
end end
request_handlers["reloadpackage"] = function(_, _, reqlist) request_handlers["reloadpackage"] = function(_, _, reqlist)
@ -135,31 +142,16 @@ request_handlers["reloadpackage"] = function(_, _, reqlist)
Fk:reloadPackage(path) Fk:reloadPackage(path)
end end
-- 处理异步请求的协程,本身也是个死循环就是了。 return function(self, request)
-- 为了适应调度器,目前又暂且将请求分为“耗时请求”和不耗时请求。 local reqlist = request:split(",")
-- 耗时请求处理后会立刻挂起。不耗时的请求则会不断处理直到请求队列空后再挂起。 local roomId = tonumber(table.remove(reqlist, 1))
local function requestLoop(self) local room = self:getRoom(roomId)
while true do
local ret = false
local request = self.thread:fetchRequest()
if request ~= "" then
ret = true
local reqlist = request:split(",")
local roomId = tonumber(table.remove(reqlist, 1))
local room = self:getRoom(roomId)
if room then if room then
RoomInstance = room RoomInstance = room
local id = tonumber(reqlist[1]) local id = tonumber(reqlist[1])
local command = reqlist[2] local command = reqlist[2]
Pcall(request_handlers[command], room, id, reqlist) Pcall(request_handlers[command], room, id, reqlist)
RoomInstance = nil RoomInstance = nil
end
end
if not ret then
coroutine.yield()
end
end end
end end
return requestLoop

View File

@ -84,6 +84,11 @@ function Room:initialize(_room)
self.request_queue = {} self.request_queue = {}
self.request_self = {} self.request_self = {}
-- doNotify过载保护每次获得控制权时置为0
-- 若在yield之前执行了max次doNotify则强制让出
self.notify_count = 0
self.notify_max = 500
self.settings = json.decode(self.room:settings()) self.settings = json.decode(self.room:settings())
self.disabled_packs = self.settings.disabledPack self.disabled_packs = self.settings.disabledPack
if not Fk.game_modes[self.settings.gameMode] then if not Fk.game_modes[self.settings.gameMode] then
@ -108,10 +113,11 @@ function Room:resume()
local main_co = self.main_co local main_co = self.main_co
if self:checkNoHuman() then if self:checkNoHuman() then
return true goto GAME_OVER
end end
if not self.game_finished then if not self.game_finished then
self.notify_count = 0
ret, err_msg, rest_time = coroutine.resume(main_co, err_msg) ret, err_msg, rest_time = coroutine.resume(main_co, err_msg)
-- handle error -- handle error
@ -162,17 +168,6 @@ function Room:isReady()
return true return true
end end
-- 因为delay函数而延时判断延时是否已经结束。
-- 注意整个delay函数的实现都搬到这来了delay本身只负责挂起协程了。
if self.in_delay then
local rest = self.delay_duration - (os.getms() - self.delay_start) / 1000
if rest <= 0 then
self.in_delay = false
return true
end
return false, rest
end
-- 剩下的就是因为等待应答而未就绪了 -- 剩下的就是因为等待应答而未就绪了
-- 检查所有正在等回答的玩家,如果已经过了烧条时间 -- 检查所有正在等回答的玩家,如果已经过了烧条时间
-- 那么就不认为他还需要时间就绪了 -- 那么就不认为他还需要时间就绪了
@ -182,13 +177,14 @@ function Room:isReady()
for _, p in ipairs(self.players) do for _, p in ipairs(self.players) do
-- 这里判断的话需要用_splayer了不然一控多的情况下会导致重复判断 -- 这里判断的话需要用_splayer了不然一控多的情况下会导致重复判断
if p._splayer:thinking() then if p._splayer:thinking() then
ret = false
-- 烧条烧光了的话就把thinking设为false -- 烧条烧光了的话就把thinking设为false
rest = p.request_timeout * 1000 - (os.getms() - rest = p.request_timeout * 1000 - (os.getms() -
p.request_start) / 1000 p.request_start) / 1000
if rest <= 0 or p.serverplayer:getState() ~= fk.Player_Online then if rest <= 0 or p.serverplayer:getState() ~= fk.Player_Online then
p._splayer:setThinking(false) p._splayer:setThinking(false)
else
ret = false
end end
end end
@ -244,7 +240,6 @@ function Room:run()
local logic = (mode.logic and mode.logic() or GameLogic):new(self) local logic = (mode.logic and mode.logic() or GameLogic):new(self)
self.logic = logic self.logic = logic
if mode.rule then logic:addTriggerSkill(mode.rule) end if mode.rule then logic:addTriggerSkill(mode.rule) end
-- GameEvent(GameEvent.Game):exec()
logic:start() logic:start()
end end
@ -337,7 +332,7 @@ end
--- 获得当前房间中的所有玩家。 --- 获得当前房间中的所有玩家。
--- ---
--- 返回的数组的第一个元素是当前回合玩家,并且按行动顺序进行排序。 --- 返回的数组的第一个元素是当前回合玩家,并且按行动顺序进行排序。
---@param sortBySeat? boolean @ 是否无视按座位排序直接返回 ---@param sortBySeat? boolean @ 是否按座位排序,默认是
---@return ServerPlayer[] @ 房间中玩家的数组 ---@return ServerPlayer[] @ 房间中玩家的数组
function Room:getAllPlayers(sortBySeat) function Room:getAllPlayers(sortBySeat)
if not self.game_started then if not self.game_started then
@ -359,7 +354,7 @@ function Room:getAllPlayers(sortBySeat)
end end
--- 获得所有存活玩家参看getAllPlayers --- 获得所有存活玩家参看getAllPlayers
---@param sortBySeat? boolean ---@param sortBySeat? boolean @ 是否按座位排序,默认是
---@return ServerPlayer[] ---@return ServerPlayer[]
function Room:getAlivePlayers(sortBySeat) function Room:getAlivePlayers(sortBySeat)
if sortBySeat == nil or sortBySeat then if sortBySeat == nil or sortBySeat then
@ -370,7 +365,7 @@ function Room:getAlivePlayers(sortBySeat)
if temp == nil then if temp == nil then
return { table.unpack(self.players) } return { table.unpack(self.players) }
end end
local ret = {current} local ret = current.dead and {} or {current}
while temp ~= current do while temp ~= current do
if not temp.dead then if not temp.dead then
table.insert(ret, temp) table.insert(ret, temp)
@ -386,7 +381,7 @@ end
--- 获得除一名玩家外的其他玩家。 --- 获得除一名玩家外的其他玩家。
---@param player ServerPlayer @ 要排除的玩家 ---@param player ServerPlayer @ 要排除的玩家
---@param sortBySeat? boolean @ 是否要按座位排序? ---@param sortBySeat? boolean @ 是否按座位排序,默认是
---@param include_dead? boolean @ 是否要把死人也算进去? ---@param include_dead? boolean @ 是否要把死人也算进去?
---@return ServerPlayer[] @ 其他玩家列表 ---@return ServerPlayer[] @ 其他玩家列表
function Room:getOtherPlayers(player, sortBySeat, include_dead) function Room:getOtherPlayers(player, sortBySeat, include_dead)
@ -565,8 +560,8 @@ function Room:setBanner(name, value)
end end
---@return boolean ---@return boolean
local function execGameEvent(type, ...) local function execGameEvent(tp, ...)
local event = GameEvent:new(type, ...) local event = tp:create(...)
local _, ret = event:exec() local _, ret = event:exec()
return ret return ret
end end
@ -600,6 +595,41 @@ function Room:setDeputyGeneral(player, general)
self:notifyProperty(player, player, "deputyGeneral") self:notifyProperty(player, player, "deputyGeneral")
end end
---@param player ServerPlayer
---@param general string
---@param deputy string
---@param broadcast boolean|nil
function Room:prepareGeneral(player, general, deputy, broadcast)
self:findGeneral(general)
self:findGeneral(deputy)
local skills = Fk.generals[general]:getSkillNameList()
if Fk.generals[deputy] then
table.insertTable(skills, Fk.generals[deputy]:getSkillNameList())
end
if table.find(skills, function (s) return Fk.skills[s].isHiddenSkill end) then
self:setPlayerMark(player, "__hidden_general", general)
if Fk.generals[deputy] then
self:setPlayerMark(player, "__hidden_deputy", deputy)
deputy = ""
end
general = "hiddenone"
end
player.general = general
player.gender = Fk.generals[general].gender
self:broadcastProperty(player, "gender")
if Fk.generals[deputy] then
player.deputyGeneral = deputy
end
player.kingdom = Fk.generals[general].kingdom
for _, property in ipairs({"general","deputyGeneral","kingdom"}) do
if broadcast then
self:broadcastProperty(player, property)
else
self:notifyProperty(player, player, property)
end
end
end
---@param player ServerPlayer @ 要换将的玩家 ---@param player ServerPlayer @ 要换将的玩家
---@param new_general string @ 要变更的武将,若不存在则变身为孙策,孙策不存在变身为士兵 ---@param new_general string @ 要变更的武将,若不存在则变身为孙策,孙策不存在变身为士兵
---@param full? boolean @ 是否血量满状态变身 ---@param full? boolean @ 是否血量满状态变身
@ -757,6 +787,10 @@ local function surrenderCheck(room)
room.hasSurrendered = false room.hasSurrendered = false
end end
local function setRequestTimer(room)
room.room:setRequestTimer(room.timeout * 1000 + 500)
end
--- 向某个玩家发起一次Request。 --- 向某个玩家发起一次Request。
---@param player ServerPlayer @ 发出这个请求的目标玩家 ---@param player ServerPlayer @ 发出这个请求的目标玩家
---@param command string @ 请求的类型 ---@param command string @ 请求的类型
@ -770,9 +804,11 @@ function Room:doRequest(player, command, jsonData, wait)
player:doRequest(command, jsonData, self.timeout) player:doRequest(command, jsonData, self.timeout)
if wait then if wait then
setRequestTimer(self)
local ret = player:waitForReply(self.timeout) local ret = player:waitForReply(self.timeout)
player.serverplayer:setBusy(false) player.serverplayer:setBusy(false)
player.serverplayer:setThinking(false) player.serverplayer:setThinking(false)
self.room:destroyRequestTimer()
surrenderCheck(self) surrenderCheck(self)
return ret return ret
end end
@ -786,6 +822,7 @@ function Room:doBroadcastRequest(command, players, jsonData)
players = players or self.players players = players or self.players
self.request_queue = {} self.request_queue = {}
self.race_request_list = nil self.race_request_list = nil
setRequestTimer(self)
for _, p in ipairs(players) do for _, p in ipairs(players) do
p:doRequest(command, jsonData or p.request_data) p:doRequest(command, jsonData or p.request_data)
end end
@ -803,6 +840,7 @@ function Room:doBroadcastRequest(command, players, jsonData)
p.serverplayer:setThinking(false) p.serverplayer:setThinking(false)
end end
self.room:destroyRequestTimer()
surrenderCheck(self) surrenderCheck(self)
end end
@ -819,6 +857,7 @@ function Room:doRaceRequest(command, players, jsonData)
players = players or self.players players = players or self.players
players = table.simpleClone(players) players = table.simpleClone(players)
local player_len = #players local player_len = #players
setRequestTimer(self)
-- self:notifyMoveFocus(players, command) -- self:notifyMoveFocus(players, command)
self.request_queue = {} self.request_queue = {}
self.race_request_list = players self.race_request_list = players
@ -837,7 +876,8 @@ function Room:doRaceRequest(command, players, jsonData)
if remainTime - elapsed <= 0 then if remainTime - elapsed <= 0 then
break break
end end
for _, p in ipairs(players) do for i = #players, 1, -1 do
local p = players[i]
p:waitForReply(0) p:waitForReply(0)
if p.reply_ready == true then if p.reply_ready == true then
winner = p winner = p
@ -845,7 +885,7 @@ function Room:doRaceRequest(command, players, jsonData)
end end
if p.reply_cancel then if p.reply_cancel then
table.removeOne(players, p) table.remove(players, i)
table.insertIfNeed(canceled_players, p) table.insertIfNeed(canceled_players, p)
elseif p.id > 0 then elseif p.id > 0 then
-- 骗过调度器让他以为自己尚未就绪 -- 骗过调度器让他以为自己尚未就绪
@ -871,20 +911,16 @@ function Room:doRaceRequest(command, players, jsonData)
p.serverplayer:setThinking(false) p.serverplayer:setThinking(false)
end end
self.room:destroyRequestTimer()
surrenderCheck(self) surrenderCheck(self)
return ret return ret
end end
--- 延迟一段时间。 --- 延迟一段时间。
---
--- 这个函数不应该在请求处理协程中使用。
---@param ms integer @ 要延迟的毫秒数 ---@param ms integer @ 要延迟的毫秒数
function Room:delay(ms) function Room:delay(ms)
local start = os.getms() self.room:delay(ms)
self.delay_start = start
self.delay_duration = ms
self.in_delay = true
coroutine.yield("__handleRequest", ms) coroutine.yield("__handleRequest", ms)
end end
@ -913,24 +949,6 @@ function Room:notifyMoveCards(players, card_moves, forceVisible)
end end
end end
local function containArea(area, relevant) --处理区的处理?
local areas = relevant
and {Card.PlayerEquip, Card.PlayerJudge, Card.DiscardPile, Card.Processing, Card.PlayerHand, Card.PlayerSpecial}
or {Card.PlayerEquip, Card.PlayerJudge, Card.DiscardPile, Card.Processing}
return table.contains(areas, area)
end
-- forceVisible make the move visible
-- if move is relevant to player's hands or equips, it should be open
-- cards move from/to equip/judge/discard/processing should be open
if not (move.moveVisible or forceVisible or containArea(move.toArea, move.to and p.isBuddy and p:isBuddy(move.to))) then
for _, info in ipairs(move.moveInfo) do
if not containArea(info.fromArea, move.from and p.isBuddy and p:isBuddy(move.from)) then
info.cardId = -1
end
end
end
end end
p:doNotify("MoveCards", json.encode(arg)) p:doNotify("MoveCards", json.encode(arg))
end end
@ -1058,7 +1076,7 @@ end
--- 与此同时在战报里面发一条“xxx发动了xxx” --- 与此同时在战报里面发一条“xxx发动了xxx”
---@param player ServerPlayer @ 发动技能的那个玩家 ---@param player ServerPlayer @ 发动技能的那个玩家
---@param skill_name string @ 技能名 ---@param skill_name string @ 技能名
---@param skill_type? string @ 技能的动画效果默认是那个技能的anim_type ---@param skill_type? string | AnimationType @ 技能的动画效果默认是那个技能的anim_type
function Room:notifySkillInvoked(player, skill_name, skill_type) function Room:notifySkillInvoked(player, skill_name, skill_type)
local bigAnim = false local bigAnim = false
if not skill_type then if not skill_type then
@ -1842,7 +1860,13 @@ function Room:askForChoice(player, choices, skill_name, prompt, detailed, all_ch
local result = self:doRequest(player, command, json.encode{ local result = self:doRequest(player, command, json.encode{
choices, all_choices, skill_name, prompt, detailed choices, all_choices, skill_name, prompt, detailed
}) })
if result == "" then result = choices[1] end if result == "" then
if table.contains(choices, "Cancel") then
result = "Cancel"
else
result = choices[1]
end
end
return result return result
end end
@ -1970,6 +1994,93 @@ function Room:askForAddTarget(player, targets, num, can_minus, distance_limited,
return {} return {}
end end
--- 询问玩家在自定义大小的框中排列卡牌(观星、交换、拖拽选牌)
---@param player ServerPlayer @ 要询问的玩家
---@param skillname string @ 烧条技能名
---@param cardMap any @ { "牌堆1卡表", "牌堆2卡表", …… }
---@param prompt? string @ 操作提示
---@param box_size? integer @ 数值对应卡牌平铺张数的最大值为0则有单个卡位每张卡占100单位长度默认为7
---@param max_limit? integer[] @ 每一行牌上限 { 第一行, 第二行,…… },不填写则不限
---@param min_limit? integer[] @ 每一行牌下限 { 第一行, 第二行,…… },不填写则不限
---@param free_arrange? boolean @ 是否允许自由排列第一行卡的位置,默认不能
---@param pattern? string @ 控制第一行卡牌是否可以操作,不填写默认均可操作
---@param poxi_type? string @ 控制每张卡牌是否可以操作、确定键是否可以点击,不填写默认均可操作
---@param default_choice? table[] @ 超时的默认响应值在带poxi_type时需要填写
---@return table[]
function Room:askForArrangeCards(player, skillname, cardMap, prompt, free_arrange, box_size, max_limit, min_limit, pattern, poxi_type, default_choice)
prompt = prompt or ""
local areaNames = {}
if type(cardMap[1]) == "number" then
cardMap = {cardMap}
else
for i = #cardMap, 1, -1 do
if type(cardMap[i]) == "string" then
table.insert(areaNames, 1, cardMap[i])
table.remove(cardMap, i)
end
end
end
if #areaNames == 0 then
areaNames = {skillname, "toObtain"}
end
box_size = box_size or 7
max_limit = max_limit or {#cardMap[1], #cardMap > 1 and #cardMap[2] or #cardMap[1]}
min_limit = min_limit or {0, 0}
for _ = #cardMap + 1, #min_limit, 1 do
table.insert(cardMap, {})
end
pattern = pattern or "."
poxi_type = poxi_type or ""
local command = "AskForArrangeCards"
local data = {
cards = cardMap,
names = areaNames,
prompt = prompt,
size = box_size,
capacities = max_limit,
limits = min_limit,
is_free = free_arrange or false,
pattern = pattern or ".",
poxi_type = poxi_type or "",
cancelable = ((pattern ~= "." or poxi_type ~= "") and (default_choice == nil))
}
local result = self:doRequest(player, command, json.encode(data))
-- local result = player.room:askForCustomDialog(player, skillname,
-- "RoomElement/ArrangeCardsBox.qml", {
-- cardMap, prompt, box_size, max_limit, min_limit, free_arrange or false, areaNames,
-- pattern or ".", poxi_type or "", ((pattern ~= "." or poxi_type ~= "") and (default_choice == nil))
-- })
if result == "" then
if default_choice then return default_choice end
for j = 1, #min_limit, 1 do
if #cardMap[j] < min_limit[j] then
local cards = {table.connect(table.unpack(cardMap))}
if #min_limit > 1 then
for i = 2, #min_limit, 1 do
table.insert(cards, {})
if #cards[i] < min_limit[i] then
for _ = 1, min_limit[i] - #cards[i], 1 do
table.insert(cards[i], table.remove(cards[1], #cards[1] + #cards[i] - min_limit[i] + 1))
end
end
end
if #cards[1] > max_limit[1] then
for i = 2, #max_limit, 1 do
while #cards[i] < max_limit[i] do
table.insert(cards[i], table.remove(cards[1], max_limit[1] + 1))
if #cards[1] == max_limit[1] then return cards end
end
end
end
end
return cards
end
end
return cardMap
end
return json.decode(result)
end
-- TODO: guanxing type -- TODO: guanxing type
--- 询问玩家对若干牌进行观星。 --- 询问玩家对若干牌进行观星。
--- ---
@ -2003,9 +2114,15 @@ function Room:askForGuanxing(player, cards, top_limit, bottom_limit, customNotif
end end
local command = "AskForGuanxing" local command = "AskForGuanxing"
self:notifyMoveFocus(player, customNotify or command) self:notifyMoveFocus(player, customNotify or command)
local max_top = top_limit and top_limit[2] or #cards
local card_map = {table.slice(cards, 1, max_top + 1)}
if max_top < #cards then
table.insert(card_map, table.slice(cards, max_top))
end
local data = { local data = {
prompt = "", prompt = "",
cards = cards, is_free = true,
cards = card_map,
min_top_cards = top_limit and top_limit[1] or 0, min_top_cards = top_limit and top_limit[1] or 0,
max_top_cards = top_limit and top_limit[2] or #cards, max_top_cards = top_limit and top_limit[2] or #cards,
min_bottom_cards = bottom_limit and bottom_limit[1] or 0, min_bottom_cards = bottom_limit and bottom_limit[1] or 0,
@ -2034,7 +2151,7 @@ function Room:askForGuanxing(player, cards, top_limit, bottom_limit, customNotif
for i = #top, 1, -1 do for i = #top, 1, -1 do
table.insert(self.draw_pile, 1, top[i]) table.insert(self.draw_pile, 1, top[i])
end end
for i = 1, #bottom, -1 do for i = 1, #bottom, 1 do
table.insert(self.draw_pile, bottom[i]) table.insert(self.draw_pile, bottom[i])
end end
@ -2062,7 +2179,7 @@ function Room:askForExchange(player, piles, piles_name, customNotify)
if #piles_name ~= #piles then if #piles_name ~= #piles then
piles_name = {} piles_name = {}
for i, _ in ipairs(piles) do for i, _ in ipairs(piles) do
table.insert(piles_name, "Pile" .. i) table.insert(piles_name, Fk:translate("Pile") .. i)
end end
end end
self:notifyMoveFocus(player, customNotify or command) self:notifyMoveFocus(player, customNotify or command)
@ -2420,6 +2537,7 @@ end
-- Show a qml dialog and return qml's ClientInstance.replyToServer -- Show a qml dialog and return qml's ClientInstance.replyToServer
-- Do anything you like through this function -- Do anything you like through this function
-- 调用一个自定义对话框须自备loadData方法
---@param player ServerPlayer ---@param player ServerPlayer
---@param focustxt string ---@param focustxt string
---@param qmlPath string ---@param qmlPath string
@ -2434,6 +2552,7 @@ function Room:askForCustomDialog(player, focustxt, qmlPath, extra_data)
}) })
end end
--- 询问移动场上的一张牌
---@param player ServerPlayer @ 移动的操作 ---@param player ServerPlayer @ 移动的操作
---@param targetOne ServerPlayer @ 移动的目标1玩家 ---@param targetOne ServerPlayer @ 移动的目标1玩家
---@param targetTwo ServerPlayer @ 移动的目标2玩家 ---@param targetTwo ServerPlayer @ 移动的目标2玩家
@ -2731,15 +2850,16 @@ function Room:doCardUseEffect(cardUseEvent)
return return
end end
if self:getPlayerById(TargetGroup:getRealTargets(cardUseEvent.tos)[1]).dead then local target = TargetGroup:getRealTargets(cardUseEvent.tos)[1]
self:moveCards({ if not (self:getPlayerById(target).dead or table.contains((cardUseEvent.nullifiedTargets or Util.DummyTable), target)) then
ids = realCardIds, local existingEquipId
toArea = Card.DiscardPile, if cardUseEvent.toPutSlot and cardUseEvent.toPutSlot:startsWith("#EquipmentChoice") then
moveReason = fk.ReasonPutIntoDiscardPile, local index = cardUseEvent.toPutSlot:split(":")[2]
}) existingEquipId = self:getPlayerById(target):getEquipments(cardUseEvent.card.sub_type)[tonumber(index)]
else elseif not self:getPlayerById(target):hasEmptyEquipSlot(cardUseEvent.card.sub_type) then
local target = TargetGroup:getRealTargets(cardUseEvent.tos)[1] existingEquipId = self:getPlayerById(target):getEquipment(cardUseEvent.card.sub_type)
local existingEquipId = self:getPlayerById(target):getEquipment(cardUseEvent.card.sub_type) end
if existingEquipId then if existingEquipId then
self:moveCards( self:moveCards(
{ {
@ -2772,7 +2892,7 @@ function Room:doCardUseEffect(cardUseEvent)
end end
local target = TargetGroup:getRealTargets(cardUseEvent.tos)[1] local target = TargetGroup:getRealTargets(cardUseEvent.tos)[1]
if not self:getPlayerById(target).dead then if not (self:getPlayerById(target).dead or table.contains((cardUseEvent.nullifiedTargets or Util.DummyTable), target)) then
local findSameCard = false local findSameCard = false
for _, cardId in ipairs(self:getPlayerById(target):getCardIds(Player.Judge)) do for _, cardId in ipairs(self:getPlayerById(target):getCardIds(Player.Judge)) do
if Fk:getCardById(cardId).trueName == cardUseEvent.card.trueName then if Fk:getCardById(cardId).trueName == cardUseEvent.card.trueName then
@ -2783,6 +2903,13 @@ function Room:doCardUseEffect(cardUseEvent)
if not findSameCard then if not findSameCard then
if cardUseEvent.card:isVirtual() then if cardUseEvent.card:isVirtual() then
self:getPlayerById(target):addVirtualEquip(cardUseEvent.card) self:getPlayerById(target):addVirtualEquip(cardUseEvent.card)
elseif cardUseEvent.card.name ~= Fk:getCardById(cardUseEvent.card.id, true).name then
local card = Fk:cloneCard(cardUseEvent.card.name)
card.skillNames = cardUseEvent.card.skillNames
card:addSubcard(cardUseEvent.card.id)
self:getPlayerById(target):addVirtualEquip(card)
else
self:getPlayerById(target):removeVirtualEquip(cardUseEvent.card.id)
end end
self:moveCards({ self:moveCards({
@ -2796,12 +2923,6 @@ function Room:doCardUseEffect(cardUseEvent)
end end
end end
self:moveCards({
ids = realCardIds,
toArea = Card.DiscardPile,
moveReason = fk.ReasonPutIntoDiscardPile,
})
return return
end end
@ -3091,34 +3212,16 @@ end
--- 让一名玩家获得一张牌 --- 让一名玩家获得一张牌
---@param player integer|ServerPlayer @ 要拿牌的玩家 ---@param player integer|ServerPlayer @ 要拿牌的玩家
---@param cid integer|Card|integer[] @ 要拿到的卡牌 ---@param card integer|integer[]|Card|Card[] @ 要拿到的卡牌
---@param unhide? boolean @ 是否明着拿 ---@param unhide? boolean @ 是否明着拿
---@param reason? CardMoveReason @ 卡牌移动的原因 ---@param reason? CardMoveReason @ 卡牌移动的原因
---@param proposer? integer @ 移动操作者的id ---@param proposer? integer @ 移动操作者的id
function Room:obtainCard(player, cid, unhide, reason, proposer) ---@param skill_name? string @ 技能名
if type(cid) ~= "number" then ---@param moveMark? table|string @ 移动后自动赋予标记,格式:{标记名(支持-inarea后缀移出值代表区域后清除), 值}
assert(cid and type(cid) == "table") ---@param visiblePlayers? integer|integer[] @ 控制移动对特定角色可见在moveVisible为false时生效
if cid[1] == nil then function Room:obtainCard(player, card, unhide, reason, proposer, skill_name, moveMark, visiblePlayers)
cid = cid:isVirtual() and cid.subcards or {cid.id} local pid = type(player) == "number" and player or player.id
end self:moveCardTo(card, Card.PlayerHand, player, reason, skill_name, nil, unhide, proposer or pid, moveMark, visiblePlayers)
else
cid = {cid}
end
if #cid == 0 then return end
if type(player) == "table" then
player = player.id
end
self:moveCards({
ids = cid,
from = self.owner_map[cid[1]],
to = player,
toArea = Card.PlayerHand,
moveReason = reason or fk.ReasonJustMove,
proposer = proposer or player,
moveVisible = unhide or false,
})
end end
--- 让玩家摸牌 --- 让玩家摸牌
@ -3126,8 +3229,9 @@ end
---@param num integer @ 摸牌数 ---@param num integer @ 摸牌数
---@param skillName? string @ 技能名 ---@param skillName? string @ 技能名
---@param fromPlace? string @ 摸牌的位置,"top" 或者 "bottom" ---@param fromPlace? string @ 摸牌的位置,"top" 或者 "bottom"
---@param moveMark? table|string @ 移动后自动赋予标记,格式:{标记名(支持-inarea后缀移出值代表区域后清除), 值}
---@return integer[] @ 摸到的牌 ---@return integer[] @ 摸到的牌
function Room:drawCards(player, num, skillName, fromPlace) function Room:drawCards(player, num, skillName, fromPlace, moveMark)
local drawData = { local drawData = {
who = player, who = player,
num = num, num = num,
@ -3150,6 +3254,7 @@ function Room:drawCards(player, num, skillName, fromPlace)
moveReason = fk.ReasonDraw, moveReason = fk.ReasonDraw,
proposer = player.id, proposer = player.id,
skillName = skillName, skillName = skillName,
moveMark = moveMark,
}) })
return { table.unpack(topCards) } return { table.unpack(topCards) }
@ -3158,13 +3263,15 @@ end
--- 将一张或多张牌移动到某处 --- 将一张或多张牌移动到某处
---@param card integer | integer[] | Card | Card[] @ 要移动的牌 ---@param card integer | integer[] | Card | Card[] @ 要移动的牌
---@param to_place integer @ 移动的目标位置 ---@param to_place integer @ 移动的目标位置
---@param target? ServerPlayer @ 移动的目标角色 ---@param target? ServerPlayer|integer @ 移动的目标角色
---@param reason? integer @ 移动时使用的移牌原因 ---@param reason? integer @ 移动时使用的移牌原因
---@param skill_name? string @ 技能名 ---@param skill_name? string @ 技能名
---@param special_name? string @ 私人牌堆名 ---@param special_name? string @ 私人牌堆名
---@param visible? boolean @ 是否明置 ---@param visible? boolean @ 是否明置
---@param proposer? integer @ 移动操作者的id ---@param proposer? integer @ 移动操作者的id
function Room:moveCardTo(card, to_place, target, reason, skill_name, special_name, visible, proposer) ---@param moveMark? table|string @ 移动后自动赋予标记,格式:{标记名(支持-inarea后缀移出值代表区域后清除), 值}
---@param visiblePlayers? integer|integer[] @ 控制移动对特定角色可见在moveVisible为false时生效
function Room:moveCardTo(card, to_place, target, reason, skill_name, special_name, visible, proposer, moveMark, visiblePlayers)
reason = reason or fk.ReasonJustMove reason = reason or fk.ReasonJustMove
skill_name = skill_name or "" skill_name = skill_name or ""
special_name = special_name or "" special_name = special_name or ""
@ -3174,7 +3281,12 @@ function Room:moveCardTo(card, to_place, target, reason, skill_name, special_nam
if table.contains( if table.contains(
{Card.PlayerEquip, Card.PlayerHand, {Card.PlayerEquip, Card.PlayerHand,
Card.PlayerJudge, Card.PlayerSpecial}, to_place) then Card.PlayerJudge, Card.PlayerSpecial}, to_place) then
to = target.id assert(target)
if type(target) == "number" then
to = target
else
to = target.id
end
end end
local movesSplitedByOwner = {} local movesSplitedByOwner = {}
@ -3196,6 +3308,8 @@ function Room:moveCardTo(card, to_place, target, reason, skill_name, special_nam
specialName = special_name, specialName = special_name,
moveVisible = visible, moveVisible = visible,
proposer = proposer, proposer = proposer,
moveMark = moveMark,
visiblePlayers = visiblePlayers,
}) })
end end
end end
@ -3559,6 +3673,10 @@ function Room:recastCard(card_ids, who, skillName)
moveReason = fk.ReasonRecast, moveReason = fk.ReasonRecast,
proposer = who.id proposer = who.id
}) })
self:sendFootnote(card_ids, {
type = "##RecastCard",
from = who.id,
})
self:broadcastPlaySound("./audio/system/recast") self:broadcastPlaySound("./audio/system/recast")
self:sendLog{ self:sendLog{
type = skillName == "recast" and "#Recast" or "#RecastBySkill", type = skillName == "recast" and "#Recast" or "#RecastBySkill",
@ -3712,6 +3830,7 @@ end
---@param winner string @ 获胜的身份,空字符串表示平局 ---@param winner string @ 获胜的身份,空字符串表示平局
function Room:gameOver(winner) function Room:gameOver(winner)
if not self.game_started then return end if not self.game_started then return end
self.room:destroyRequestTimer()
if table.contains( if table.contains(
{ "running", "normal" }, { "running", "normal" },
@ -3727,6 +3846,7 @@ function Room:gameOver(winner)
self:broadcastProperty(p, "role") self:broadcastProperty(p, "role")
end end
self:doBroadcastNotify("GameOver", winner) self:doBroadcastNotify("GameOver", winner)
fk.qInfo(string.format("[GameOver] %d, %s, %s, in %ds", self.id, self.settings.gameMode, winner, os.time() - self.start_time))
if shouldUpdateWinRate(self) then if shouldUpdateWinRate(self) then
for _, p in ipairs(self.players) do for _, p in ipairs(self.players) do
@ -3990,6 +4110,52 @@ function Room:resumePlayerArea(player, playerSlots)
end end
end end
---@param player ServerPlayer
---@param playerSlots string | string[]
function Room:addPlayerEquipSlots(player, playerSlots)
assert(type(playerSlots) == "string" or type(playerSlots) == "table")
if type(playerSlots) == "string" then
playerSlots = { playerSlots }
end
for _, slot in ipairs(playerSlots) do
local slotIndex = table.indexOf(player.equipSlots, slot)
if slotIndex > -1 then
table.insert(player.equipSlots, slotIndex, slot)
else
table.insert(player.equipSlots, slot)
end
end
self:broadcastProperty(player, "equipSlots")
end
---@param player ServerPlayer
---@param playerSlots string | string[]
function Room:removePlayerEquipSlots(player, playerSlots)
assert(type(playerSlots) == "string" or type(playerSlots) == "table")
if type(playerSlots) == "string" then
playerSlots = { playerSlots }
end
for _, slot in ipairs(playerSlots) do
table.removeOne(player.equipSlots, slot)
end
self:broadcastProperty(player, "equipSlots")
end
---@param player ServerPlayer
---@param playerSlots string[]
function Room:setPlayerEquipSlots(player, playerSlots)
assert(type(playerSlots) == "table")
player.equipSlots = playerSlots
self:broadcastProperty(player, "equipSlots")
end
--- 设置休整 --- 设置休整
---@param player ServerPlayer ---@param player ServerPlayer
---@param roundNum integer ---@param roundNum integer

View File

@ -2,49 +2,19 @@
local Room = require "server.room" local Room = require "server.room"
--[[
local verbose = function(...)
printf(...)
end
--]]
-- 所有当前正在运行的房间(即游戏尚未结束的房间) -- 所有当前正在运行的房间(即游戏尚未结束的房间)
---@type table<integer, Room> ---@type table<integer, Room>
local runningRooms = {} local runningRooms = {}
-- 所有处于就绪态的房间以及request协程如果就绪的话
---@type Room[]
local readyRooms = {}
local requestCo = coroutine.create(function(room)
require "server.request"(room)
end)
-- 仿照Room接口编写的request协程处理器 -- 仿照Room接口编写的request协程处理器
local requestRoom = setmetatable({ local requestRoom = setmetatable({
id = -1, id = -1,
runningRooms = runningRooms, runningRooms = runningRooms,
-- minDelayTime 是当没有任何就绪房间时,可以睡眠的时间。
-- 因为这个时间是所有房间预期就绪用时的最小值故称为minDelayTime。
minDelayTime = -1,
getRoom = function(_, roomId) getRoom = function(_, roomId)
return runningRooms[roomId] return runningRooms[roomId]
end, end,
resume = function(self)
local err, msg = coroutine.resume(requestCo, self)
if err == false then
fk.qCritical(msg .. "\n" .. debug.traceback(requestCo))
end
return nil, 0
end,
isReady = function(self)
return self.thread:hasRequest()
end,
registerRoom = function(self, id) registerRoom = function(self, id)
local cRoom = self.thread:getRoom(id) local cRoom = self.thread:getRoom(id)
local room = Room:new(cRoom) local room = Room:new(cRoom)
@ -59,116 +29,44 @@ local requestRoom = setmetatable({
runningRooms[-1] = requestRoom runningRooms[-1] = requestRoom
-- 从所有运行中房间中挑出就绪的房间。
-- 方法暂时就是最简单的遍历。
local function refreshReadyRooms()
-- verbose '[+] Refreshing ready queue...'
for k, v in pairs(runningRooms) do
local ready, rest = v:isReady()
if ready then
table.insertIfNeed(readyRooms, v)
elseif rest and rest >= 0 then
local time = requestRoom.minDelayTime
time = math.min((time <= 0 and 9999999 or time), rest)
requestRoom.minDelayTime = math.ceil(time)
end
end
-- verbose('[+] now have %d ready rooms...', #readyRooms)
end
-- 主循环。只要线程没有被杀掉,就一直循环下去。
-- 函数每轮循环会从队列中取一个元素并交给控制权,
-- 如果没有,则尝试刷新队列,无法刷新则开始睡眠。
local function mainLoop()
-- request协程的专用特判变量。因为处理request不应当重置睡眠时长
local rest_sleep_time
while not requestRoom.thread:isTerminated() do
local room = table.remove(readyRooms, 1)
if room then
-- verbose '============= LOOP =============='
-- verbose('[*] Switching to %s...', tostring(room))
RoomInstance = (room ~= requestRoom and room or nil)
local over, rest = room:resume()
RoomInstance = nil
if over then
-- verbose('[#] %s is finished, removing ...', tostring(room))
for _, e in ipairs(room.logic.game_event_stack.t) do
coroutine.close(e._co)
end
for _, e in ipairs(room.logic.cleaner_stack.t) do
coroutine.close(e._co)
end
room.logic = nil
runningRooms[room.id] = nil
else
local time = requestRoom.minDelayTime
if room == requestRoom then
rest = rest_sleep_time
end
if rest and rest >= 0 then
time = math.min((time <= 0 and 9999999 or time), rest)
else
time = -1
end
requestRoom.minDelayTime = math.ceil(time)
-- verbose("[+] minDelay is %d ms...", requestRoom.minDelayTime)
-- verbose('[-] %s successfully yielded, %d ready rooms left...',
-- tostring(room), #readyRooms)
end
else
refreshReadyRooms()
if #readyRooms == 0 then
refreshReadyRooms()
if #readyRooms == 0 then
local time = requestRoom.minDelayTime
-- verbose('[.] Sleeping for %d ms...', time)
local cur = os.getms()
time = math.min((time <= 0 and 9999999 or time), 200)
-- 调用RoomThread的trySleep函数开始真正的睡眠。会被wakeUp(c++)唤醒。
requestRoom.thread:trySleep(time)
local runningRoomsCount = -1 -- 必有requestRoom从-1开始算
for _ in pairs(runningRooms) do
runningRoomsCount = runningRoomsCount + 1
if runningRoomsCount > 0 then break end
end
if runningRoomsCount == 0 and requestRoom.thread:isOutdated() then
break
end
-- verbose('[!] Waked up after %f ms...', (os.getms() - cur) / 1000)
if time > 0 then
rest_sleep_time = math.floor(time - (os.getms() - cur) / 1000)
else
rest_sleep_time = -1
end
requestRoom.minDelayTime = -1
end
end
end
end
-- verbose '=========== LOOP END ============'
-- verbose '[:)] Goodbye!'
end
-- 当Cpp侧的RoomThread运行时以下这个函数就是这个线程的主函数。 -- 当Cpp侧的RoomThread运行时以下这个函数就是这个线程的主函数。
-- 而这个函数里面又调用了上面的mainLoop。 -- 而这个函数里面又调用了上面的mainLoop。
function InitScheduler(_thread) function InitScheduler(_thread)
requestRoom.thread = _thread requestRoom.thread = _thread
Pcall(mainLoop) -- Pcall(mainLoop)
end end
function IsConsoleStart() function IsConsoleStart()
return requestRoom.thread:isConsoleStart() return requestRoom.thread:isConsoleStart()
end end
local Req = require "server.request"
function HandleRequest(req)
Req(requestRoom, req)
return true
end
function ResumeRoom(roomId)
local room = requestRoom:getRoom(roomId)
if not room then return false end
if not room:isReady() then return false end
RoomInstance = (room ~= requestRoom and room or nil)
local over = room:resume()
RoomInstance = nil
if over then
for _, e in ipairs(room.logic.game_event_stack.t) do
coroutine.close(e._co)
end
for _, e in ipairs(room.logic.cleaner_stack.t) do
coroutine.close(e._co)
end
room.logic = nil
runningRooms[room.id] = nil
end
return over
end
if FileIO.pwd():endsWith("packages/freekill-core") then if FileIO.pwd():endsWith("packages/freekill-core") then
FileIO.cd("../..") FileIO.cd("../..")
end end

View File

@ -53,17 +53,25 @@ end
---@param command string ---@param command string
---@param jsonData string ---@param jsonData string
function ServerPlayer:doNotify(command, jsonData) function ServerPlayer:doNotify(command, jsonData)
local room = self.room
for _, p in ipairs(self._observers) do for _, p in ipairs(self._observers) do
if p:getState() ~= fk.Player_Robot then
room.notify_count = room.notify_count + 1
end
p:doNotify(command, jsonData) p:doNotify(command, jsonData)
end end
local room = self.room
for _, t in ipairs(room.observers) do for _, t in ipairs(room.observers) do
local id, p = table.unpack(t) local id, p = table.unpack(t)
if id == self.id and room.room:hasObserver(p) then if id == self.id and room.room:hasObserver(p) then
p:doNotify(command, jsonData) p:doNotify(command, jsonData)
end end
end end
if room.notify_count >= room.notify_max and
coroutine.status(room.main_co) == "normal" then
room:delay(100)
end
end end
--- Send a request to client, and allow client to reply within *timeout* seconds. --- Send a request to client, and allow client to reply within *timeout* seconds.
@ -155,6 +163,16 @@ local function _waitForReply(player, timeout)
end end
end end
--- 发送一句聊天
---@param msg string
function ServerPlayer:chat(msg)
self.room:doBroadcastNotify("Chat", json.encode {
type = 2,
sender = self.id,
msg = msg,
})
end
--- Wait for at most *timeout* seconds for reply from client. --- Wait for at most *timeout* seconds for reply from client.
--- ---
--- If *timeout* is negative or **nil**, the function will wait forever until get reply. --- If *timeout* is negative or **nil**, the function will wait forever until get reply.
@ -369,7 +387,7 @@ function ServerPlayer:changePhase(from_phase, to_phase)
table.remove(self.phases, 1) table.remove(self.phases, 1)
end end
GameEvent(GameEvent.Phase, self, self.phase):exec() GameEvent.Phase:create(self, self.phase):exec()
return false return false
end end
@ -412,7 +430,7 @@ function ServerPlayer:gainAnExtraPhase(phase, delay)
arg = phase_name_table[phase], arg = phase_name_table[phase],
} }
GameEvent(GameEvent.Phase, self, self.phase):exec() GameEvent.Phase:create(self, self.phase):exec()
phase_change = { phase_change = {
from = phase, from = phase,
@ -491,7 +509,7 @@ function ServerPlayer:play(phase_table)
end end
if (not skip) or (cancel_skip) then if (not skip) or (cancel_skip) then
GameEvent(GameEvent.Phase, self, self.phase):exec() GameEvent.Phase:create(self, self.phase):exec()
else else
room:sendLog{ room:sendLog{
type = "#PhaseSkipped", type = "#PhaseSkipped",
@ -554,7 +572,7 @@ function ServerPlayer:gainAnExtraTurn(delay, skillName)
local ex_tag = self.tag["_extra_turn_count"] local ex_tag = self.tag["_extra_turn_count"]
table.insert(ex_tag, skillName) table.insert(ex_tag, skillName)
GameEvent(GameEvent.Turn, self):exec() GameEvent.Turn:create(self):exec()
table.remove(ex_tag) table.remove(ex_tag)
@ -581,18 +599,21 @@ end
---@param num integer @ 摸牌数 ---@param num integer @ 摸牌数
---@param skillName? string @ 技能名 ---@param skillName? string @ 技能名
---@param fromPlace? string @ 摸牌的位置,"top" 或者 "bottom" ---@param fromPlace? string @ 摸牌的位置,"top" 或者 "bottom"
---@param moveMark? table|string @ 移动后自动赋予标记,格式:{标记名(支持-inarea后缀移出值代表区域后清除), 值}
---@return integer[] @ 摸到的牌 ---@return integer[] @ 摸到的牌
function ServerPlayer:drawCards(num, skillName, fromPlace) function ServerPlayer:drawCards(num, skillName, fromPlace, moveMark)
return self.room:drawCards(self, num, skillName, fromPlace) return self.room:drawCards(self, num, skillName, fromPlace, moveMark)
end end
---@param pile_name string ---@param pile_name string
---@param card integer|Card ---@param card integer | integer[] | Card | Card[]
---@param visible? boolean ---@param visible? boolean
---@param skillName? string ---@param skillName? string
function ServerPlayer:addToPile(pile_name, card, visible, skillName) ---@param proposer? integer
local room = self.room ---@param visiblePlayers? integer | integer[] @ 为nil时默认对自己可见
room:moveCardTo(card, Card.PlayerSpecial, self, fk.ReasonJustMove, skillName, pile_name, visible) function ServerPlayer:addToPile(pile_name, card, visible, skillName, proposer, visiblePlayers)
self.room:moveCardTo(card, Card.PlayerSpecial, self, fk.ReasonJustMove, skillName, pile_name, visible,
proposer or self.id, nil, visiblePlayers)
end end
function ServerPlayer:bury() function ServerPlayer:bury()
@ -637,6 +658,7 @@ function ServerPlayer:clearPiles()
end end
function ServerPlayer:addVirtualEquip(card) function ServerPlayer:addVirtualEquip(card)
self:removeVirtualEquip(card:getEffectiveId())
Player.addVirtualEquip(self, card) Player.addVirtualEquip(self, card)
self.room:doBroadcastNotify("AddVirtualEquip", json.encode{ self.room:doBroadcastNotify("AddVirtualEquip", json.encode{
player = self.id, player = self.id,
@ -647,10 +669,12 @@ end
function ServerPlayer:removeVirtualEquip(cid) function ServerPlayer:removeVirtualEquip(cid)
local ret = Player.removeVirtualEquip(self, cid) local ret = Player.removeVirtualEquip(self, cid)
self.room:doBroadcastNotify("RemoveVirtualEquip", json.encode{ if ret then
player = self.id, self.room:doBroadcastNotify("RemoveVirtualEquip", json.encode{
id = cid, player = self.id,
}) id = cid,
})
end
return ret return ret
end end

View File

@ -15,7 +15,8 @@
---@field public specialName? string @ 若终点区域为PlayerSpecial则存至对应私人牌堆内 ---@field public specialName? string @ 若终点区域为PlayerSpecial则存至对应私人牌堆内
---@field public specialVisible? boolean @ 控制上述创建私人牌堆后是否令其可见 ---@field public specialVisible? boolean @ 控制上述创建私人牌堆后是否令其可见
---@field public drawPilePosition? integer @ 移至牌堆的索引位置,值为-1代表置入牌堆底或者牌堆牌数+1也为牌堆底 ---@field public drawPilePosition? integer @ 移至牌堆的索引位置,值为-1代表置入牌堆底或者牌堆牌数+1也为牌堆底
---@field public moveMark? table @ 移动后自动赋予标记,格式:{标记名(支持-inarea后缀移出值代表区域后清除), 值} ---@field public moveMark? table|string @ 移动后自动赋予标记,格式:{标记名(支持-inarea后缀移出值代表区域后清除), 值}
---@field public visiblePlayers? integer|integer[] @ 控制移动对特定角色可见在moveVisible为false时生效
--- MoveInfo 一张牌的来源信息 --- MoveInfo 一张牌的来源信息
---@class MoveInfo ---@class MoveInfo
@ -36,7 +37,8 @@
---@field public specialName? string @ 若终点区域为PlayerSpecial则存至对应私人牌堆内 ---@field public specialName? string @ 若终点区域为PlayerSpecial则存至对应私人牌堆内
---@field public specialVisible? boolean @ 控制上述创建私人牌堆后是否令其可见 ---@field public specialVisible? boolean @ 控制上述创建私人牌堆后是否令其可见
---@field public drawPilePosition? integer @ 移至牌堆的索引位置,值为-1代表置入牌堆底或者牌堆牌数+1也为牌堆底 ---@field public drawPilePosition? integer @ 移至牌堆的索引位置,值为-1代表置入牌堆底或者牌堆牌数+1也为牌堆底
---@field public moveMark? table @ 移动后自动赋予标记,格式:{标记名(支持-inarea后缀移出值代表区域后清除), 值} ---@field public moveMark? table|string @ 移动后自动赋予标记,格式:{标记名(支持-inarea后缀移出值代表区域后清除), 值}
---@field public visiblePlayers? integer|integer[] @ 控制移动对特定角色可见在moveVisible为false时生效
--- PindianResult 拼点结果 --- PindianResult 拼点结果
---@class PindianResult ---@class PindianResult

View File

@ -2,21 +2,21 @@ return {
["maneuvering"] = "Maneuvering", ["maneuvering"] = "Maneuvering",
["thunder__slash"] = "Thunder Slash", ["thunder__slash"] = "Thunder Slash",
[":thunder__slash"] = "Thunder Slash (basic card)<br /><b>Phase</b>: Action phase<br /><b>Target</b>: Another player within your ATK range<br /><b>Effect</b>: Deal 1 Thunder DMG to the targets.<br/><b>Note</b>: You can only use 1 Slash per action phase.", [":thunder__slash"] = "Thunder Slash (basic card)<br /><b>Phase</b>: Action phase<br /><b>Target</b>: Another player within your ATK range<br /><b>Effect</b>: Deal 1 Thunder DMG to the targets.<br/><b>Note</b>: You can only use 1 Slash per action phase.",
["#thunder__slash_skill"] = "Choose 1 player within your ATK range, deal 1 Thunder DMG to him", ["#thunder__slash_skill"] = "Choose 1 player within your ATK range, deal 1 Thunder DMG to him",
["#thunder__slash_skill_multi"] = "Choose up to %arg players within your ATK range. Deal 1 Thunder DMG to them", ["#thunder__slash_skill_multi"] = "Choose up to %arg players within your ATK range. Deal 1 Thunder DMG to them",
["fire__slash"] = "Fire Slash", ["fire__slash"] = "Fire Slash",
[":fire__slash"] = "Fire Slash (basic card)<br /><b>Phase</b>: Action phase<br /><b>Target</b>: Another player within your ATK range<br /><b>Effect</b>: Deal 1 Fire DMG to the targets.<br/><b>Note</b>: You can only use 1 Slash per action phase.", [":fire__slash"] = "Fire Slash (basic card)<br /><b>Phase</b>: Action phase<br /><b>Target</b>: Another player within your ATK range<br /><b>Effect</b>: Deal 1 Fire DMG to the targets.<br/><b>Note</b>: You can only use 1 Slash per action phase.",
["#fire__slash_skill"] = "Choose 1 player within your ATK range, deal 1 Fire DMG to him", ["#fire__slash_skill"] = "Choose 1 player within your ATK range, deal 1 Fire DMG to him",
["#fire__slash_skill_multi"] = "Choose up to %arg players within your ATK range. Deal 1 Fire DMG to them", ["#fire__slash_skill_multi"] = "Choose up to %arg players within your ATK range. Deal 1 Fire DMG to them",
["analeptic"] = "Alcohol", ["analeptic"] = "Alcohol",
[":analeptic"] = "Alcohol (basic card)<br /><b>Phase</b>: 1. Action phase 2. When you are dying<br /><b>Target</b>: Yourself<br /><b>Effect</b>: 1. the DMG of the next Slash you use this turn is increased by +1. (This effect can only be used once per turn) 2. You heal 1 HP.", [":analeptic"] = "Alcohol (basic card)<br /><b>Phase</b>: 1. Action phase 2. When you are dying<br /><b>Target</b>: Yourself<br /><b>Effect</b>: 1. the DMG of the next Slash you use this turn is increased by +1. (This effect can only be used once per turn) 2. You heal 1 HP.",
["#analeptic_skill"] = "the DMG of the next Slash you use this turn is increased by +1", ["#analeptic_skill"] = "the DMG of the next Slash you use this turn is increased by +1",
["iron_chain"] = "Iron Chain", ["iron_chain"] = "Iron Chain",
[":iron_chain"] = "Iron Chain (trick card)<br /><b>Phase</b>: Action phase<br /><b>Target</b>: 1~2 players<br /><b>Effect</b>: Change chain state of the targets.", [":iron_chain"] = "Iron Chain (trick card)<br /><b>Phase</b>: Action phase<br /><b>Target</b>: 1~2 players<br /><b>Effect</b>: Change chain state of the targets.",
["#iron_chain_skill"] = "Choose 1~2 players. Change their chain states", ["#iron_chain_skill"] = "Choose 1~2 players. Change their chain states",
["_normal_use"] = "Normally use", ["_normal_use"] = "Normally use",
["recast"] = "Recast", ["recast"] = "Recast",
@ -25,29 +25,29 @@ return {
["fire_attack"] = "Fire Attack", ["fire_attack"] = "Fire Attack",
["fire_attack_skill"] = "Fire Attack", ["fire_attack_skill"] = "Fire Attack",
[":fire_attack"] = "Fire Attack (trick card)<br /><b>Phase</b>: Action phase<br /><b>Target</b>: A player with hand cards<br /><b>Effect</b>: The target player shows 1 hand card; then, if you discard 1 card with the same suit, you deal 1 Fire DMG to him.", [":fire_attack"] = "Fire Attack (trick card)<br /><b>Phase</b>: Action phase<br /><b>Target</b>: A player with hand cards<br /><b>Effect</b>: The target player shows 1 hand card; then, if you discard 1 card with the same suit, you deal 1 Fire DMG to him.",
["#fire_attack-show"] = "%src used Fire Attack to you, please show 1 hand card", ["#fire_attack-show"] = "%src used Fire Attack to you, please show 1 hand card",
["#fire_attack-discard"] = "You can discard 1 %arg hand card, then deal 1 Fire DMG to %src", ["#fire_attack-discard"] = "You can discard 1 %arg hand card, then deal 1 Fire DMG to %src",
["#fire_attack_skill"] = "Choose a player with hand cards. He shows 1 hand card;<br />then, if you discard 1 card with the same suit, you deal 1 Fire DMG to him", ["#fire_attack_skill"] = "Choose a player with hand cards. He shows 1 hand card;<br />then, if you discard 1 card with the same suit, you deal 1 Fire DMG to him",
["supply_shortage"] = "Supply Shortage", ["supply_shortage"] = "Supply Shortage",
[":supply_shortage"] = "Supply Shortage (delayed trick card)<br /><b>Phase</b>: Action phase<br /><b>Target</b>: Another player at distance 1<br /><b>Effect</b>: Place this card in target's judgement area. He performs a judgement in his judge phase: if result is not ♣, he skips his draw phase.", [":supply_shortage"] = "Supply Shortage (delayed trick card)<br /><b>Phase</b>: Action phase<br /><b>Target</b>: Another player at distance 1<br /><b>Effect</b>: Place this card in target's judgement area. He performs a judgement in his judge phase: if result is not ♣, he skips his draw phase.",
["#supply_shortage_skill"] = "Place this card in another player's judgement area. He performs a judgement in his judge phase:<br />If result is not ♣, he skips his draw phase", ["#supply_shortage_skill"] = "Place this card in another player's judgement area. He performs a judgement in his judge phase:<br />If result is not ♣, he skips his draw phase",
["guding_blade"] = "Ancient Scimitar", ["guding_blade"] = "Ancient Scimitar",
[":guding_blade"] = "Ancient Scimitar (equip card, weapon)<br /><b>ATK range</b>: 2<br /><b>Weapon skill</b>: When your used Slash is about to cause DMG, if the target player has no hand cards: the DMG is increased by +1.", [":guding_blade"] = "Ancient Scimitar (equip card, weapon)<br /><b>ATK range</b>: 2<br /><b>Weapon skill</b>: When your used Slash is about to cause DMG, if the target player has no hand cards: the DMG is increased by +1.",
["#guding_blade_skill"] = "Ancient Scimitar", ["#guding_blade_skill"] = "Ancient Scimitar",
["fan"] = "Fan", ["fan"] = "Fan",
[":fan"] = "Fan (equip card, weapon)<br /><b>ATK range</b>: 4<br /><b>Weapon skill</b>: You can use any basic Slash as Fire Slash.", [":fan"] = "Fan (equip card, weapon)<br /><b>ATK range</b>: 4<br /><b>Weapon skill</b>: You can use any basic Slash as Fire Slash.",
["#fan_skill"] = "Fan", ["#fan_skill"] = "Fan",
["vine"] = "Vine", ["vine"] = "Vine",
[":vine"] = "Vine (equip card, armor)<br /><b>Armor skill</b>: Savage Assault, Archery Attack and basic Slash have no effect on you. When you are about to suffer Fire DMG, the DMG is increased by +1.", [":vine"] = "Vine (equip card, armor)<br /><b>Armor skill</b>: Savage Assault, Archery Attack and basic Slash have no effect on you. When you are about to suffer Fire DMG, the DMG is increased by +1.",
["#vine_skill"] = "Vine", ["#vine_skill"] = "Vine",
["silver_lion"] = "Sliver Lion", ["silver_lion"] = "Sliver Lion",
[":silver_lion"] = "Sliver Lion (equip card, armor)<br /><b>Armor skill</b>: When you are about to suffer DMG: that DMG is reduced to 1. When you lose this card in your equipment area: you heal 1 HP.", [":silver_lion"] = "Sliver Lion (equip card, armor)<br /><b>Armor skill</b>: When you are about to suffer DMG: that DMG is reduced to 1. When you lose this card in your equipment area: you heal 1 HP.",
["#silver_lion_skill"] = "Sliver Lion", ["#silver_lion_skill"] = "Sliver Lion",
["hualiu"] = "Hua Liu", ["hualiu"] = "Hua Liu",

View File

@ -136,7 +136,7 @@ local analepticSkill = fk.CreateActiveSkill{
card = effect.card, card = effect.card,
}) })
else else
to.drank = to.drank + 1 to.drank = to.drank + 1 + ((effect.extra_data or {}).additionalDrank or 0)
room:broadcastProperty(to, "drank") room:broadcastProperty(to, "drank")
end end
end end
@ -497,21 +497,21 @@ Fk:loadTranslationTable{
["maneuvering"] = "军争", ["maneuvering"] = "军争",
["thunder__slash"] = "雷杀", ["thunder__slash"] = "雷杀",
[":thunder__slash"] = "基本牌<br /><b>时机</b>:出牌阶段<br /><b>目标</b>:攻击范围内的一名角色<br /><b>效果</b>对目标角色造成1点雷电伤害。", [":thunder__slash"] = "基本牌<br /><b>时机</b>:出牌阶段<br /><b>目标</b>:攻击范围内的一名角色<br /><b>效果</b>对目标角色造成1点雷电伤害。",
["#thunder__slash_skill"] = "选择攻击范围内的一名角色对其造成1点雷电伤害", ["#thunder__slash_skill"] = "选择攻击范围内的一名角色对其造成1点雷电伤害",
["#thunder__slash_skill_multi"] = "选择攻击范围内的至多%arg名角色对这些角色各造成1点雷电伤害", ["#thunder__slash_skill_multi"] = "选择攻击范围内的至多%arg名角色对这些角色各造成1点雷电伤害",
["fire__slash"] = "火杀", ["fire__slash"] = "火杀",
[":fire__slash"] = "基本牌<br /><b>时机</b>:出牌阶段<br /><b>目标</b>:攻击范围内的一名角色<br /><b>效果</b>对目标角色造成1点火焰伤害。", [":fire__slash"] = "基本牌<br /><b>时机</b>:出牌阶段<br /><b>目标</b>:攻击范围内的一名角色<br /><b>效果</b>对目标角色造成1点火焰伤害。",
["#fire__slash_skill"] = "选择攻击范围内的一名角色对其造成1点火焰伤害", ["#fire__slash_skill"] = "选择攻击范围内的一名角色对其造成1点火焰伤害",
["#fire__slash_skill_multi"] = "选择攻击范围内的至多%arg名角色对这些角色各造成1点火焰伤害", ["#fire__slash_skill_multi"] = "选择攻击范围内的至多%arg名角色对这些角色各造成1点火焰伤害",
["analeptic"] = "", ["analeptic"] = "",
[":analeptic"] = "基本牌<br /><b>时机</b>:出牌阶段/你处于濒死状态时<br /><b>目标</b>:你<br /><b>效果</b>:目标角色本回合使用的下一张【杀】将要造成的伤害+1/目标角色回复1点体力。", [":analeptic"] = "基本牌<br /><b>时机</b>:出牌阶段/你处于濒死状态时<br /><b>目标</b>:你<br /><b>效果</b>:目标角色本回合使用的下一张【杀】将要造成的伤害+1/目标角色回复1点体力。",
["#analeptic_skill"] = "你于此回合内使用的下一张【杀】的伤害值基数+1", ["#analeptic_skill"] = "你于此回合内使用的下一张【杀】的伤害值基数+1",
["iron_chain"] = "铁锁连环", ["iron_chain"] = "铁锁连环",
[":iron_chain"] = "锦囊牌<br /><b>时机</b>:出牌阶段<br /><b>目标</b>:一至两名角色<br /><b>效果</b>:横置或重置目标角色的武将牌。", [":iron_chain"] = "锦囊牌<br /><b>时机</b>:出牌阶段<br /><b>目标</b>:一至两名角色<br /><b>效果</b>:横置或重置目标角色的武将牌。",
["#iron_chain_skill"] = "选择一至两名角色,这些角色横置或重置", ["#iron_chain_skill"] = "选择一至两名角色,这些角色横置或重置",
["_normal_use"] = "正常使用", ["_normal_use"] = "正常使用",
["recast"] = "重铸", ["recast"] = "重铸",
@ -520,29 +520,29 @@ Fk:loadTranslationTable{
["fire_attack"] = "火攻", ["fire_attack"] = "火攻",
["fire_attack_skill"] = "火攻", ["fire_attack_skill"] = "火攻",
[":fire_attack"] = "锦囊牌<br /><b>时机</b>:出牌阶段<br /><b>目标</b>:一名有手牌的角色<br /><b>效果</b>目标角色展示一张手牌然后你可以弃置一张与此牌花色相同的手牌对其造成1点火焰伤害。", [":fire_attack"] = "锦囊牌<br /><b>时机</b>:出牌阶段<br /><b>目标</b>:一名有手牌的角色<br /><b>效果</b>目标角色展示一张手牌然后你可以弃置一张与此牌花色相同的手牌对其造成1点火焰伤害。",
["#fire_attack-show"] = "%src 对你使用了火攻,请展示一张手牌", ["#fire_attack-show"] = "%src 对你使用了火攻,请展示一张手牌",
["#fire_attack-discard"] = "你可弃置一张 %arg 手牌,对 %src 造成1点火属性伤害", ["#fire_attack-discard"] = "你可弃置一张 %arg 手牌,对 %src 造成1点火属性伤害",
["#fire_attack_skill"] = "选择一名有手牌的角色,令其展示一张手牌,<br />然后你可以弃置一张与此牌花色相同的手牌对其造成1点火焰伤害", ["#fire_attack_skill"] = "选择一名有手牌的角色,令其展示一张手牌,<br />然后你可以弃置一张与此牌花色相同的手牌对其造成1点火焰伤害",
["supply_shortage"] = "兵粮寸断", ["supply_shortage"] = "兵粮寸断",
[":supply_shortage"] = "延时锦囊牌<br /><b>时机</b>:出牌阶段<br /><b>目标</b>距离1的一名其他角色<br /><b>效果</b>:将此牌置于目标角色判定区内。其判定阶段进行判定:若结果不为♣,其跳过摸牌阶段。然后将【兵粮寸断】置入弃牌堆。", [":supply_shortage"] = "延时锦囊牌<br /><b>时机</b>:出牌阶段<br /><b>目标</b>距离1的一名其他角色<br /><b>效果</b>:将此牌置于目标角色判定区内。其判定阶段进行判定:若结果不为♣,其跳过摸牌阶段。然后将【兵粮寸断】置入弃牌堆。",
["#supply_shortage_skill"] = "选择距离1的一名角色将此牌置于其判定区内。其判定阶段判定<br />若结果不为♣,其跳过摸牌阶段", ["#supply_shortage_skill"] = "选择距离1的一名角色将此牌置于其判定区内。其判定阶段判定<br />若结果不为♣,其跳过摸牌阶段",
["guding_blade"] = "古锭刀", ["guding_blade"] = "古锭刀",
[":guding_blade"] = "装备牌·武器<br /><b>攻击范围</b><br /><b>武器技能</b>:锁定技。每当你使用【杀】对目标角色造成伤害时,若该角色没有手牌,此伤害+1。", [":guding_blade"] = "装备牌·武器<br /><b>攻击范围</b><br /><b>武器技能</b>:锁定技。每当你使用【杀】对目标角色造成伤害时,若该角色没有手牌,此伤害+1。",
["#guding_blade_skill"] = "古锭刀", ["#guding_blade_skill"] = "古锭刀",
["fan"] = "朱雀羽扇", ["fan"] = "朱雀羽扇",
[":fan"] = "装备牌·武器<br /><b>攻击范围</b><br /><b>武器技能</b>:当你声明使用普【杀】后,你可以将此【杀】改为火【杀】。", [":fan"] = "装备牌·武器<br /><b>攻击范围</b><br /><b>武器技能</b>:当你声明使用普【杀】后,你可以将此【杀】改为火【杀】。",
["#fan_skill"] = "朱雀羽扇", ["#fan_skill"] = "朱雀羽扇",
["vine"] = "藤甲", ["vine"] = "藤甲",
[":vine"] = "装备牌·防具<br /><b>防具技能</b>:锁定技。【南蛮入侵】、【万箭齐发】和普通【杀】对你无效。每当你受到火焰伤害时,此伤害+1。", [":vine"] = "装备牌·防具<br /><b>防具技能</b>:锁定技。【南蛮入侵】、【万箭齐发】和普通【杀】对你无效。每当你受到火焰伤害时,此伤害+1。",
["#vine_skill"] = "藤甲", ["#vine_skill"] = "藤甲",
["silver_lion"] = "白银狮子", ["silver_lion"] = "白银狮子",
[":silver_lion"] = "装备牌·防具<br /><b>防具技能</b>锁定技。每当你受到伤害时若此伤害大于1点防止多余的伤害。每当你失去装备区里的【白银狮子】后你回复1点体力。", [":silver_lion"] = "装备牌·防具<br /><b>防具技能</b>锁定技。每当你受到伤害时若此伤害大于1点防止多余的伤害。每当你失去装备区里的【白银狮子】后你回复1点体力。",
["#silver_lion_skill"] = "白银狮子", ["#silver_lion_skill"] = "白银狮子",
["hualiu"] = "骅骝", ["hualiu"] = "骅骝",

View File

@ -213,7 +213,7 @@ local uncompulsoryInvalidity = fk.CreateInvaliditySkill {
end end
return return
(skill.frequency ~= Skill.Compulsory and skill.frequency ~= Skill.Wake) and (skill.frequency ~= Skill.Compulsory and skill.frequency ~= Skill.Wake) and
not (skill:isEquipmentSkill() or skill.name:endsWith("&")) and skill:isPlayerSkill(from) and
hasMark(from, MarkEnum.UncompulsoryInvalidity, MarkEnum.TempMarkSuffix) hasMark(from, MarkEnum.UncompulsoryInvalidity, MarkEnum.TempMarkSuffix)
-- ( -- (
-- from:getMark(MarkEnum.UncompulsoryInvalidity) ~= 0 or -- from:getMark(MarkEnum.UncompulsoryInvalidity) ~= 0 or

View File

@ -53,7 +53,14 @@ GameRule = fk.CreateTriggerSkill{
end) end)
if #cardNames == 0 then return end if #cardNames == 0 then return end
local peach_use = room:askForUseCard(player, "peach", table.concat(cardNames, ",") , prompt, true, {analepticRecover = true}) local peach_use = room:askForUseCard(
player,
"peach",
table.concat(cardNames, ","),
prompt,
true,
{analepticRecover = true, must_targets = { dyingPlayer.id }}
)
if not peach_use then break end if not peach_use then break end
peach_use.tos = { {dyingPlayer.id} } peach_use.tos = { {dyingPlayer.id} }
if peach_use.card.trueName == "analeptic" then if peach_use.card.trueName == "analeptic" then

View File

@ -52,6 +52,7 @@ Fk:loadTranslationTable({
["liubei"] = "Liu Bei", ["liubei"] = "Liu Bei",
["rende"] = "Benevolence", ["rende"] = "Benevolence",
[":rende"] = "In your Action Phase: you can give any # of hand cards to other players; then, if you have given a total of 2 or more cards, you heal 1 HP (only once).", [":rende"] = "In your Action Phase: you can give any # of hand cards to other players; then, if you have given a total of 2 or more cards, you heal 1 HP (only once).",
["#rende-active"] = "Use Benevolence, give any # of hand cards to other players;<br >then, if you have given a total of 2 or more cards, you heal 1 HP (only once)",
["jijiang"] = "Rouse", ["jijiang"] = "Rouse",
[":jijiang"] = "(lord) When you need to use/play Slash: you can ask other Shu characters to play Slash, which is regard as you use/play that.", [":jijiang"] = "(lord) When you need to use/play Slash: you can ask other Shu characters to play Slash, which is regard as you use/play that.",
["#jijiang-ask"] = "Rouse: you can play a Slash, which is regarded as %src uses/plays", ["#jijiang-ask"] = "Rouse: you can play a Slash, which is regarded as %src uses/plays",
@ -89,6 +90,7 @@ Fk:loadTranslationTable({
["sunquan"] = "Sun Quan", ["sunquan"] = "Sun Quan",
["zhiheng"] = "Balance of Power", ["zhiheng"] = "Balance of Power",
[":zhiheng"] = "Once per Action Phase: you can discard any # of cards; then, draw the same # of cards.", [":zhiheng"] = "Once per Action Phase: you can discard any # of cards; then, draw the same # of cards.",
["#zhiheng-active"] = "Use Balance of Power, discard any # of cards; then, draw the same # of cards",
["jiuyuan"] = "Rescued", ["jiuyuan"] = "Rescued",
[":jiuyuan"] = "(lord, forced) When another Wu character uses Peach to you, you heal +1 HP.", [":jiuyuan"] = "(lord, forced) When another Wu character uses Peach to you, you heal +1 HP.",
@ -103,18 +105,20 @@ Fk:loadTranslationTable({
["huanggai"] = "Huang Gai", ["huanggai"] = "Huang Gai",
["kurou"] = "Trojan Flesh", ["kurou"] = "Trojan Flesh",
[":kurou"] = "In your Action Phase: you can lose 1 HP; then, draw 2 cards.", [":kurou"] = "In your Action Phase: you can lose 1 HP; then, draw 2 cards.",
["#kurou-active"] = "Use Trojan Flesh, lose 1 HP; then, draw 2 cards",
["zhouyu"] = "Zhou Yu", ["zhouyu"] = "Zhou Yu",
["yingzi"] = "Handsome", ["yingzi"] = "Handsome",
[":yingzi"] = "In your Draw Phase: you can draw +1 additional card.", [":yingzi"] = "In your Draw Phase: you can draw +1 additional card.",
["fanjian"] = "Sow Dissension", ["fanjian"] = "Sow Dissension",
[":fanjian"] = "Once per Action Phase: you can make another player choose 1 suit; then, that player takes 1 hand card from you and displays it. If the guess was wrong, you cause him 1 DMG.", [":fanjian"] = "Once per Action Phase: you can make another player choose 1 suit; then, he takes 1 hand card from you and displays it. If the guess was wrong, you cause him 1 DMG.",
["#fanjian-active"] = "Use Sow Dissension, select another player; he chooses 1 suit;<br />then he takes 1 hand card from you and displays it. If the guess was wrong, you cause him 1 DMG",
["daqiao"] = "Da Qiao", ["daqiao"] = "Da Qiao",
["guose"] = "National Beauty", ["guose"] = "National Beauty",
[":guose"] = "You can use any diamond card as Indulgence.", [":guose"] = "You can use any diamond card as Indulgence.",
["liuli"] = "Shirk", ["liuli"] = "Shirk",
[":liuli"] = "When you become the target of Slash: you can discard 1 card and select another player (except the attacker) within your attack range; then, that player becomes the target of the Slash instead.", [":liuli"] = "When you become the target of Slash: you can discard 1 card and select another player (except the attacker) within your attack range; then, he becomes the target of the Slash instead.",
["#liuli-target"] = "Shirk: you can discard 1 card and transfer the Slash", ["#liuli-target"] = "Shirk: you can discard 1 card and transfer the Slash",
["luxun"] = "Lu Xun", ["luxun"] = "Lu Xun",
@ -128,10 +132,12 @@ Fk:loadTranslationTable({
[":xiaoji"] = "After you lose 1 card in your equipment area: you can draw 2 cards.", [":xiaoji"] = "After you lose 1 card in your equipment area: you can draw 2 cards.",
["jieyin"] = "Marriage", ["jieyin"] = "Marriage",
[":jieyin"] = "Once per Action Phase: you can discard 2 hand cards and select a hurt male character; then, both of you heal 1 HP.", [":jieyin"] = "Once per Action Phase: you can discard 2 hand cards and select a hurt male character; then, both of you heal 1 HP.",
["#jieyin-active"] = "Use Marriage, discard 2 hand cards and select a hurt male character; then, both of you heal 1 HP",
["huatuo"] = "Hua Tuo", ["huatuo"] = "Hua Tuo",
["qingnang"] = "Green Salve", ["qingnang"] = "Green Salve",
[":qingnang"] = "Once per Action Phase: you can discard 1 hand card and select a wounded player; then, he heals 1 HP.", [":qingnang"] = "Once per Action Phase: you can discard 1 hand card and select a wounded player; then, he heals 1 HP.",
["#qingnang-active"] = "Use Green Salve, discard 1 hand card and select a wounded player; then, he heals 1 HP",
["jijiu"] = "First Aid", ["jijiu"] = "First Aid",
[":jijiu"] = "Outside of your turn: you can use any red card as Peach.", [":jijiu"] = "Outside of your turn: you can use any red card as Peach.",
@ -142,6 +148,7 @@ Fk:loadTranslationTable({
["diaochan"] = "Diao Chan", ["diaochan"] = "Diao Chan",
["lijian"] = "Seed of Animosity", ["lijian"] = "Seed of Animosity",
[":lijian"] = "Once per Action Phase: you may discard 1 card and select 2 male characters; then, this is regarded as one of them having used Duel to target the other. This Duel can't be countered by Nullification.", [":lijian"] = "Once per Action Phase: you may discard 1 card and select 2 male characters; then, this is regarded as one of them having used Duel to target the other. This Duel can't be countered by Nullification.",
["#lijian-active"] = "Use Seed of Animosity, discard 1 card and select 2 male characters;<br />then, this is regarded as one of them having used Duel to target the other.<br />This Duel can't be countered by Nullification",
["biyue"] = "Envious by Moon", ["biyue"] = "Envious by Moon",
[":biyue"] = "In your Finish Phase, you can draw 1 card.", [":biyue"] = "In your Finish Phase, you can draw 1 card.",

View File

@ -100,6 +100,7 @@ Fk:loadTranslationTable{
["$rende2"] = "唯贤唯德,能服于人。", ["$rende2"] = "唯贤唯德,能服于人。",
["rende"] = "仁德", ["rende"] = "仁德",
[":rende"] = "出牌阶段你可以将至少一张手牌任意分配给其他角色。你于本阶段内以此法给出的手牌首次达到两张或更多后你回复1点体力。", [":rende"] = "出牌阶段你可以将至少一张手牌任意分配给其他角色。你于本阶段内以此法给出的手牌首次达到两张或更多后你回复1点体力。",
["#rende-active"] = "发动 仁德,将至少一张手牌交给其他角色",
["$jijiang1"] = "蜀将何在?", ["$jijiang1"] = "蜀将何在?",
["$jijiang2"] = "尔等敢应战否?", ["$jijiang2"] = "尔等敢应战否?",
["jijiang"] = "激将", ["jijiang"] = "激将",
@ -175,7 +176,8 @@ Fk:loadTranslationTable{
["$zhiheng1"] = "容我三思。", ["$zhiheng1"] = "容我三思。",
["$zhiheng2"] = "且慢。", ["$zhiheng2"] = "且慢。",
["zhiheng"] = "制衡", ["zhiheng"] = "制衡",
[":zhiheng"] = "出牌阶段限一次,你可以弃置至少一张牌然后摸等量的牌。", [":zhiheng"] = "出牌阶段限一次,你可以弃置任意张牌,然后摸等量的牌。",
["#zhiheng-active"] = "发动 制衡,弃置任意张牌,然后摸等量的牌",
["$jiuyuan1"] = "有汝辅佐,甚好!", ["$jiuyuan1"] = "有汝辅佐,甚好!",
["$jiuyuan2"] = "好舒服啊。", ["$jiuyuan2"] = "好舒服啊。",
["jiuyuan"] = "救援", ["jiuyuan"] = "救援",
@ -206,7 +208,8 @@ Fk:loadTranslationTable{
["$kurou1"] = "请鞭笞我吧,公瑾!", ["$kurou1"] = "请鞭笞我吧,公瑾!",
["$kurou2"] = "赴汤蹈火,在所不辞!", ["$kurou2"] = "赴汤蹈火,在所不辞!",
["kurou"] = "苦肉", ["kurou"] = "苦肉",
[":kurou"] = "出牌阶段你可以失去1点体力然后摸两张牌。", [":kurou"] = "出牌阶段你可以失去1点体力然后摸两张牌。",
["#kurou-active"] = "发动 苦肉失去1点体力然后摸两张牌",
["zhouyu"] = "周瑜", ["zhouyu"] = "周瑜",
["#zhouyu"] = "大都督", ["#zhouyu"] = "大都督",
@ -219,7 +222,8 @@ Fk:loadTranslationTable{
["$fanjian1"] = "挣扎吧,在血和暗的深渊里!", ["$fanjian1"] = "挣扎吧,在血和暗的深渊里!",
["$fanjian2"] = "痛苦吧,在仇与恨的地狱中!", ["$fanjian2"] = "痛苦吧,在仇与恨的地狱中!",
["fanjian"] = "反间", ["fanjian"] = "反间",
[":fanjian"] = "阶段技。你可以令一名其他角色选择一种花色然后正面朝上获得你的一张手牌。若此牌花色与该角色所选花色不同你对其造成1点伤害。", [":fanjian"] = "出牌阶段限一次你可以令一名其他角色选择一种花色然后正面朝上获得你的一张手牌。若此牌花色与其所选花色不同你对其造成1点伤害。",
["#fanjian-active"] = "发动 反间,选择一名其他角色,令其选择一种花色,然后正面朝上获得你的一张手牌<br />若此牌花色与其所选花色不同你对其造成1点伤害",
["daqiao"] = "大乔", ["daqiao"] = "大乔",
["#daqiao"] = "矜持之花", ["#daqiao"] = "矜持之花",
@ -246,7 +250,7 @@ Fk:loadTranslationTable{
["$lianying1"] = "牌不是万能的,但是没牌是万万不能的。", ["$lianying1"] = "牌不是万能的,但是没牌是万万不能的。",
["$lianying2"] = "旧的不去,新的不来。", ["$lianying2"] = "旧的不去,新的不来。",
["lianying"] = "连营", ["lianying"] = "连营",
[":lianying"] = "当你失去最后的手牌后,你可以摸一张牌。", [":lianying"] = "当你失去手牌后,若你没有手牌,你可以摸一张牌。",
["sunshangxiang"] = "孙尚香", ["sunshangxiang"] = "孙尚香",
["#sunshangxiang"] = "弓腰姬", ["#sunshangxiang"] = "弓腰姬",
@ -259,7 +263,8 @@ Fk:loadTranslationTable{
["$jieyin1"] = "夫君,身体要紧。", ["$jieyin1"] = "夫君,身体要紧。",
["$jieyin2"] = "他好,我也好。", ["$jieyin2"] = "他好,我也好。",
["jieyin"] = "结姻", ["jieyin"] = "结姻",
[":jieyin"] = "出牌阶段限一次你可以弃置两张手牌并选择一名已受伤的男性角色若如此做你和该角色各回复1点体力。", [":jieyin"] = "出牌阶段限一次你可以弃置两张手牌并选择一名已受伤的男性角色然后你与其各回复1点体力。",
["#jieyin-active"] = "发动 结姻弃置两张手牌并选择一名已受伤的男性角色你与其各回复1点体力",
["huatuo"] = "华佗", ["huatuo"] = "华佗",
["#huatuo"] = "神医", ["#huatuo"] = "神医",
@ -268,7 +273,8 @@ Fk:loadTranslationTable{
["$qingnang1"] = "早睡早起,方能养生。", ["$qingnang1"] = "早睡早起,方能养生。",
["$qingnang2"] = "越老越要补啊。", ["$qingnang2"] = "越老越要补啊。",
["qingnang"] = "青囊", ["qingnang"] = "青囊",
[":qingnang"] = "出牌阶段限一次你可以弃置一张手牌并选择一名已受伤的角色若如此做该角色回复1点体力。", [":qingnang"] = "出牌阶段限一次你可以弃置一张手牌并选择一名已受伤的角色然后其回复1点体力。",
["#qingnang-active"] = "发动 青囊弃置一张手牌并选择一名已受伤的角色其回复1点体力",
["$jijiu1"] = "别紧张,有老夫呢。", ["$jijiu1"] = "别紧张,有老夫呢。",
["$jijiu2"] = "救人一命,胜造七级浮屠。", ["$jijiu2"] = "救人一命,胜造七级浮屠。",
["jijiu"] = "急救", ["jijiu"] = "急救",
@ -291,6 +297,7 @@ Fk:loadTranslationTable{
["$lijian2"] = "夫君,你要替妾身作主啊……", ["$lijian2"] = "夫君,你要替妾身作主啊……",
["lijian"] = "离间", ["lijian"] = "离间",
[":lijian"] = "出牌阶段限一次,你可以弃置一张牌并选择两名其他男性角色,后选择的角色视为对先选择的角色使用了一张不能被【无懈可击】的【决斗】。", [":lijian"] = "出牌阶段限一次,你可以弃置一张牌并选择两名其他男性角色,后选择的角色视为对先选择的角色使用了一张不能被【无懈可击】的【决斗】。",
["#lijian-active"] = "发动 离间,弃置一张手牌并选择两名其他男性角色,后选择的角色视为对先选择的角色使用了一张不能被【无懈可击】的【决斗】",
["$biyue1"] = "失礼了~", ["$biyue1"] = "失礼了~",
["$biyue2"] = "羡慕吧~", ["$biyue2"] = "羡慕吧~",
["biyue"] = "闭月", ["biyue"] = "闭月",
@ -529,4 +536,7 @@ Fk:loadTranslationTable{
["revealDeputy"] = "明置副将 %arg", ["revealDeputy"] = "明置副将 %arg",
["game_rule"] = "弃牌阶段", ["game_rule"] = "弃牌阶段",
["replace_equip"] = "替换装备",
["#EquipmentChoice"] = "%arg",
["#GameRuleReplaceEquipment"] = "请选择要置入的区域",
} }

View File

@ -13,11 +13,7 @@ local jianxiong = fk.CreateTriggerSkill{
anim_type = "masochism", anim_type = "masochism",
events = {fk.Damaged}, events = {fk.Damaged},
can_trigger = function(self, event, target, player, data) can_trigger = function(self, event, target, player, data)
if target == player and player:hasSkill(self) and data.card then return target == player and player:hasSkill(self) and data.card and player.room:getCardArea(data.card) == Card.Processing
local room = player.room
local subcards = data.card:isVirtual() and data.card.subcards or {data.card.id}
return #subcards>0 and table.every(subcards, function(id) return room:getCardArea(id) == Card.Processing end)
end
end, end,
on_use = function(self, event, target, player, data) on_use = function(self, event, target, player, data)
player.room:obtainCard(player.id, data.card, true, fk.ReasonJustMove) player.room:obtainCard(player.id, data.card, true, fk.ReasonJustMove)
@ -157,11 +153,11 @@ local tuxi = fk.CreateTriggerSkill{
events = {fk.EventPhaseStart}, events = {fk.EventPhaseStart},
can_trigger = function(self, event, target, player, data) can_trigger = function(self, event, target, player, data)
return target == player and player:hasSkill(self) and player.phase == Player.Draw and return target == player and player:hasSkill(self) and player.phase == Player.Draw and
table.find(player.room:getOtherPlayers(player), function(p) return not p:isKongcheng() end) table.find(player.room:getOtherPlayers(player, false), function(p) return not p:isKongcheng() end)
end, end,
on_cost = function(self, event, target, player, data) on_cost = function(self, event, target, player, data)
local room = player.room local room = player.room
local targets = table.map(table.filter(room:getOtherPlayers(player), function(p) local targets = table.map(table.filter(room:getOtherPlayers(player, false), function(p)
return not p:isKongcheng() end), Util.IdMapper) return not p:isKongcheng() end), Util.IdMapper)
local result = room:askForChoosePlayers(player, targets, 1, 2, "#tuxi-ask", self.name) local result = room:askForChoosePlayers(player, targets, 1, 2, "#tuxi-ask", self.name)
@ -273,7 +269,8 @@ local yiji = fk.CreateTriggerSkill{
for _, id in ipairs(ret.cards) do for _, id in ipairs(ret.cards) do
table.removeOne(ids, id) table.removeOne(ids, id)
end end
room:moveCardTo(ret.cards, Card.PlayerHand, room:getPlayerById(ret.targets[1]), fk.ReasonGive, self.name, nil, false, player.id) room:moveCardTo(ret.cards, Card.PlayerHand, room:getPlayerById(ret.targets[1]), fk.ReasonGive,
self.name, nil, false, player.id, nil, player.id)
if #ids == 0 then break end if #ids == 0 then break end
if player.dead then if player.dead then
room:moveCards({ room:moveCards({
@ -357,6 +354,7 @@ zhenji:addSkill(qingguo)
local rende = fk.CreateActiveSkill{ local rende = fk.CreateActiveSkill{
name = "rende", name = "rende",
prompt = "#rende-active",
anim_type = "support", anim_type = "support",
card_filter = function(self, to_select, selected) card_filter = function(self, to_select, selected)
return Fk:currentRoom():getCardArea(to_select) == Card.PlayerHand return Fk:currentRoom():getCardArea(to_select) == Card.PlayerHand
@ -633,6 +631,7 @@ huangyueying:addSkill(qicai)
local zhiheng = fk.CreateActiveSkill{ local zhiheng = fk.CreateActiveSkill{
name = "zhiheng", name = "zhiheng",
prompt = "#zhiheng-active",
anim_type = "drawcard", anim_type = "drawcard",
can_use = function(self, player) can_use = function(self, player)
return player:usedSkillTimes(self.name, Player.HistoryPhase) == 0 return player:usedSkillTimes(self.name, Player.HistoryPhase) == 0
@ -737,10 +736,9 @@ lvmeng:addSkill(keji)
local kurou = fk.CreateActiveSkill{ local kurou = fk.CreateActiveSkill{
name = "kurou", name = "kurou",
prompt = "#kurou-active",
anim_type = "drawcard", anim_type = "drawcard",
card_filter = function(self, to_select, selected, selected_targets) card_filter = Util.FalseFunc,
return false
end,
on_use = function(self, room, effect) on_use = function(self, room, effect)
local from = room:getPlayerById(effect.from) local from = room:getPlayerById(effect.from)
room:loseHp(from, 1, self.name) room:loseHp(from, 1, self.name)
@ -762,6 +760,7 @@ local yingzi = fk.CreateTriggerSkill{
} }
local fanjian = fk.CreateActiveSkill{ local fanjian = fk.CreateActiveSkill{
name = "fanjian", name = "fanjian",
prompt = "#fanjian-active",
can_use = function(self, player) can_use = function(self, player)
return player:usedSkillTimes(self.name, Player.HistoryPhase) == 0 return player:usedSkillTimes(self.name, Player.HistoryPhase) == 0
end, end,
@ -937,6 +936,7 @@ local xiaoji = fk.CreateTriggerSkill{
} }
local jieyin = fk.CreateActiveSkill{ local jieyin = fk.CreateActiveSkill{
name = "jieyin", name = "jieyin",
prompt = "#jieyin-active",
anim_type = "support", anim_type = "support",
can_use = function(self, player) can_use = function(self, player)
return player:usedSkillTimes(self.name, Player.HistoryPhase) == 0 return player:usedSkillTimes(self.name, Player.HistoryPhase) == 0
@ -978,6 +978,7 @@ sunshangxiang:addSkill(jieyin)
local qingnang = fk.CreateActiveSkill{ local qingnang = fk.CreateActiveSkill{
name = "qingnang", name = "qingnang",
prompt = "#qingnang-active",
anim_type = "support", anim_type = "support",
can_use = function(self, player) can_use = function(self, player)
return player:usedSkillTimes(self.name, Player.HistoryPhase) == 0 return player:usedSkillTimes(self.name, Player.HistoryPhase) == 0
@ -993,8 +994,8 @@ local qingnang = fk.CreateActiveSkill{
card_num = 1, card_num = 1,
on_use = function(self, room, effect) on_use = function(self, room, effect)
local from = room:getPlayerById(effect.from) local from = room:getPlayerById(effect.from)
room:throwCard(effect.cards, self.name, from, from)
local to = room:getPlayerById(effect.tos[1]) local to = room:getPlayerById(effect.tos[1])
room:throwCard(effect.cards, self.name, from, from)
if to:isAlive() and to:isWounded() then if to:isAlive() and to:isWounded() then
room:recover({ room:recover({
who = to, who = to,
@ -1063,6 +1064,7 @@ lvbu:addSkill(wushuang)
local lijian = fk.CreateActiveSkill{ local lijian = fk.CreateActiveSkill{
name = "lijian", name = "lijian",
prompt = "#lijian-active",
anim_type = "offensive", anim_type = "offensive",
can_use = function(self, player) can_use = function(self, player)
return player:usedSkillTimes(self.name, Player.HistoryPhase) == 0 return player:usedSkillTimes(self.name, Player.HistoryPhase) == 0
@ -1072,7 +1074,9 @@ local lijian = fk.CreateActiveSkill{
end, end,
target_filter = function(self, to_select, selected) target_filter = function(self, to_select, selected)
if #selected < 2 and to_select ~= Self.id then if #selected < 2 and to_select ~= Self.id then
return Fk:currentRoom():getPlayerById(to_select):isMale() local target = Fk:currentRoom():getPlayerById(to_select)
return target:isMale() and (#selected == 0 or
target:canUseTo(Fk:cloneCard("duel"), Fk:currentRoom():getPlayerById(selected[1])))
end end
end, end,
target_num = 2, target_num = 2,
@ -1151,12 +1155,10 @@ local role_getlogic = function()
end) end)
room:returnToGeneralPile(generals) room:returnToGeneralPile(generals)
room:setPlayerGeneral(lord, lord_general, true) room:prepareGeneral(lord, lord_general, deputy, true)
room:askForChooseKingdom({lord}) room:askForChooseKingdom({lord})
room:broadcastProperty(lord, "general")
room:broadcastProperty(lord, "kingdom") room:broadcastProperty(lord, "kingdom")
room:setDeputyGeneral(lord, deputy)
room:broadcastProperty(lord, "deputyGeneral")
-- 显示技能 -- 显示技能
local canAttachSkill = function(player, skillName) local canAttachSkill = function(player, skillName)
@ -1210,8 +1212,7 @@ local role_getlogic = function()
end end
local nonlord = room:getOtherPlayers(lord, true) local nonlord = room:getOtherPlayers(lord, true)
local generals = room:getNGenerals(#nonlord * generalNum) local generals = table.random(room.general_pile, #nonlord * generalNum)
table.shuffle(generals)
for i, p in ipairs(nonlord) do for i, p in ipairs(nonlord) do
local arg = table.slice(generals, (i - 1) * generalNum + 1, i * generalNum + 1) local arg = table.slice(generals, (i - 1) * generalNum + 1, i * generalNum + 1)
p.request_data = json.encode{ arg, n } p.request_data = json.encode{ arg, n }
@ -1221,30 +1222,22 @@ local role_getlogic = function()
room:notifyMoveFocus(nonlord, "AskForGeneral") room:notifyMoveFocus(nonlord, "AskForGeneral")
room:doBroadcastRequest("AskForGeneral", nonlord) room:doBroadcastRequest("AskForGeneral", nonlord)
local selected = {}
for _, p in ipairs(nonlord) do for _, p in ipairs(nonlord) do
local general, deputy
if p.general == "" and p.reply_ready then if p.general == "" and p.reply_ready then
local general_ret = json.decode(p.client_reply) local general_ret = json.decode(p.client_reply)
local general = general_ret[1] general = general_ret[1]
local deputy = general_ret[2] deputy = general_ret[2]
table.insertTableIfNeed(selected, general_ret)
room:setPlayerGeneral(p, general, true, true)
room:setDeputyGeneral(p, deputy)
else else
table.insertTableIfNeed(selected, p.default_reply) general = p.default_reply[1]
room:setPlayerGeneral(p, p.default_reply[1], true, true) deputy = p.default_reply[2]
room:setDeputyGeneral(p, p.default_reply[2])
end end
room:findGeneral(general)
room:findGeneral(deputy)
room:prepareGeneral(p, general, deputy)
p.default_reply = "" p.default_reply = ""
end end
generals = table.filter(generals, function(g)
return not table.find(selected, function(lg)
return Fk.generals[lg].trueName == Fk.generals[g].trueName
end)
end)
room:returnToGeneralPile(generals)
room:askForChooseKingdom(nonlord) room:askForChooseKingdom(nonlord)
end end
@ -1375,6 +1368,6 @@ Fk:loadTranslationTable{
} }
-- load translations of this package -- load translations of this package
dofile "packages/standard/i18n/init.lua" dofile "packages/freekill-core/standard/i18n/init.lua"
return extension return extension

View File

@ -52,6 +52,8 @@ Fk:loadTranslationTable({
["method_draw"] = "draw", ["method_draw"] = "draw",
["method_discard"] = "discard", ["method_discard"] = "discard",
["prohibit"] = " prohibit ",
["slash"] = "Slash", ["slash"] = "Slash",
[":slash"] = "Slash (basic card)<br /><b>Phase</b>: Action phase<br /><b>Target</b>: Another player within your ATK range<br /><b>Effect</b>: Deal 1 DMG to the targets.<br/><b>Note</b>: You can only use 1 Slash per action phase.", [":slash"] = "Slash (basic card)<br /><b>Phase</b>: Action phase<br /><b>Target</b>: Another player within your ATK range<br /><b>Effect</b>: Deal 1 DMG to the targets.<br/><b>Note</b>: You can only use 1 Slash per action phase.",
["#slash-jink"] = "%src used Slash to you, please use a Dodge", ["#slash-jink"] = "%src used Slash to you, please use a Dodge",

View File

@ -52,10 +52,12 @@ Fk:loadTranslationTable{
["method_draw"] = "", ["method_draw"] = "",
["method_discard"] = "弃置", ["method_discard"] = "弃置",
["prohibit"] = "",
["slash"] = "", ["slash"] = "",
[":slash"] = "基本牌<br /><b>时机</b>:出牌阶段<br /><b>目标</b>:攻击范围内的一名角色<br /><b>效果</b>对目标角色造成1点伤害。", [":slash"] = "基本牌<br /><b>时机</b>:出牌阶段<br /><b>目标</b>:攻击范围内的一名角色<br /><b>效果</b>对目标角色造成1点伤害。",
["#slash-jink"] = "%src 对你使用了杀,请使用一张闪", ["#slash-jink"] = "%src 对你使用了,请使用一张",
["#slash-jink-multi"] = "%src 对你使用了杀,请使用一张闪(此为第 %arg 张,共需 %arg2 张)", ["#slash-jink-multi"] = "%src 对你使用了,请使用一张(此为第 %arg 张,共需 %arg2 张)",
["#slash_skill"] = "选择攻击范围内的一名角色对其造成1点伤害", ["#slash_skill"] = "选择攻击范围内的一名角色对其造成1点伤害",
["#slash_skill_multi"] = "选择攻击范围内的至多%arg名角色对这些角色各造成1点伤害", ["#slash_skill_multi"] = "选择攻击范围内的至多%arg名角色对这些角色各造成1点伤害",

View File

@ -622,6 +622,7 @@ local amazingGraceSkill = fk.CreateActiveSkill{
ids = toDisplay, ids = toDisplay,
toArea = Card.Processing, toArea = Card.Processing,
moveReason = fk.ReasonPut, moveReason = fk.ReasonPut,
proposer = use.from,
}) })
table.forEach(room.players, function(p) table.forEach(room.players, function(p)
@ -722,7 +723,17 @@ local lightningSkill = fk.CreateActiveSkill{
local nextp = to local nextp = to
repeat repeat
nextp = nextp:getNextAlive(true) nextp = nextp:getNextAlive(true)
if nextp == to then break end if nextp == to then
if nextp:isProhibited(nextp, effect.card) then
room:moveCards{
ids = room:getSubcardsByRule(effect.card, { Card.Processing }),
toArea = Card.DiscardPile,
moveReason = fk.ReasonPut
}
return
end
break
end
until not nextp:hasDelayedTrick("lightning") and not nextp:isProhibited(nextp, effect.card) until not nextp:hasDelayedTrick("lightning") and not nextp:isProhibited(nextp, effect.card)
@ -816,10 +827,16 @@ local crossbowAudio = fk.CreateTriggerSkill{
local crossbowSkill = fk.CreateTargetModSkill{ local crossbowSkill = fk.CreateTargetModSkill{
name = "#crossbow_skill", name = "#crossbow_skill",
attached_equip = "crossbow", attached_equip = "crossbow",
bypass_times = function(self, player, skill, scope) bypass_times = function(self, player, skill, scope, card)
if player:hasSkill(self) and skill.trueName == "slash_skill" if player:hasSkill(self) and skill.trueName == "slash_skill" and scope == Player.HistoryPhase then
and scope == Player.HistoryPhase then --FIXME: 无法检测到非转化的cost选牌的情况如活墨等
return true local cardIds = Card:getIdList(card)
local crossbows = table.filter(player:getEquipments(Card.SubtypeWeapon), function(id)
return Fk:getCardById(id).equip_skill == self
end)
return #crossbows == 0 or not table.every(crossbows, function(id)
return table.contains(cardIds, id)
end)
end end
end, end,
} }
@ -1156,9 +1173,13 @@ local eightDiagramSkill = fk.CreateTriggerSkill{
attached_equip = "eight_diagram", attached_equip = "eight_diagram",
events = {fk.AskForCardUse, fk.AskForCardResponse}, events = {fk.AskForCardUse, fk.AskForCardResponse},
can_trigger = function(self, event, target, player, data) can_trigger = function(self, event, target, player, data)
return target == player and player:hasSkill(self) and if not (target == player and player:hasSkill(self) and
(data.cardName == "jink" or (data.pattern and Exppattern:Parse(data.pattern):matchExp("jink|0|nosuit|none"))) and (data.cardName == "jink" or (data.pattern and Exppattern:Parse(data.pattern):matchExp("jink|0|nosuit|none")))) then return end
(event == fk.AskForCardUse and not player:prohibitUse(Fk:cloneCard("jink")) or not player:prohibitResponse(Fk:cloneCard("jink"))) if event == fk.AskForCardUse then
return not player:prohibitUse(Fk:cloneCard("jink"))
else
return not player:prohibitResponse(Fk:cloneCard("jink"))
end
end, end,
on_use = function(self, event, target, player, data) on_use = function(self, event, target, player, data)
local room = player.room local room = player.room

View File

@ -4,6 +4,7 @@
#include "network/client_socket.h" #include "network/client_socket.h"
#include "server/server.h" #include "server/server.h"
#include "core/util.h" #include "core/util.h"
#include <QNetworkDatagram>
ServerSocket::ServerSocket(QObject *parent) : QObject(parent) { ServerSocket::ServerSocket(QObject *parent) : QObject(parent) {
server = new QTcpServer(this); server = new QTcpServer(this);

View File

@ -3,6 +3,7 @@
#include "server/serverplayer.h" #include "server/serverplayer.h"
#include "core/util.h" #include "core/util.h"
#include "network/client_socket.h" #include "network/client_socket.h"
#include <openssl/bn.h>
AuthManager::AuthManager(QObject *parent) : QObject(parent) { AuthManager::AuthManager(QObject *parent) : QObject(parent) {
rsa = initRSA(); rsa = initRSA();