Changelog: v0.4.20
Some checks failed
Check Whitespace and New Line / check (push) Has been cancelled
Deploy Doxygen to Pages / build (push) Has been cancelled
Deploy Doxygen to Pages / deploy (push) Has been cancelled

This commit is contained in:
notify 2024-10-22 00:10:53 +08:00
parent 220fb93b0e
commit 30df075db2
76 changed files with 5007 additions and 2437 deletions

View File

@ -41,6 +41,7 @@ jobs:
find -name "*.cpp" -exec sed -i '1i #include "pch.h"' "{}" \; find -name "*.cpp" -exec sed -i '1i #include "pch.h"' "{}" \;
find -name "*.h" -exec sed -i '1i #include "pch.h"' "{}" \; find -name "*.h" -exec sed -i '1i #include "pch.h"' "{}" \;
sed -i '1d' pch.h sed -i '1d' pch.h
sed -i '1d' main.cpp
sed -i '/pch.h/d' CMakeLists.txt sed -i '/pch.h/d' CMakeLists.txt
- name: Configure CMake Project - name: Configure CMake Project

View File

@ -1,5 +1,18 @@
# ChangeLog # ChangeLog
## v0.4.20
- 重构了UI逻辑
- 重构了客户端隐藏信息的处理
- 旁观不会看到手牌和身份了
- 牌局内可查看一览
- 新增了明置牌概念 以及相关状态技
- 新增了筛选房间功能
- 新增拖动手牌排序
- 新增技能times
___
## v0.4.19 ## v0.4.19
- 修改加入服务器界面,新增服务器列表(暂不会自动更新) - 修改加入服务器界面,新增服务器列表(暂不会自动更新)

View File

@ -6,7 +6,7 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(FreeKill VERSION 0.4.19) project(FreeKill VERSION 0.4.20)
add_definitions(-DFK_VERSION=\"${CMAKE_PROJECT_VERSION}\") add_definitions(-DFK_VERSION=\"${CMAKE_PROJECT_VERSION}\")
find_package(Qt6 REQUIRED COMPONENTS find_package(Qt6 REQUIRED COMPONENTS
@ -40,22 +40,6 @@ include_directories(include)
include_directories(include/libgit2) include_directories(include/libgit2)
include_directories(src) include_directories(src)
file(GLOB SWIG_FILES "${PROJECT_SOURCE_DIR}/src/swig/*.i")
if (DEFINED FK_SERVER_ONLY)
set(SWIG_SOURCE ${PROJECT_SOURCE_DIR}/src/swig/freekill-nogui.i)
else ()
set(SWIG_SOURCE ${PROJECT_SOURCE_DIR}/src/swig/freekill.i)
endif ()
add_custom_command(
OUTPUT ${PROJECT_SOURCE_DIR}/src/swig/freekill-wrap.cxx
DEPENDS ${SWIG_FILES}
COMMENT "Generating freekill-wrap.cxx"
COMMAND swig -c++ -lua -Wall -o
${PROJECT_SOURCE_DIR}/src/swig/freekill-wrap.cxx
${SWIG_SOURCE}
)
qt_add_executable(FreeKill) qt_add_executable(FreeKill)
if (NOT DEFINED FK_SERVER_ONLY) if (NOT DEFINED FK_SERVER_ONLY)

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="419" android:versionCode="420"
android:versionName="0.4.19"> android:versionName="0.4.20">
<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" />

32
deploycore.sh Executable file
View File

@ -0,0 +1,32 @@
#!/bin/bash
# 游戏依托freekill-core这个特殊仓库进行日常更新与开发
# 在更新版本号之前需要先把它们与自带的lua/同步一下
PWD=$(pwd)
if ! [ -e packages/freekill-core ]; then
echo '需要有freekill-core才可执行'
cd $PWD
exit 1
fi
rm -r lua/
delcode() {
cd $1
find -name '*.lua' -delete
find -empty -delete
cd ..
}
cd packages
delcode standard
delcode standard_cards
delcode maneuvering
cp -r freekill-core/lua ..
cp -r freekill-core/standard .
cp -r freekill-core/standard_cards .
cp -r freekill-core/maneuvering .
cd $PWD

1
include Submodule

@ -0,0 +1 @@
Subproject commit 4fd2070d099d1f967d1070d72beb0fae2cb6e4be

View File

@ -6,7 +6,6 @@
---@field public alive_players ClientPlayer[] @ 所有存活玩家的数组 ---@field public alive_players ClientPlayer[] @ 所有存活玩家的数组
---@field public observers ClientPlayer[] @ 观察者的数组 ---@field public observers ClientPlayer[] @ 观察者的数组
---@field public current ClientPlayer @ 当前回合玩家 ---@field public current ClientPlayer @ 当前回合玩家
---@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的时间 ---@field public last_update_ui integer @ 上次刷新状态技UI的时间
@ -35,7 +34,7 @@ local no_decode_commands = {
function Client:initialize() function Client:initialize()
AbstractRoom.initialize(self) AbstractRoom.initialize(self)
self.client = fk.ClientInstance self.client = fk.ClientInstance
self.notifyUI = function(self, command, data) self.notifyUI = function(_, command, data)
fk.Backend:notifyUI(command, data) fk.Backend:notifyUI(command, data)
end end
self.client.callback = function(_self, command, jsonData, isRequest) self.client.callback = function(_self, command, jsonData, isRequest)
@ -83,12 +82,11 @@ function Client:initialize()
for _, cid in ipairs(Self:getCardIds("h")) do for _, cid in ipairs(Self:getCardIds("h")) do
self:notifyUI("UpdateCard", cid) self:notifyUI("UpdateCard", cid)
end end
-- 刷技能状态
self:notifyUI("UpdateSkill", nil)
end end
end end
self.discard_pile = {}
self._processing = {}
self.disabled_packs = {} self.disabled_packs = {}
self.disabled_generals = {} self.disabled_generals = {}
-- self.last_update_ui = os.getms() -- self.last_update_ui = os.getms()
@ -106,79 +104,20 @@ function Client:getPlayerById(id)
return nil return nil
end end
---@param cardId integer | Card ---@param moves CardsMoveStruct[]
---@return CardArea
function Client:getCardArea(cardId)
local cardIds = Card:getIdList(cardId)
local resultPos = {}
for _, cid in ipairs(cardIds) do
if not table.contains(resultPos, Card.PlayerHand) and table.contains(Self.player_cards[Player.Hand], cid) then
table.insert(resultPos, Card.PlayerHand)
end
if not table.contains(resultPos, Card.PlayerEquip) and table.contains(Self.player_cards[Player.Equip], cid) then
table.insert(resultPos, Card.PlayerEquip)
end
for _, t in pairs(Self.special_cards) do
if table.contains(t, cid) then
table.insertIfNeed(resultPos, Card.PlayerSpecial)
end
end
end
if #resultPos == 1 then
return resultPos[1]
end
return Card.Unknown
end
function Client:moveCards(moves) function Client:moveCards(moves)
for _, move in ipairs(moves) do for _, data in ipairs(moves) do
if move.from and move.fromArea then if #data.moveInfo > 0 then
local from = self:getPlayerById(move.from) for _, info in ipairs(data.moveInfo) do
if move.fromArea == Card.PlayerHand and not Self:isBuddy(self:getPlayerById(move.from)) then self:applyMoveInfo(data, info)
for _ = 1, #move.ids do Fk:filterCard(info.cardId, self:getPlayerById(data.to))
table.remove(from.player_cards[Player.Hand])
end end
else
if table.contains({ Player.Hand, Player.Equip, Player.Judge, Player.Special }, move.fromArea) then
from:removeCards(move.fromArea, move.ids, move.fromSpecialName)
end
end
elseif move.fromArea == Card.DiscardPile then
table.removeOne(self.discard_pile, move.ids[1])
end
if move.to and move.toArea then
local ids = move.ids
if (move.toArea == Card.PlayerHand and not Self:isBuddy(self:getPlayerById(move.to))) or
(move.toArea == Card.PlayerSpecial and not move.moveVisible) then
ids = {-1}
end
self:getPlayerById(move.to):addCards(move.toArea, ids, move.specialName)
elseif move.toArea == Card.DiscardPile then
table.insert(self.discard_pile, move.ids[1])
end
-- FIXME: 需要系统化的重构
if move.fromArea == Card.Processing then
for _, v in ipairs(move.ids) do
self._processing[v] = nil
end
end
if move.toArea == Card.Processing then
for _, v in ipairs(move.ids) do
self._processing[v] = true
end
end
if (move.ids[1] ~= -1) then
Fk:filterCard(move.ids[1], ClientInstance:getPlayerById(move.to))
end end
end end
end end
---@param msg LogMessage ---@param msg LogMessage
local function parseMsg(msg, nocolor) local function parseMsg(msg, nocolor, visible_data)
local self = ClientInstance local self = ClientInstance
local data = msg local data = msg
local function getPlayerStr(pid, color) local function getPlayerStr(pid, color)
@ -218,7 +157,9 @@ local function parseMsg(msg, nocolor)
local allUnknown = true local allUnknown = true
local unknownCount = 0 local unknownCount = 0
for _, id in ipairs(card) do for _, id in ipairs(card) do
if id ~= -1 then local known = id ~= -1
if visible_data then known = visible_data[tostring(id)] end
if known then
allUnknown = false allUnknown = false
else else
unknownCount = unknownCount + 1 unknownCount = unknownCount + 1
@ -230,11 +171,15 @@ local function parseMsg(msg, nocolor)
else else
local card_str = {} local card_str = {}
for _, id in ipairs(card) do for _, id in ipairs(card) do
local known = id ~= -1
if visible_data then known = visible_data[tostring(id)] end
if known then
table.insert(card_str, Fk:getCardById(id, true):toLogString()) table.insert(card_str, Fk:getCardById(id, true):toLogString())
end end
end
if unknownCount > 0 then if unknownCount > 0 then
table.insert(card_str, Fk:translate("unknown_card") local suffix = unknownCount > 1 and ("x" .. unknownCount) or ""
.. unknownCount == 1 and "x" .. unknownCount or "") table.insert(card_str, Fk:translate("unknown_card") .. suffix)
end end
card = table.concat(card_str, ", ") card = table.concat(card_str, ", ")
end end
@ -261,8 +206,8 @@ local function parseMsg(msg, nocolor)
end end
---@param msg LogMessage ---@param msg LogMessage
function Client:appendLog(msg) function Client:appendLog(msg, visible_data)
local text = parseMsg(msg) local text = parseMsg(msg, nil, visible_data)
self:notifyUI("GameLog", text) self:notifyUI("GameLog", text)
if msg.toast then if msg.toast then
self:notifyUI("ShowToast", text) self:notifyUI("ShowToast", text)
@ -298,7 +243,10 @@ end
fk.client_callback["EnterRoom"] = function(_data) fk.client_callback["EnterRoom"] = function(_data)
Self = ClientPlayer:new(fk.Self) Self = ClientPlayer:new(fk.Self)
-- 垃圾bug 怎么把这玩意忘了
local ob = ClientInstance.observing
ClientInstance = Client:new() -- clear old client data ClientInstance = Client:new() -- clear old client data
ClientInstance.observing = ob
ClientInstance.players = {Self} ClientInstance.players = {Self}
ClientInstance.alive_players = {Self} ClientInstance.alive_players = {Self}
ClientInstance.discard_pile = {} ClientInstance.discard_pile = {}
@ -405,6 +353,14 @@ fk.client_callback["PropertyUpdate"] = function(data)
ClientInstance:notifyUI("PropertyUpdate", data) ClientInstance:notifyUI("PropertyUpdate", data)
end end
fk.client_callback["PlayCard"] = function(data)
local h = Fk.request_handlers["PlayCard"]:new(Self)
h.change = {}
h:setup()
h.scene:notifyUI()
ClientInstance:notifyUI("PlayCard", data)
end
fk.client_callback["AskForCardChosen"] = function(data) fk.client_callback["AskForCardChosen"] = function(data)
-- jsonData: [ int target_id, string flag, int reason ] -- jsonData: [ int target_id, string flag, int reason ]
local id, flag, reason, prompt = data[1], data[2], data[3], data[4] local id, flag, reason, prompt = data[1], data[2], data[3], data[4]
@ -557,7 +513,8 @@ local function mergeMoves(moves)
proposer = move.proposer, proposer = move.proposer,
} }
end end
table.insert(temp[info].ids, move.moveVisible and move.ids[1] or -1) -- table.insert(temp[info].ids, move.moveVisible and move.ids[1] or -1)
table.insert(temp[info].ids, move.ids[1])
end end
for _, v in pairs(temp) do for _, v in pairs(temp) do
table.insert(ret, v) table.insert(ret, v)
@ -565,109 +522,111 @@ local function mergeMoves(moves)
return ret return ret
end end
local function sendMoveCardLog(move) local function sendMoveCardLog(move, visible_data)
local client = ClientInstance ---@class Client local client = ClientInstance ---@class Client
if #move.ids == 0 then return end if #move.ids == 0 then return end
local hidden = table.contains(move.ids, -1) local hidden = not not table.find(move.ids, function(id)
return visible_data[tostring(id)] == false
end)
local msgtype local msgtype
if move.toArea == Card.PlayerHand then if move.toArea == Card.PlayerHand then
if move.fromArea == Card.PlayerSpecial then if move.fromArea == Card.PlayerSpecial then
client:appendLog{ client:appendLog({
type = "$GetCardsFromPile", type = "$GetCardsFromPile",
from = move.to, from = move.to,
arg = move.fromSpecialName, arg = move.fromSpecialName,
arg2 = #move.ids, arg2 = #move.ids,
card = move.ids, card = move.ids,
} }, visible_data)
elseif move.fromArea == Card.DrawPile then elseif move.fromArea == Card.DrawPile then
client:appendLog{ client:appendLog({
type = "$DrawCards", type = "$DrawCards",
from = move.to, from = move.to,
card = move.ids, card = move.ids,
arg = #move.ids, arg = #move.ids,
} }, visible_data)
elseif move.fromArea == Card.Processing then elseif move.fromArea == Card.Processing then
client:appendLog{ client:appendLog({
type = "$GotCardBack", type = "$GotCardBack",
from = move.to, from = move.to,
card = move.ids, card = move.ids,
arg = #move.ids, arg = #move.ids,
} }, visible_data)
elseif move.fromArea == Card.DiscardPile then elseif move.fromArea == Card.DiscardPile then
client:appendLog{ client:appendLog({
type = "$RecycleCard", type = "$RecycleCard",
from = move.to, from = move.to,
card = move.ids, card = move.ids,
arg = #move.ids, arg = #move.ids,
} }, visible_data)
elseif move.from then elseif move.from then
client:appendLog{ client:appendLog({
type = "$MoveCards", type = "$MoveCards",
from = move.from, from = move.from,
to = { move.to }, to = { move.to },
arg = #move.ids, arg = #move.ids,
card = move.ids, card = move.ids,
} }, visible_data)
else else
client:appendLog{ client:appendLog({
type = "$PreyCardsFromPile", type = "$PreyCardsFromPile",
from = move.to, from = move.to,
card = move.ids, card = move.ids,
arg = #move.ids, arg = #move.ids,
} }, visible_data)
end end
elseif move.toArea == Card.PlayerEquip then elseif move.toArea == Card.PlayerEquip then
client:appendLog{ client:appendLog({
type = "$InstallEquip", type = "$InstallEquip",
from = move.to, from = move.to,
card = move.ids, card = move.ids,
} }, visible_data)
elseif move.toArea == Card.PlayerJudge then elseif move.toArea == Card.PlayerJudge then
if move.from ~= move.to and move.fromArea == Card.PlayerJudge then if move.from ~= move.to and move.fromArea == Card.PlayerJudge then
client:appendLog{ client:appendLog({
type = "$LightningMove", type = "$LightningMove",
from = move.from, from = move.from,
to = { move.to }, to = { move.to },
card = move.ids, card = move.ids,
} }, visible_data)
elseif move.from then elseif move.from then
client:appendLog{ client:appendLog({
type = "$PasteCard", type = "$PasteCard",
from = move.from, from = move.from,
to = { move.to }, to = { move.to },
card = move.ids, card = move.ids,
} }, visible_data)
end end
elseif move.toArea == Card.PlayerSpecial then elseif move.toArea == Card.PlayerSpecial then
client:appendLog{ client:appendLog({
type = "$AddToPile", type = "$AddToPile",
arg = move.specialName, arg = move.specialName,
arg2 = #move.ids, arg2 = #move.ids,
from = move.to, from = move.to,
card = move.ids, card = move.ids,
} }, visible_data)
elseif move.fromArea == Card.PlayerEquip then elseif move.fromArea == Card.PlayerEquip then
client:appendLog{ client:appendLog({
type = "$UninstallEquip", type = "$UninstallEquip",
from = move.from, from = move.from,
card = move.ids, card = move.ids,
} }, visible_data)
elseif move.toArea == Card.Processing then elseif move.toArea == Card.Processing then
if move.fromArea == Card.DrawPile and (move.moveReason == fk.ReasonPut or move.moveReason == fk.ReasonJustMove) then if move.fromArea == Card.DrawPile and (move.moveReason == fk.ReasonPut or move.moveReason == fk.ReasonJustMove) then
if hidden then if hidden then
client:appendLog{ client:appendLog({
type = "$ViewCardFromDrawPile", type = "$ViewCardFromDrawPile",
from = move.proposer, from = move.proposer,
arg = #move.ids, arg = #move.ids,
} }, visible_data)
else else
client:appendLog{ client:appendLog({
type = "$TurnOverCardFromDrawPile", type = "$TurnOverCardFromDrawPile",
from = move.proposer, from = move.proposer,
card = move.ids, card = move.ids,
arg = #move.ids, arg = #move.ids,
} }, visible_data)
client:setCardNote(move.ids, { client:setCardNote(move.ids, {
type = "$$TurnOverCard", type = "$$TurnOverCard",
from = move.proposer, from = move.proposer,
@ -676,12 +635,12 @@ local function sendMoveCardLog(move)
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({
type = msgtype, type = msgtype,
from = move.from, from = move.from,
card = move.ids, card = move.ids,
arg = #move.ids, arg = #move.ids,
} }, visible_data)
client:setCardNote(move.ids, { client:setCardNote(move.ids, {
type = "$$PutCard", type = "$$PutCard",
from = move.from, from = move.from,
@ -689,27 +648,27 @@ local function sendMoveCardLog(move)
elseif move.toArea == Card.DiscardPile then elseif move.toArea == Card.DiscardPile then
if move.moveReason == fk.ReasonDiscard then if move.moveReason == fk.ReasonDiscard then
if move.proposer and move.proposer ~= move.from then if move.proposer and move.proposer ~= move.from then
client:appendLog{ client:appendLog({
type = "$DiscardOther", type = "$DiscardOther",
from = move.from, from = move.from,
to = {move.proposer}, to = {move.proposer},
card = move.ids, card = move.ids,
arg = #move.ids, arg = #move.ids,
} }, visible_data)
else else
client:appendLog{ client:appendLog({
type = "$DiscardCards", type = "$DiscardCards",
from = move.from, from = move.from,
card = move.ids, card = move.ids,
arg = #move.ids, arg = #move.ids,
} }, visible_data)
end end
elseif move.moveReason == fk.ReasonPutIntoDiscardPile then elseif move.moveReason == fk.ReasonPutIntoDiscardPile then
client:appendLog{ client:appendLog({
type = "$PutToDiscard", type = "$PutToDiscard",
card = move.ids, card = move.ids,
arg = #move.ids, arg = #move.ids,
} }, visible_data)
end end
-- elseif move.toArea == Card.Void then -- elseif move.toArea == Card.Void then
-- nop -- nop
@ -724,14 +683,23 @@ local function sendMoveCardLog(move)
end end
end end
---@param raw_moves CardsMoveStruct[]
fk.client_callback["MoveCards"] = function(raw_moves) fk.client_callback["MoveCards"] = function(raw_moves)
-- jsonData: CardsMoveStruct[] -- jsonData: CardsMoveStruct[]
ClientInstance:moveCards(raw_moves)
local visible_data = {}
for _, move in ipairs(raw_moves) do
for _, info in ipairs(move.moveInfo) do
local cid = info.cardId
visible_data[tostring(cid)] = Self:cardVisible(cid, move)
end
end
local separated = separateMoves(raw_moves) local separated = separateMoves(raw_moves)
ClientInstance:moveCards(separated)
local merged = mergeMoves(separated) local merged = mergeMoves(separated)
ClientInstance:notifyUI("MoveCards", merged) visible_data.merged = merged
ClientInstance:notifyUI("MoveCards", visible_data)
for _, move in ipairs(merged) do for _, move in ipairs(merged) do
sendMoveCardLog(move) sendMoveCardLog(move, visible_data)
end end
end end
@ -861,6 +829,17 @@ fk.client_callback["AddSkill"] = function(data)
updateLimitSkill(id, skill, target:usedSkillTimes(skill_name, Player.HistoryGame)) updateLimitSkill(id, skill, target:usedSkillTimes(skill_name, Player.HistoryGame))
end end
fk.client_callback["AskForSkillInvoke"] = function(data)
-- jsonData: [ string name, string prompt ]
local h = Fk.request_handlers["AskForSkillInvoke"]:new(Self)
h.prompt = data[2]
h.change = {}
h:setup()
h.scene:notifyUI()
ClientInstance:notifyUI("AskForSkillInvoke", data)
end
fk.client_callback["AskForUseActiveSkill"] = function(data) fk.client_callback["AskForUseActiveSkill"] = function(data)
-- jsonData: [ string skill_name, string prompt, bool cancelable. json extra_data ] -- jsonData: [ string skill_name, string prompt, bool cancelable. json extra_data ]
local skill = Fk.skills[data[1]] local skill = Fk.skills[data[1]]
@ -868,16 +847,44 @@ fk.client_callback["AskForUseActiveSkill"] = function(data)
skill._extra_data = extra_data skill._extra_data = extra_data
Fk.currentResponseReason = extra_data.skillName Fk.currentResponseReason = extra_data.skillName
local h = Fk.request_handlers["AskForUseActiveSkill"]:new(Self)
h.skill_name = data[1]
h.prompt = data[2]
h.cancelable = data[3]
h.extra_data = data[4]
h.change = {}
h:setup()
h.scene:notifyUI()
ClientInstance:notifyUI("AskForUseActiveSkill", data) ClientInstance:notifyUI("AskForUseActiveSkill", data)
end end
fk.client_callback["AskForUseCard"] = function(data) fk.client_callback["AskForUseCard"] = function(data)
-- jsonData: card, pattern, prompt, cancelable, {}
Fk.currentResponsePattern = data[2] Fk.currentResponsePattern = data[2]
local h = Fk.request_handlers["AskForUseCard"]:new(Self)
-- h.skill_name = data[1] (skill_name是给选中的视为技用的)
h.pattern = data[2]
h.prompt = data[3]
h.cancelable = data[4]
h.extra_data = data[5]
h.change = {}
h:setup()
h.scene:notifyUI()
ClientInstance:notifyUI("AskForUseCard", data) ClientInstance:notifyUI("AskForUseCard", data)
end end
fk.client_callback["AskForResponseCard"] = function(data) fk.client_callback["AskForResponseCard"] = function(data)
-- jsonData: card, pattern, prompt, cancelable, {}
Fk.currentResponsePattern = data[2] Fk.currentResponsePattern = data[2]
local h = Fk.request_handlers["AskForResponseCard"]:new(Self)
-- h.skill_name = data[1] (skill_name是给选中的视为技用的)
h.pattern = data[2]
h.prompt = data[3]
h.cancelable = data[4]
h.extra_data = data[5]
h.change = {}
h:setup()
h.scene:notifyUI()
ClientInstance:notifyUI("AskForResponseCard", data) ClientInstance:notifyUI("AskForResponseCard", data)
end end
@ -894,7 +901,7 @@ fk.client_callback["SetPlayerMark"] = function(data)
local spec = Fk.qml_marks[mtype] local spec = Fk.qml_marks[mtype]
if spec then if spec then
local text = spec.how_to_show(mark, value, p) local text = spec.how_to_show(mark, value, p)
if text == "#hidden" then return end if text == "#hidden" then data[3] = 0 end
end end
end end
ClientInstance:notifyUI("SetPlayerMark", data) ClientInstance:notifyUI("SetPlayerMark", data)
@ -1006,10 +1013,11 @@ fk.client_callback["Heartbeat"] = function()
end end
fk.client_callback["ChangeSelf"] = function(data) fk.client_callback["ChangeSelf"] = function(data)
local p = ClientInstance:getPlayerById(data.id) local pid = tonumber(data)
p.player_cards[Player.Hand] = data.handcards local c = ClientInstance
p.special_cards = data.special_cards c.client:changeSelf(pid) -- for qml
ClientInstance:notifyUI("ChangeSelf", data.id) Self = c:getPlayerById(pid)
ClientInstance:notifyUI("ChangeSelf", pid)
end end
fk.client_callback["UpdateQuestSkillUI"] = function(data) fk.client_callback["UpdateQuestSkillUI"] = function(data)
@ -1111,161 +1119,60 @@ end
fk.client_callback["PrintCard"] = function(data) fk.client_callback["PrintCard"] = function(data)
local n, s, num = table.unpack(data) local n, s, num = table.unpack(data)
local cd = Fk:cloneCard(n, s, num) ClientInstance:printCard(n, s, num)
Fk:_addPrintedCard(cd)
end end
fk.client_callback["AddBuddy"] = function(data) fk.client_callback["AddBuddy"] = function(data)
local c = ClientInstance local c = ClientInstance
local id, hand = table.unpack(data) local fromid, id = table.unpack(data)
local from = c:getPlayerById(fromid)
local to = c:getPlayerById(id) local to = c:getPlayerById(id)
Self:addBuddy(to) from:addBuddy(to)
to.player_cards[Player.Hand] = hand
end end
fk.client_callback["RmBuddy"] = function(data) fk.client_callback["RmBuddy"] = function(data)
local c = ClientInstance local c = ClientInstance
local id = data local fromid, id = table.unpack(data)
local from = c:getPlayerById(fromid)
local to = c:getPlayerById(id) local to = c:getPlayerById(id)
Self:removeBuddy(to) from:removeBuddy(to)
to.player_cards[Player.Hand] = table.map(to.player_cards, function() return -1 end)
end
local function loadPlayerSummary(pdata)
local f = fk.client_callback["PropertyUpdate"]
local id = pdata.d[1]
local properties = {
"general", "deputyGeneral", "maxHp", "hp", "shield", "gender", "kingdom",
"dead", "role", "rest", "seat", "phase", "faceup", "chained",
"sealedSlots",
}
for _, k in ipairs(properties) do
if pdata.p[k] ~= nil then
f{ id, k, pdata.p[k] }
end
end
local card_moves = {}
local cards = pdata.c
if #cards[Player.Hand] ~= 0 then
local info = {}
for _, i in ipairs(cards[Player.Hand]) do
table.insert(info, { cardId = i, fromArea = Card.DrawPile })
end
local move = { moveInfo = info, to = id, toArea = Card.PlayerHand }
table.insert(card_moves, move)
end
if #cards[Player.Equip] ~= 0 then
local info = {}
for _, i in ipairs(cards[Player.Equip]) do
table.insert(info, { cardId = i, fromArea = Card.DrawPile })
end
local move = { moveInfo = info, to = id, toArea = Card.PlayerEquip }
table.insert(card_moves, move)
end
if #cards[Player.Judge] ~= 0 then
local info = {}
for _, i in ipairs(cards[Player.Judge]) do
table.insert(info, { cardId = i, fromArea = Card.DrawPile })
end
local move = { moveInfo = info, to = id, toArea = Card.PlayerJudge }
table.insert(card_moves, move)
end
for k, v in pairs(pdata.sc) do
local info = {}
for _, i in ipairs(v) do
table.insert(info, { cardId = i, fromArea = Card.DrawPile })
end
local move = {
moveInfo = info,
to = id,
toArea = Card.PlayerSpecial,
specialName = k,
moveVisible = true,
}
table.insert(card_moves, move)
end
if #card_moves > 0 then
-- TODO: visibility
fk.client_callback["MoveCards"](card_moves)
end
f = fk.client_callback["SetPlayerMark"]
for k, v in pairs(pdata.m) do
f{ id, k, v }
end
f = fk.client_callback["AddSkill"]
for _, v in pairs(pdata.s) do
f{ id, v }
end
f = fk.client_callback["AddCardUseHistory"]
for k, v in pairs(pdata.ch) do
if v[1] > 0 then
f{ k, v[1] }
end
end
f = fk.client_callback["SetSkillUseHistory"]
for k, v in pairs(pdata.sh) do
if v[4] > 0 then
f{ id, k, v[1], 1 }
f{ id, k, v[2], 2 }
f{ id, k, v[3], 3 }
f{ id, k, v[4], 4 }
end
end
end end
local function loadRoomSummary(data) local function loadRoomSummary(data)
local players = data.p local players = data.players
fk.client_callback["StartGame"]("") fk.client_callback["StartGame"]("")
for _, pid in ipairs(data.circle) do for _, pid in ipairs(data.circle) do
if pid ~= data.you then if pid ~= data.you then
fk.client_callback["AddPlayer"](players[tostring(pid)].d) fk.client_callback["AddPlayer"](players[tostring(pid)].setup_data)
end end
end end
fk.client_callback["ArrangeSeats"](data.circle) fk.client_callback["ArrangeSeats"](data.circle)
for _, d in ipairs(data.pc) do ClientInstance:loadJsonObject(data) -- 此处已同步全部数据 剩下就是更新UI
local cd = Fk:cloneCard(table.unpack(d))
Fk:_addPrintedCard(cd)
end
for cid, marks in pairs(data.cm) do for k, v in pairs(ClientInstance.banners) do
for k, v in pairs(marks) do if k[1] == "@" then
Fk:getCardById(tonumber(cid)):setMark(k, v) ClientInstance:notifyUI("SetBanner", { k, v })
ClientInstance:notifyUI("UpdateCard", cid)
end end
end end
for k, v in pairs(data.b) do for _, p in ipairs(ClientInstance.players) do p:sendDataToUI() end
fk.client_callback["SetBanner"]{ k, v }
end
for _, pid in ipairs(data.circle) do ClientInstance:notifyUI("UpdateDrawPile", #ClientInstance.draw_pile)
local pdata = data.p[tostring(pid)] ClientInstance:notifyUI("UpdateRoundNum", data.round_count)
loadPlayerSummary(pdata)
end
ClientInstance:notifyUI("UpdateDrawPile", data.dp)
ClientInstance:notifyUI("UpdateRoundNum", data.rnd)
end end
fk.client_callback["Reconnect"] = function(data) fk.client_callback["Reconnect"] = function(data)
local players = data.p local players = data.players
local setup_data = players[tostring(data.you)].d
local setup_data = players[tostring(data.you)].setup_data
setup(setup_data[1], setup_data[2], setup_data[3]) setup(setup_data[1], setup_data[2], setup_data[3])
fk.client_callback["AddTotalGameTime"]{ setup_data[1], setup_data[5] } fk.client_callback["AddTotalGameTime"]{ setup_data[1], setup_data[5] }
local enter_room_data = data.d local enter_room_data = { data.timeout, data.settings }
table.insert(enter_room_data, 1, #data.circle) table.insert(enter_room_data, 1, #data.circle)
fk.client_callback["EnterLobby"]("") fk.client_callback["EnterLobby"]("")
fk.client_callback["EnterRoom"](enter_room_data) fk.client_callback["EnterRoom"](enter_room_data)
@ -1274,19 +1181,28 @@ fk.client_callback["Reconnect"] = function(data)
end end
fk.client_callback["Observe"] = function(data) fk.client_callback["Observe"] = function(data)
local players = data.p local players = data.players
local setup_data = players[tostring(data.you)].d local setup_data = players[tostring(data.you)].setup_data
setup(setup_data[1], setup_data[2], setup_data[3]) setup(setup_data[1], setup_data[2], setup_data[3])
local enter_room_data = data.d local enter_room_data = { data.timeout, data.settings }
table.insert(enter_room_data, 1, #data.circle) table.insert(enter_room_data, 1, #data.circle)
fk.client_callback["EnterRoom"](enter_room_data) fk.client_callback["EnterRoom"](enter_room_data)
fk.client_callback["StartGame"]("")
loadRoomSummary(data) loadRoomSummary(data)
end end
fk.client_callback["PrepareDrawPile"] = function(data)
local seed = tonumber(data)
ClientInstance:prepareDrawPile(seed)
end
fk.client_callback["ShuffleDrawPile"] = function(data)
local seed = tonumber(data)
ClientInstance:shuffleDrawPile(seed)
end
-- Create ClientInstance (used by Lua) -- Create ClientInstance (used by Lua)
ClientInstance = Client:new() ClientInstance = Client:new()
dofile "lua/client/client_util.lua" dofile "lua/client/client_util.lua"

View File

@ -37,31 +37,13 @@ function GetGeneralDetail(name)
deputyMaxHp = general.deputyMaxHpAdjustedValue, deputyMaxHp = general.deputyMaxHpAdjustedValue,
gender = general.gender, gender = general.gender,
skill = {}, skill = {},
related_skill = {},
companions = general.companions companions = general.companions
} }
for _, s in ipairs(general.skills) do for _, s in ipairs(general.all_skills) do
table.insert(ret.skill, { table.insert(ret.skill, {
name = s.name, name = s[1],
description = Fk:getDescription(s.name) description = Fk:getDescription(s[1]),
}) is_related_skill = s[2],
end
for _, s in ipairs(general.other_skills) do
table.insert(ret.skill, {
name = s,
description = Fk:getDescription(s)
})
end
for _, s in ipairs(general.related_skills) do
table.insert(ret.related_skill, {
name = s.name,
description = Fk:getDescription(s.name)
})
end
for _, s in ipairs(general.related_other_skills) do
table.insert(ret.related_skill, {
name = s,
description = Fk:getDescription(s)
}) })
end end
for _, g in pairs(Fk.generals) do for _, g in pairs(Fk.generals) do
@ -114,7 +96,8 @@ function GetCardData(id, virtualCardForm)
color = card:getColorString(), color = card:getColorString(),
mark = mark, mark = mark,
type = card.type, type = card.type,
subtype = cardSubtypeStrings[card.sub_type] subtype = cardSubtypeStrings[card.sub_type],
known = Self:cardVisible(id)
} }
if card.skillName ~= "" then if card.skillName ~= "" then
local orig = Fk:getCardById(id, true) local orig = Fk:getCardById(id, true)
@ -352,6 +335,49 @@ function CanUseCardToTarget(card, to_select, selected, extra_data_str)
return ret return ret
end end
---@param card string | integer
---@param to_select integer @ id of the target
---@param selected integer[] @ ids of selected targets
---@param selectable bool
---@param extra_data_str string @ extra data
function GetUseCardTargetTip(card, to_select, selected, selectable, extra_data_str)
local extra_data = extra_data_str == "" and nil or json.decode(extra_data_str)
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 ActiveTargetTip(t.skill, to_select, selected, t.subcards, selectable, extra_data)
end
local ret
local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or Util.DummyTable
for _, skill in ipairs(status_skills) do
ret = ret or {}
if #ret > 4 then
return ret
end
local tip = skill:getTargetTip(Self, to_select, selected, selected_cards, c, selectable, extra_data)
if type(tip) == "string" then
table.insert(ret, { content = ret, type = "normal" })
elseif type(tip) == "table" then
table.insertTable(ret, tip)
end
end
ret = ret or {}
local tip = c.skill:targetTip(to_select, selected, selected_cards, c, selectable, extra_data)
if type(tip) == "string" then
table.insert(ret, { content = ret, type = "normal" })
elseif type(tip) == "table" then
table.insertTable(ret, tip)
end
return ret
end
---@param card string | integer ---@param card string | integer
---@param to_select integer @ id of a card not selected ---@param to_select integer @ id of a card not selected
---@param selected_targets integer[] @ ids of selected players ---@param selected_targets integer[] @ ids of selected players
@ -430,6 +456,18 @@ function GetSkillData(skill_name)
} }
end end
function GetSkillStatus(skill_name)
local skill = Fk.skills[skill_name]
local locked = not skill:isEffectable(Self)
if not locked and type(Self:getMark(MarkEnum.InvalidSkills)) == "table" and table.contains(Self:getMark(MarkEnum.InvalidSkills), skill_name) then
locked = true
end
return {
locked = locked, ---@type boolean
times = skill:getTimes()
}
end
function ActiveCanUse(skill_name, extra_data_str) function ActiveCanUse(skill_name, extra_data_str)
local extra_data = extra_data_str == "" and nil or json.decode(extra_data_str) local extra_data = extra_data_str == "" and nil or json.decode(extra_data_str)
local skill = Fk.skills[skill_name] local skill = Fk.skills[skill_name]
@ -505,6 +543,46 @@ function ActiveTargetFilter(skill_name, to_select, selected, selected_cards, ext
return ret return ret
end end
function ActiveTargetTip(skill_name, to_select, selected, selected_cards, selectable, extra_data)
local skill = Fk.skills[skill_name]
local ret
if skill then
if skill:isInstanceOf(ActiveSkill) then
ret = skill:targetTip(to_select, selected, selected_cards, nil, selectable)
if type(ret) == "string" then
ret = { { content = ret, type = "normal" } }
end
elseif skill:isInstanceOf(ViewAsSkill) then
local card = skill:viewAs(selected_cards)
if card then
local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or Util.DummyTable
for _, skill in ipairs(status_skills) do
ret = ret or {}
if #ret > 4 then
return ret
end
local tip = skill:getTargetTip(Self, to_select, selected, selected_cards, card, selectable, extra_data)
if type(tip) == "string" then
table.insert(ret, { content = ret, type = "normal" })
elseif type(tip) == "table" then
table.insertTable(ret, tip)
end
end
ret = ret or {}
local tip = card.skill:targetTip(to_select, selected, selected_cards, card, selectable, extra_data)
if type(tip) == "string" then
table.insert(ret, { content = ret, type = "normal" })
elseif type(tip) == "table" then
table.insertTable(ret, tip)
end
end
end
end
return ret
end
function ActiveFeasible(skill_name, selected, selected_cards) function ActiveFeasible(skill_name, selected, selected_cards)
local skill = Fk.skills[skill_name] local skill = Fk.skills[skill_name]
local ret = false local ret = false
@ -654,12 +732,6 @@ function SetInteractionDataOfSkill(skill_name, data)
end end
end end
function ChangeSelf(pid)
local c = ClientInstance
c.client:changeSelf(pid) -- for qml
Self = c:getPlayerById(pid)
end
function GetPlayerHandcards(pid) function GetPlayerHandcards(pid)
local c = ClientInstance local c = ClientInstance
local p = c:getPlayerById(pid) local p = c:getPlayerById(pid)
@ -767,6 +839,10 @@ function GetCardProhibitReason(cid, method, pattern)
end end
end end
function CanSortHandcards(pid)
return ClientInstance:getPlayerById(pid):getMark(MarkEnum.SortProhibited) == 0
end
function PoxiPrompt(poxi_type, data, extra_data) function PoxiPrompt(poxi_type, data, extra_data)
local poxi = Fk.poxi_methods[poxi_type] local poxi = Fk.poxi_methods[poxi_type]
if not poxi or not poxi.prompt then return "" end if not poxi or not poxi.prompt then return "" end
@ -810,4 +886,91 @@ function ReloadPackage(path)
Fk:reloadPackage(path) Fk:reloadPackage(path)
end end
function GetPendingSkill()
local h = ClientInstance.current_request_handler
local reqActive = Fk.request_handlers["AskForUseActiveSkill"]
return h and h:isInstanceOf(reqActive) and
(h.selected_card == nil and h.skill_name) or ""
end
function RevertSelection()
local h = ClientInstance.current_request_handler ---@type ReqActiveSkill
local reqActive = Fk.request_handlers["AskForUseActiveSkill"]
if not (h and h:isInstanceOf(reqActive) and h.pendings) then return end
h.change = {}
-- 1. 取消选中所有已选 2. 尝试选中所有之前未选的牌
local unselectData = { selected = false }
local selectData = { selected = true }
local to_select = {}
for cid, cardItem in pairs(h.scene:getAllItems("CardItem")) do
if table.contains(h.pendings, cid) then
h:selectCard(cid, unselectData)
else
table.insert(to_select, cardItem)
end
end
for _, cardItem in ipairs(to_select) do
if cardItem.enabled then
h:selectCard(cardItem.id, selectData)
end
end
h.scene:notifyUI()
end
function UpdateRequestUI(elemType, id, action, data)
local h = ClientInstance.current_request_handler
h.change = {}
local finish = h:update(elemType, id, action, data)
if not finish then
h.scene:notifyUI()
else
h:_finish()
end
end
function FinishRequestUI()
local h = ClientInstance.current_request_handler
if h then
h:_finish()
end
end
-- TODO 传参带上cardMoveData...
function CardVisibility(cardId, move)
local player = Self
local card = Fk:getCardById(cardId)
if not card then return false end
return player:cardVisible(cardId, move)
end
function RoleVisibility(targetId)
local player = Self
local target = ClientInstance:getPlayerById(targetId)
if not target then return false end
return player:roleVisible(target)
end
function IsMyBuddy(me, other)
local from = ClientInstance:getPlayerById(me)
local to = ClientInstance:getPlayerById(other)
return from and to and from:isBuddy(to)
end
-- special_name 为nil时是手牌
function HasVisibleCard(me, other, special_name)
local from = ClientInstance:getPlayerById(me)
local to = ClientInstance:getPlayerById(other)
if not (from and to) then return false end
local ids
if not special_name then ids = to:getCardIds("h")
else ids = to:getPile(special_name) end
for _, id in ipairs(ids) do
if from:cardVisible(id) then
return true
end
end
return false
end
dofile "lua/client/i18n/init.lua" dofile "lua/client/i18n/init.lua"

View File

@ -2,16 +2,84 @@
---@class ClientPlayer: Player ---@class ClientPlayer: Player
---@field public player fk.Player ---@field public player fk.Player
---@field public known_cards integer[]
---@field public global_known_cards integer[]
local ClientPlayer = Player:subclass("ClientPlayer") local ClientPlayer = Player:subclass("ClientPlayer")
function ClientPlayer:initialize(cp) function ClientPlayer:initialize(cp)
Player.initialize(self) Player.initialize(self)
self.id = cp:getId() self.id = cp:getId()
self.player = cp self.player = cp
self.known_cards = {} -- you know he/she have this card, but not shown end
self.global_known_cards = {} -- card that visible to all players
local function fillMoveData(card_moves, visible_data, self, area, specialName)
local cards = self.player_cards
local ids = cards[area]
if specialName then ids = ids[specialName] end
if #ids ~= 0 then
for _, id in ipairs(ids) do
visible_data[tostring(id)] = Self:cardVisible(id)
end
local move = {
ids = ids,
to = self.id,
fromArea = Card.DrawPile,
toArea = area,
specialName = specialName,
}
table.insert(card_moves, move)
end
end
-- 仅用于断线重连或者旁观时将数据同步到qml界面中
function ClientPlayer:sendDataToUI()
local c = ClientInstance
local id = self.id
for _, k in ipairs(self.property_keys) do
c:notifyUI("PropertyUpdate", { id, k, self[k] })
end
local card_moves = {}
local visible_data = {}
for _, area in ipairs { Player.Hand, Player.Equip, Player.Judge } do
fillMoveData(card_moves, visible_data, self, area)
end
for name in pairs(self.special_cards) do
fillMoveData(card_moves, visible_data, self, Card.PlayerSpecial, name)
end
if #card_moves > 0 then
visible_data.merged = card_moves
c:notifyUI("MoveCards", visible_data)
end
for mark, value in pairs(self.mark) do
if mark[1] == "@" then
if mark:startsWith("@[") and mark:find(']') then
local close = mark:find(']')
local mtype = mark:sub(3, close - 1)
local spec = Fk.qml_marks[mtype]
if spec then
local text = spec.how_to_show(mark, value, p)
if text == "#hidden" then value = 0 end
end
end
c:notifyUI("SetPlayerMark", { id, mark, value })
end
end
for _, skill in ipairs(self.player_skills) do
if skill.visible then
c:notifyUI("AddSkill", { id, skill.name })
end
end
local f = fk.client_callback["SetSkillUseHistory"]
for k, v in pairs(self.skillUsedHistory) do
if v[4] > 0 then
f{ id, k, v[1], 1 }
f{ id, k, v[2], 2 }
f{ id, k, v[3], 3 }
f{ id, k, v[4], 4 }
end
end
end end
return ClientPlayer return ClientPlayer

View File

@ -34,6 +34,8 @@ Fk:loadTranslationTable({
-- ["Refresh Room List"] = "刷新房间列表", -- ["Refresh Room List"] = "刷新房间列表",
["Disable Extension"] = "Please ignore this checkbox", ["Disable Extension"] = "Please ignore this checkbox",
-- ["Filter"] = "筛选",
-- ["Room ID"] = "房间ID",
-- ["Create Room"] = "创建房间", -- ["Create Room"] = "创建房间",
-- ["Room Name"] = "房间名字", -- ["Room Name"] = "房间名字",
["$RoomName"] = "%1's room", ["$RoomName"] = "%1's room",
@ -42,9 +44,14 @@ Fk:loadTranslationTable({
-- ["No enough generals"] = "可用武将不足!", -- ["No enough generals"] = "可用武将不足!",
["Operation timeout"] = "Operation timeout (sec)", ["Operation timeout"] = "Operation timeout (sec)",
["Luck Card Times"] = "Luck card count", ["Luck Card Times"] = "Luck card count",
["Has Password"] = "(PW) ", -- ["Has Password"] = "有密码",
-- ["No Password"] = "无密码",
-- ["Room Password"] = "房间密码", -- ["Room Password"] = "房间密码",
-- ["Please input room's password"] = "请输入房间的密码", -- ["Please input room's password"] = "请输入房间的密码",
-- ["Room Fullness"] = "房间满员",
-- ["Full"] = "已满",
-- ["Not Full"] = "未满",
-- ["Room Capacity"] = "人数上限",
["Add Robot"] = "Add robot", ["Add Robot"] = "Add robot",
["Start Game"] = "Start game", ["Start Game"] = "Start game",
-- ["Ready"] = "准备", -- ["Ready"] = "准备",
@ -89,6 +96,7 @@ Fk:loadTranslationTable({
["$OnlineInfo"] = "Lobby: %1, Online: %2", ["$OnlineInfo"] = "Lobby: %1, Online: %2",
-- ["Overview"] = "一览",
["Generals Overview"] = "Characters", ["Generals Overview"] = "Characters",
["Cards Overview"] = "Cards", ["Cards Overview"] = "Cards",
["Special card skills:"] = "<b>Special use method:</b>", ["Special card skills:"] = "<b>Special use method:</b>",
@ -97,7 +105,7 @@ Fk:loadTranslationTable({
-- ["Female Audio"] = "女性音效", -- ["Female Audio"] = "女性音效",
-- ["Equip Effect Audio"] = "效果音效", -- ["Equip Effect Audio"] = "效果音效",
-- ["Equip Use Audio"] = "使用音效", -- ["Equip Use Audio"] = "使用音效",
["Scenarios Overview"] = "Game modes", ["Modes Overview"] = "Game modes",
-- ["Replay"] = "录像", -- ["Replay"] = "录像",
-- ["Replay Manager"] = "来欣赏潇洒的录像吧!", -- ["Replay Manager"] = "来欣赏潇洒的录像吧!",
-- ["Replay from File"] = "从文件打开", -- ["Replay from File"] = "从文件打开",
@ -142,6 +150,8 @@ Fk:loadTranslationTable({
-- ["Quit"] = "退出", -- ["Quit"] = "退出",
["BanGeneral"] = "Ban", ["BanGeneral"] = "Ban",
["ResumeGeneral"] = "Unban", ["ResumeGeneral"] = "Unban",
-- ["Enable"] = "启用",
-- ["Prohibit"] = "禁",
["BanPackage"] = "Ban packages", ["BanPackage"] = "Ban packages",
["$BanPkgHelp"] = "Banning packages", ["$BanPkgHelp"] = "Banning packages",
["$BanCharaHelp"] = "Banning characters", ["$BanCharaHelp"] = "Banning characters",
@ -153,6 +163,11 @@ Fk:loadTranslationTable({
["Designer"] = "Designer: ", ["Designer"] = "Designer: ",
["Voice Actor"] = "Voice Actor: ", ["Voice Actor"] = "Voice Actor: ",
["Illustrator"] = "Illustrator: ", ["Illustrator"] = "Illustrator: ",
-- ["Hidden General"] = "隐藏武将",
["Audio Code Copy Success"] = "Audio code has been copied to your clipboard",
["Audio Text Copy Success"] = "Audio text has been copied to your clipboard",
-- ["Copy Audio Code"] = "复制语音代码",
-- ["Copy Audio Text"] = "复制语音文本",
["$WelcomeToLobby"] = "Welcome to FreeKill lobby!", ["$WelcomeToLobby"] = "Welcome to FreeKill lobby!",
["GameMode"] = "Game mode: ", ["GameMode"] = "Game mode: ",
@ -253,8 +268,12 @@ Fk:loadTranslationTable({
-- ["Trust"] = "托管", -- ["Trust"] = "托管",
["Sort Cards"] = "Sort", ["Sort Cards"] = "Sort",
["Sort by Type"] = "by Type",
["Sort by Number"] = "by Num",
["Sort by Suit"] = "by Suit",
-- ["Chat"] = "聊天", -- ["Chat"] = "聊天",
["Log"] = "Game Log", ["Log"] = "Game Log",
-- ["Return to Bottom"] = "回到底部",
-- ["Trusting ..."] = "托管中 ...", -- ["Trusting ..."] = "托管中 ...",
-- ["Observing ..."] = "旁观中 ...", -- ["Observing ..."] = "旁观中 ...",
@ -404,6 +423,7 @@ Fk:loadTranslationTable({
-- skill -- skill
["#InvokeSkill"] = '%from used skill "%arg"', ["#InvokeSkill"] = '%from used skill "%arg"',
["#InvokeSkillTo"] = '%from used skill "%arg" to %to',
-- judge -- judge
["#StartJudgeReason"] = "%from started a judgement (%arg)", ["#StartJudgeReason"] = "%from started a judgement (%arg)",

View File

@ -29,6 +29,7 @@ Fk:loadTranslationTable{
["Hide unselectable cards"] = "下移不可选卡牌", ["Hide unselectable cards"] = "下移不可选卡牌",
["Hide observer chatter"] = "屏蔽旁观者聊天", ["Hide observer chatter"] = "屏蔽旁观者聊天",
["Rotate table card"] = "处理区的牌随机旋转", ["Rotate table card"] = "处理区的牌随机旋转",
["Hide presents"] = "屏蔽送花砸蛋",
["Ban General Settings"] = "禁将", ["Ban General Settings"] = "禁将",
["Set as Avatar"] = "设为头像", ["Set as Avatar"] = "设为头像",
["Search"] = "搜索", ["Search"] = "搜索",
@ -37,7 +38,9 @@ Fk:loadTranslationTable{
["Refresh Room List"] = "刷新房间列表 (%1个房间)", ["Refresh Room List"] = "刷新房间列表 (%1个房间)",
["Disable Extension"] = "禁用Lua拓展 (重启后生效)", ["Disable Extension"] = "禁用Lua拓展 (重启后生效)",
["Filter"] = "筛选",
["Create Room"] = "创建房间", ["Create Room"] = "创建房间",
["Room ID"] = "房间ID",
["Room Name"] = "房间名字", ["Room Name"] = "房间名字",
["$RoomName"] = "%1的房间", ["$RoomName"] = "%1的房间",
["Player num"] = "玩家数目", ["Player num"] = "玩家数目",
@ -45,9 +48,14 @@ Fk:loadTranslationTable{
["No enough generals"] = "可用武将不足!", ["No enough generals"] = "可用武将不足!",
["Operation timeout"] = "操作时长(秒)", ["Operation timeout"] = "操作时长(秒)",
["Luck Card Times"] = "手气卡次数", ["Luck Card Times"] = "手气卡次数",
["Has Password"] = "(有密码)", ["Has Password"] = "有密码",
["No Password"] = "无密码",
["Room Password"] = "房间密码", ["Room Password"] = "房间密码",
["Please input room's password"] = "请输入房间的密码", ["Please input room's password"] = "请输入房间的密码",
["Room Fullness"] = "房间满员",
["Full"] = "已满",
["Not Full"] = "未满",
["Room Capacity"] = "人数上限",
["Add Robot"] = "添加机器人", ["Add Robot"] = "添加机器人",
["Start Game"] = "开始游戏", ["Start Game"] = "开始游戏",
["Ready"] = "准备", ["Ready"] = "准备",
@ -97,6 +105,7 @@ Fk:loadTranslationTable{
["$OnlineInfo"] = "大厅人数:%1总在线人数%2", ["$OnlineInfo"] = "大厅人数:%1总在线人数%2",
["Overview"] = "一览",
["Generals Overview"] = "武将一览", ["Generals Overview"] = "武将一览",
["Cards Overview"] = "卡牌一览", ["Cards Overview"] = "卡牌一览",
["Special card skills:"] = "<b>卡牌的特殊用法:</b>", ["Special card skills:"] = "<b>卡牌的特殊用法:</b>",
@ -105,7 +114,7 @@ Fk:loadTranslationTable{
["Female Audio"] = "女性音效", ["Female Audio"] = "女性音效",
["Equip Effect Audio"] = "效果音效", ["Equip Effect Audio"] = "效果音效",
["Equip Use Audio"] = "使用音效", ["Equip Use Audio"] = "使用音效",
["Scenarios Overview"] = "玩法一览", ["Modes Overview"] = "玩法一览",
["Replay"] = "录像", ["Replay"] = "录像",
["Replay Manager"] = "来欣赏潇洒的录像吧!", ["Replay Manager"] = "来欣赏潇洒的录像吧!",
["Replay from File"] = "从文件打开", ["Replay from File"] = "从文件打开",
@ -121,6 +130,8 @@ Fk:loadTranslationTable{
https://github.com/Notify-ctrl/FreeKill https://github.com/Notify-ctrl/FreeKill
使 https://fkbook-all-in-one.readthedocs.io
--- ---
Notify Ho-spair Notify Ho-spair
@ -196,6 +207,8 @@ FreeKill使用的是libgit2的C API与此同时使用Git完成拓展包的下
-- ["Quit"] = "退出", -- ["Quit"] = "退出",
["BanGeneral"] = "禁将", ["BanGeneral"] = "禁将",
["ResumeGeneral"] = "解禁", ["ResumeGeneral"] = "解禁",
["Enable"] = "启用",
["Prohibit"] = "",
["BanPackage"] = "禁拓展包", ["BanPackage"] = "禁拓展包",
["$BanPkgHelp"] = "正在禁用拓展包", ["$BanPkgHelp"] = "正在禁用拓展包",
["$BanCharaHelp"] = "正在禁用武将", ["$BanCharaHelp"] = "正在禁用武将",
@ -207,6 +220,11 @@ FreeKill使用的是libgit2的C API与此同时使用Git完成拓展包的下
["Designer"] = "设计:", ["Designer"] = "设计:",
["Voice Actor"] = "配音:", ["Voice Actor"] = "配音:",
["Illustrator"] = "画师:", ["Illustrator"] = "画师:",
["Hidden General"] = "隐藏武将",
["Audio Code Copy Success"] = "语音代码已复制到剪贴板",
["Audio Text Copy Success"] = "语音文本已复制到剪贴板",
["Copy Audio Code"] = "复制语音代码",
["Copy Audio Text"] = "复制语音文本",
["$WelcomeToLobby"] = "欢迎进入新月杀游戏大厅!", ["$WelcomeToLobby"] = "欢迎进入新月杀游戏大厅!",
["GameMode"] = "游戏模式:", ["GameMode"] = "游戏模式:",
@ -229,7 +247,7 @@ FreeKill使用的是libgit2的C API与此同时使用Git完成拓展包的下
["#PlayCard"] = "出牌阶段,请使用一张牌", ["#PlayCard"] = "出牌阶段,请使用一张牌",
["#AskForGeneral"] = "请选择 1 名武将", ["#AskForGeneral"] = "请选择 1 名武将",
["#AskForSkillInvoke"] = "你想发动〖%1〗吗", ["#AskForSkillInvoke"] = "你想发动〖%1〗吗",
["#AskForLuckCard"] = "你想使用手气卡吗?还可以使用 %1 次,剩余手气卡∞张", ["#AskForLuckCard"] = "你想使用手气卡吗?还可以使用 %arg 次,剩余手气卡∞张",
["AskForLuckCard"] = "手气卡", ["AskForLuckCard"] = "手气卡",
["#AskForChoice"] = "%1请选择", ["#AskForChoice"] = "%1请选择",
["#AskForChoices"] = "%1请选择", ["#AskForChoices"] = "%1请选择",
@ -307,8 +325,12 @@ FreeKill使用的是libgit2的C API与此同时使用Git完成拓展包的下
["Trust"] = "托管", ["Trust"] = "托管",
["Sort Cards"] = "牌序", ["Sort Cards"] = "牌序",
["Sort by Type"] = "按类型",
["Sort by Number"] = "按点数",
["Sort by Suit"] = "按花色",
["Chat"] = "聊天", ["Chat"] = "聊天",
["Log"] = "战报", ["Log"] = "战报",
["Return to Bottom"] = "回到底部",
["Trusting ..."] = "托管中 ...", ["Trusting ..."] = "托管中 ...",
["Observing ..."] = "旁观中 ...", ["Observing ..."] = "旁观中 ...",
["Resting, don't leave!"] = "稍后你可返回战局,不要离开", ["Resting, don't leave!"] = "稍后你可返回战局,不要离开",
@ -360,16 +382,20 @@ Fk:loadTranslationTable{
["hp_lost"] = "失去体力", ["hp_lost"] = "失去体力",
["lose_hp"] = "失去体力", ["lose_hp"] = "失去体力",
["phase_roundstart"] = "回合开始",
["phase_start"] = "准备阶段", ["phase_start"] = "准备阶段",
["phase_judge"] = "判定阶段", ["phase_judge"] = "判定阶段",
["phase_draw"] = "摸牌阶段", ["phase_draw"] = "摸牌阶段",
["phase_play"] = "出牌阶段", ["phase_play"] = "出牌阶段",
["phase_discard"] = "弃牌阶段", ["phase_discard"] = "弃牌阶段",
["phase_finish"] = "结束阶段", ["phase_finish"] = "结束阶段",
["phase_notactive"] = "回合外",
["phase_phasenone"] = "临时阶段",
["chained"] = "横置", ["chained"] = "横置",
["un-chained"] = "重置", ["un-chained"] = "重置",
["reset-general"] = "复原", ["reset-general"] = "复原",
["reset"] = "复原武将牌",
["yang"] = "", ["yang"] = "",
["yin"] = "", ["yin"] = "",
@ -396,6 +422,7 @@ Fk:loadTranslationTable{
["Distance"] = "距离", ["Distance"] = "距离",
["Judge"] = "判定", ["Judge"] = "判定",
["Retrial"] = "改判", ["Retrial"] = "改判",
["Pindian"] = "拼点",
["_sealed"] = "废除", ["_sealed"] = "废除",
["weapon_sealed"] = "武器栏废除", ["weapon_sealed"] = "武器栏废除",
@ -408,6 +435,8 @@ Fk:loadTranslationTable{
["DefensiveRideSlot"] = "防御坐骑栏", ["DefensiveRideSlot"] = "防御坐骑栏",
["TreasureSlot"] = "宝物栏", ["TreasureSlot"] = "宝物栏",
["JudgeSlot"] = "判定区", ["JudgeSlot"] = "判定区",
["skill"] = "技能",
} }
-- related to sendLog -- related to sendLog
@ -474,9 +503,12 @@ Fk:loadTranslationTable{
["#ResponsePlayV0Card"] = "%from 打出了 %arg", ["#ResponsePlayV0Card"] = "%from 打出了 %arg",
["#FilterCard"] = "由于 %arg 的效果,与 %from 相关的 %arg2 被视为了 %arg3", ["#FilterCard"] = "由于 %arg 的效果,与 %from 相关的 %arg2 被视为了 %arg3",
["#AddTargetsBySkill"] = "用于 %arg 的效果,%from 使用的 %arg2 增加了目标 %to",
["#RemoveTargetsBySkill"] = "用于 %arg 的效果,%from 使用的 %arg2 取消了目标 %to",
-- skill -- skill
["#InvokeSkill"] = "%from 发动了〖%arg〗", ["#InvokeSkill"] = "%from 发动了〖%arg〗",
["#InvokeSkillTo"] = "%from 对 %to 发动了〖%arg〗",
-- judge -- judge
["#StartJudgeReason"] = "%from 开始了 %arg 的判定", ["#StartJudgeReason"] = "%from 开始了 %arg 的判定",
@ -488,6 +520,7 @@ Fk:loadTranslationTable{
["#TurnOver"] = "%from 将武将牌翻面,现在是 %arg", ["#TurnOver"] = "%from 将武将牌翻面,现在是 %arg",
["face_up"] = "正面朝上", ["face_up"] = "正面朝上",
["face_down"] = "背面朝上", ["face_down"] = "背面朝上",
["turnOver"] = "翻面",
-- damage, heal and lose HP -- damage, heal and lose HP
["#Damage"] = "%to 对 %from 造成了 %arg 点 %arg2 伤害", ["#Damage"] = "%to 对 %from 造成了 %arg 点 %arg2 伤害",

View File

@ -1,52 +0,0 @@
-- 作Room和Client的基类这二者有不少共通之处
---@class AbstractRoom : Object
---@fiele public players Player[] @ 房内参战角色们
---@field public alive_players Player[] @ 所有存活玩家的数组
---@field public observers Player[] @ 看戏的
---@field public current Player @ 当前行动者
---@field public status_skills table<class, Skill[]> @ 这个房间中含有的状态技列表
---@field public filtered_cards table<integer, Card> @ 见于Engine其实在这
---@field public printed_cards table<integer, Card> @ 同上
---@field public skill_costs table<string, any> @ 用来存skill.cost_data
---@field public card_marks table<integer, any> @ 用来存实体卡的card.mark
---@field public banners table<string, any> @ 全局mark
local AbstractRoom = class("AbstractRoom")
function AbstractRoom:initialize()
self.players = {}
self.alive_players = {}
self.observers = {}
self.current = nil
self.status_skills = {}
for class, skills in pairs(Fk.global_status_skill) do
self.status_skills[class] = {table.unpack(skills)}
end
self.filtered_cards = {}
self.printed_cards = {}
self.skill_costs = {}
self.card_marks = {}
self.banners = {}
end
-- 仅供注释,其余空函数一样
---@param id integer
---@return Player?
function AbstractRoom:getPlayerById(id) end
--- 获取一张牌所处的区域。
---@param cardId integer | Card @ 要获得区域的那张牌可以是Card或者一个id
---@return CardArea @ 这张牌的区域
function AbstractRoom:getCardArea(cardId) return Card.Unknown end
function AbstractRoom:setBanner(name, value)
if value == 0 then value = nil end
self.banners[name] = value
end
function AbstractRoom:getBanner(name)
return self.banners[name]
end
return AbstractRoom

View File

@ -20,7 +20,7 @@
---@field public skillName string @ 虚拟牌的技能名 for virtual cards ---@field public skillName string @ 虚拟牌的技能名 for virtual cards
---@field private _skillName string ---@field private _skillName string
---@field public skillNames string[] @ 虚拟牌的技能名们(一张虚拟牌可能有多个技能名,如芳魂、龙胆、朱雀羽扇) ---@field public skillNames string[] @ 虚拟牌的技能名们(一张虚拟牌可能有多个技能名,如芳魂、龙胆、朱雀羽扇)
---@field public skill Skill @ 技能(用于实现卡牌效果) ---@field public skill ActiveSkill @ 技能(用于实现卡牌效果)
---@field public special_skills? string[] @ 衍生技能,如重铸 ---@field public special_skills? string[] @ 衍生技能,如重铸
---@field public is_damage_card boolean @ 是否为会造成伤害的牌 ---@field public is_damage_card boolean @ 是否为会造成伤害的牌
---@field public multiple_targets boolean @ 是否为指定多个目标的牌 ---@field public multiple_targets boolean @ 是否为指定多个目标的牌
@ -514,4 +514,12 @@ function Card.static:getIdList(c)
return ret return ret
end end
--- 获得卡牌的标记并初始化为表
---@param mark string @ 标记
---@return table
function Card:getTableMark(mark)
local ret = self:getMark(mark)
return type(ret) == "table" and ret or {}
end
return Card return Card

View File

@ -29,10 +29,13 @@
---@field public printed_cards table<integer, Card> @ 被某些房间现场打印的卡牌id都是负数且从-2开始 ---@field public printed_cards table<integer, Card> @ 被某些房间现场打印的卡牌id都是负数且从-2开始
---@field private kingdoms string[] @ 总势力 ---@field private kingdoms string[] @ 总势力
---@field private kingdom_map table<string, string[]> @ 势力映射表 ---@field private kingdom_map table<string, string[]> @ 势力映射表
---@field private damage_nature table<any, table> @ 伤害映射表
---@field private _custom_events any[] @ 自定义事件列表 ---@field private _custom_events any[] @ 自定义事件列表
---@field public poxi_methods table<string, PoxiSpec> @ “魄袭”框操作方法表 ---@field public poxi_methods table<string, PoxiSpec> @ “魄袭”框操作方法表
---@field public qml_marks table<string, QmlMarkSpec> @ 自定义Qml标记的表 ---@field public qml_marks table<string, QmlMarkSpec> @ 自定义Qml标记的表
---@field public mini_games table<string, MiniGameSpec> @ 自定义多人交互表 ---@field public mini_games table<string, MiniGameSpec> @ 自定义多人交互表
---@field public request_handlers table<string, RequestHandler> @ 请求处理程序
---@field public target_tips table<string, TargetTipSpec> @ 选择目标提示对应表
local Engine = class("Engine") local Engine = class("Engine")
--- Engine的构造函数。 --- Engine的构造函数。
@ -69,13 +72,18 @@ function Engine:initialize()
self.game_mode_disabled = {} self.game_mode_disabled = {}
self.kingdoms = {} self.kingdoms = {}
self.kingdom_map = {} self.kingdom_map = {}
self.damage_nature = { [fk.NormalDamage] = { "normal_damage", false } }
self._custom_events = {} self._custom_events = {}
self.poxi_methods = {} self.poxi_methods = {}
self.qml_marks = {} self.qml_marks = {}
self.mini_games = {} self.mini_games = {}
self.request_handlers = {}
self.target_tips = {}
self:loadPackages() self:loadPackages()
self:setLords()
self:loadDisabled() self:loadDisabled()
self:loadRequestHandlers()
self:addSkills(AuxSkills) self:addSkills(AuxSkills)
end end
@ -201,15 +209,14 @@ end
--- 标包和标准卡牌包比较特殊,它们永远会在第一个加载。 --- 标包和标准卡牌包比较特殊,它们永远会在第一个加载。
---@return nil ---@return nil
function Engine:loadPackages() function Engine:loadPackages()
local new_core = false
if FileIO.pwd():endsWith("packages/freekill-core") then if FileIO.pwd():endsWith("packages/freekill-core") then
new_core = true UsingNewCore = true
FileIO.cd("../..") FileIO.cd("../..")
end end
local directories = FileIO.ls("packages") local directories = FileIO.ls("packages")
-- load standard & standard_cards first -- load standard & standard_cards first
if new_core then if UsingNewCore then
self:loadPackage(require("packages.freekill-core.standard")) self:loadPackage(require("packages.freekill-core.standard"))
self:loadPackage(require("packages.freekill-core.standard_cards")) self:loadPackage(require("packages.freekill-core.standard_cards"))
self:loadPackage(require("packages.freekill-core.maneuvering")) self:loadPackage(require("packages.freekill-core.maneuvering"))
@ -248,7 +255,7 @@ function Engine:loadPackages()
end end
end end
if new_core then if UsingNewCore then
FileIO.cd("packages/freekill-core") FileIO.cd("packages/freekill-core")
end end
end end
@ -269,6 +276,15 @@ function Engine:loadDisabled()
end end
end end
--- 载入响应事件
function Engine:loadRequestHandlers()
self.request_handlers["AskForSkillInvoke"] = require 'core.request_type.invoke'
self.request_handlers["AskForUseActiveSkill"] = require 'core.request_type.active_skill'
self.request_handlers["AskForResponseCard"] = require 'core.request_type.response_card'
self.request_handlers["AskForUseCard"] = require 'core.request_type.use_card'
self.request_handlers["PlayCard"] = require 'core.request_type.play_card'
end
--- 向翻译表中加载新的翻译表。 --- 向翻译表中加载新的翻译表。
---@param t table @ 要加载的翻译表,这是一个 原文 --> 译文 的键值对表 ---@param t table @ 要加载的翻译表,这是一个 原文 --> 译文 的键值对表
---@param lang? string @ 目标语言默认为zh_CN ---@param lang? string @ 目标语言默认为zh_CN
@ -349,9 +365,9 @@ function Engine:addGeneral(general)
table.insert(self.same_generals[tName], general.name) table.insert(self.same_generals[tName], general.name)
end end
if table.find(general.skills, function(s) return s.lordSkill end) then -- if table.find(general.skills, function(s) return s.lordSkill end) then
table.insert(self.lords, general.name) -- table.insert(self.lords, general.name)
end -- end
end end
--- 加载一系列武将。 --- 加载一系列武将。
@ -363,6 +379,19 @@ function Engine:addGenerals(generals)
end end
end end
--- 为所有武将加载主公技和主公判定
function Engine:setLords()
for _, general in pairs(self.generals) do
local other_skills = table.map(general.other_skills, Util.Name2SkillMapper)
local skills = table.connect(general.skills, other_skills)
for _, skill in ipairs(skills) do
if skill.lordSkill then
table.insert(self.lords, general.name)
end
end
end
end
--- 为一个势力添加势力映射 --- 为一个势力添加势力映射
--- ---
--- 这意味着原势力登场时必须改变为添加的几个势力之一(须存在) --- 这意味着原势力登场时必须改变为添加的几个势力之一(须存在)
@ -387,6 +416,50 @@ function Engine:getKingdomMap(kingdom)
return ret return ret
end end
--- 注册一个伤害
---@param nature string | number @ 伤害ID
---@param name string @ 属性伤害名
---@param can_chain bool @ 是否可传导
function Engine:addDamageNature(nature, name, can_chain)
assert(table.contains({ "string", "number" }, type(nature)), "Must use string or number as nature!")
assert(type(name) == "string", "Must use string as this damage nature's name!")
if can_chain == nil then can_chain = true end
self.damage_nature[nature] = { name, can_chain }
end
--- 返回伤害列表
---@return table @ 具体信息伤害ID => {伤害名,是否可传导}
function Engine:getDamageNatures()
local ret = {}
for k, v in pairs(self.damage_nature) do
ret[k] = v
end
return ret
end
--- 由伤害ID获得伤害属性
---@param nature string | number @ 伤害ID
---@return table @ 具体信息({伤害名,是否可传导}),若不存在则为空
function Engine:getDamageNature(nature)
return self.damage_nature[nature]
end
--- 由伤害ID获得伤害名
---@param nature string | number @ 伤害ID
---@return string @ 伤害名
function Engine:getDamageNatureName(nature)
local ret = self:getDamageNature(nature)
return ret and ret[1] or ""
end
--- 判断一种伤害是否可传导
---@param nature string | number @ 伤害ID
---@return bool
function Engine:canChain(nature)
local ret = self:getDamageNature(nature)
return ret and ret[2]
end
--- 判断一个武将是否在本房间可用。 --- 判断一个武将是否在本房间可用。
---@param g string @ 武将名 ---@param g string @ 武将名
function Engine:canUseGeneral(g) function Engine:canUseGeneral(g)
@ -504,6 +577,15 @@ function Engine:addMiniGame(spec)
self.mini_games[spec.name] = spec self.mini_games[spec.name] = spec
end end
---@param spec TargetTipSpec
function Engine:addTargetTip(spec)
assert(type(spec.name) == "string")
if self.target_tips[spec.name] then
fk.qCritical("Warning: duplicated target tip type " .. spec.name)
end
self.target_tips[spec.name] = spec
end
--- 从已经开启的拓展包中,随机选出若干名武将。 --- 从已经开启的拓展包中,随机选出若干名武将。
--- ---
--- 对于同名武将不会重复选取。 --- 对于同名武将不会重复选取。
@ -595,72 +677,7 @@ end
---@param player Player @ 和这张牌扯上关系的那名玩家 ---@param player Player @ 和这张牌扯上关系的那名玩家
---@param data any @ 随意目前只用到JudgeStruct为了影响判定牌 ---@param data any @ 随意目前只用到JudgeStruct为了影响判定牌
function Engine:filterCard(id, player, data) function Engine:filterCard(id, player, data)
if player == nil then return Fk:currentRoom():filterCard(id, player, data)
self.filtered_cards[id] = nil
return
end
local card = self:getCardById(id, true)
local filters = self:currentRoom().status_skills[FilterSkill] or Util.DummyTable
if #filters == 0 then
self.filtered_cards[id] = nil
return
end
local modify = false
if data and type(data) == "table" and data.card
and type(data.card) == "table" and data.card:isInstanceOf(Card) then
modify = true
end
for _, f in ipairs(filters) do
if f:cardFilter(card, player, type(data) == "table" and data.isJudgeEvent) then
local _card = f:viewAs(card, player)
_card.id = id
_card.skillName = f.name
if modify and RoomInstance then
if not f.mute then
player:broadcastSkillInvoke(f.name)
RoomInstance:doAnimate("InvokeSkill", {
name = f.name,
player = player.id,
skill_type = f.anim_type,
})
end
RoomInstance:sendLog{
type = "#FilterCard",
arg = f.name,
from = player.id,
arg2 = card:toLogString(),
arg3 = _card:toLogString(),
}
end
card = _card
end
if card == nil then
card = self:getCardById(id)
end
self.filtered_cards[id] = card
end
if modify then
self.filtered_cards[id] = nil
data.card = card
return
end
end
--- 添加一张现场打印的牌到游戏中。
---
--- 这张牌必须是clone出来的虚拟牌不能有子卡因为他接下来就要变成实体卡了
---@param card Card
function Engine:_addPrintedCard(card)
assert(card:isVirtual() and #card.subcards == 0)
table.insert(self.printed_cards, card)
local id = -#self.printed_cards - 1
card.id = id
self.printed_cards[id] = card
end end
--- 获知当前的Engine是跑在服务端还是客户端并返回相应的实例。 --- 获知当前的Engine是跑在服务端还是客户端并返回相应的实例。

View File

@ -22,6 +22,7 @@
---@field public other_skills string[] @ 武将身上属于其他武将的技能,通过字符串调用 ---@field public other_skills string[] @ 武将身上属于其他武将的技能,通过字符串调用
---@field public related_skills Skill[] @ 武将相关的不属于其他武将的技能,例如邓艾的急袭 ---@field public related_skills Skill[] @ 武将相关的不属于其他武将的技能,例如邓艾的急袭
---@field public related_other_skills string [] @ 武将相关的属于其他武将的技能,例如孙策的英姿 ---@field public related_other_skills string [] @ 武将相关的属于其他武将的技能,例如孙策的英姿
---@field public all_skills table @ 武将的所有技能,包括相关技能和属于其他武将的技能
---@field public companions string [] @ 有珠联璧合关系的武将 ---@field public companions string [] @ 有珠联璧合关系的武将
---@field public hidden boolean @ 不在选将框里出现,可以点将,可以在武将一览里查询到 ---@field public hidden boolean @ 不在选将框里出现,可以点将,可以在武将一览里查询到
---@field public total_hidden boolean @ 完全隐藏 ---@field public total_hidden boolean @ 完全隐藏
@ -60,6 +61,7 @@ function General:initialize(package, name, kingdom, hp, maxHp, gender)
self.other_skills = {} -- skill belongs other general, e.g. "mashu" of pangde self.other_skills = {} -- skill belongs other general, e.g. "mashu" of pangde
self.related_skills = {} -- skills related to this general, but not first added to it, e.g. "jixi" of dengai self.related_skills = {} -- skills related to this general, but not first added to it, e.g. "jixi" of dengai
self.related_other_skills = {} -- skills related to this general and belong to other generals, e.g. "yingzi" of sunce self.related_other_skills = {} -- skills related to this general and belong to other generals, e.g. "yingzi" of sunce
self.all_skills = {}
self.companions = {} self.companions = {}
@ -75,8 +77,10 @@ end
function General:addSkill(skill) function General:addSkill(skill)
if (type(skill) == "string") then if (type(skill) == "string") then
table.insert(self.other_skills, skill) table.insert(self.other_skills, skill)
table.insert(self.all_skills, {skill, false})
elseif (skill.class and skill.class:isSubclassOf(Skill)) then elseif (skill.class and skill.class:isSubclassOf(Skill)) then
table.insert(self.skills, skill) table.insert(self.skills, skill)
table.insert(self.all_skills, {skill.name, false})
skill.package = self.package skill.package = self.package
end end
end end
@ -86,8 +90,10 @@ end
function General:addRelatedSkill(skill) function General:addRelatedSkill(skill)
if (type(skill) == "string") then if (type(skill) == "string") then
table.insert(self.related_other_skills, skill) table.insert(self.related_other_skills, skill)
table.insert(self.all_skills, {skill, true}) -- only for UI
elseif (skill.class and skill.class:isSubclassOf(Skill)) then elseif (skill.class and skill.class:isSubclassOf(Skill)) then
table.insert(self.related_skills, skill) table.insert(self.related_skills, skill)
table.insert(self.all_skills, {skill.name, true}) -- only for UI
Fk:addSkill(skill) Fk:addSkill(skill)
skill.package = self.package skill.package = self.package
end end

View File

@ -13,6 +13,7 @@
---@field public shield integer @ 护甲数 ---@field public shield integer @ 护甲数
---@field public kingdom string @ 势力 ---@field public kingdom string @ 势力
---@field public role string @ 身份 ---@field public role string @ 身份
---@field public role_shown boolean
---@field public general string @ 武将 ---@field public general string @ 武将
---@field public deputyGeneral string @ 副将 ---@field public deputyGeneral string @ 副将
---@field public gender integer @ 性别 ---@field public gender integer @ 性别
@ -71,6 +72,11 @@ Player.JudgeSlot = 'JudgeSlot'
--- 构造函数。总之这不是随便调用的函数 --- 构造函数。总之这不是随便调用的函数
function Player:initialize() function Player:initialize()
self.id = 0 self.id = 0
self.property_keys = {
"general", "deputyGeneral", "maxHp", "hp", "shield", "gender", "kingdom",
"dead", "role", "role_shown", "rest", "seat", "phase", "faceup", "chained",
"sealedSlots",
}
self.hp = 0 self.hp = 0
self.maxHp = 0 self.maxHp = 0
self.kingdom = "qun" self.kingdom = "qun"
@ -98,8 +104,8 @@ function Player:initialize()
[Player.Equip] = {}, [Player.Equip] = {},
[Player.Judge] = {}, [Player.Judge] = {},
} }
self.virtual_equips = {}
self.special_cards = {} self.special_cards = {}
self.virtual_equips = {}
self.equipSlots = { self.equipSlots = {
Player.WeaponSlot, Player.WeaponSlot,
@ -221,6 +227,15 @@ function Player:getMark(mark)
return mark return mark
end end
--- 获取角色对应Mark并初始化为table
---@param mark string @ 标记
---@return table
function Player:getTableMark(mark)
local mark = self.mark[mark]
if type(mark) == "table" then return table.simpleClone(mark) end
return {}
end
--- 获取角色有哪些Mark。 --- 获取角色有哪些Mark。
function Player:getMarkNames() function Player:getMarkNames()
local ret = {} local ret = {}
@ -623,7 +638,7 @@ function Player:getNextAlive(ignoreRemoved, num, ignoreRest)
num = num or 1 num = num or 1
for _ = 1, num do for _ = 1, num do
ret = ret.next ret = ret.next
while (ret.dead and not ignoreRest) or (doNotIgnore and ret:isRemoved()) do while (ret.dead and (ret.rest == 0 or not ignoreRest)) or (doNotIgnore and ret:isRemoved()) do
ret = ret.next ret = ret.next
end end
end end
@ -1179,6 +1194,71 @@ function Player:isBuddy(other)
return self.id == id or table.contains(self.buddy_list, id) return self.id == id or table.contains(self.buddy_list, id)
end end
--- Player是否可看到某card
--- @param cardId integer
---@param move? CardsMoveStruct
---@return boolean
function Player:cardVisible(cardId, move)
if move then
if table.find(move.moveInfo, function(info) return info.cardId == cardId end) then
if move.moveVisible then return true end
-- specialVisible还要控制这个pile对他人是否应该可见但是不在这里生效
if move.specialVisible then return true end
if (type(move.visiblePlayers) == "number" and move.visiblePlayers == self.id) or
(type(move.visiblePlayers) == "table" and table.find(move.visiblePlayers, self.id)) then
return true
end
end
end
local room = Fk:currentRoom()
local area = room:getCardArea(cardId)
local card = Fk:getCardById(cardId)
local public_areas = {Card.DiscardPile, Card.Processing, Card.Void, Card.PlayerEquip, Card.PlayerJudge}
local player_areas = {Card.PlayerHand, Card.PlayerSpecial}
if room.observing == true then return table.contains(public_areas, area) end
local status_skills = Fk:currentRoom().status_skills[VisibilitySkill] or Util.DummyTable
for _, skill in ipairs(status_skills) do
local f = skill:cardVisible(self, card)
if f ~= nil then
return f
end
end
if area == Card.DrawPile then return false
elseif table.contains(public_areas, area) then return true
elseif move and area == Card.PlayerSpecial and not move.specialName:startsWith("$") then
return true
elseif table.contains(player_areas, area) then
local to = room:getCardOwner(cardId)
return to == self or self:isBuddy(to)
else
return false
end
end
--- Player是否可看到某target的身份
--- @param target Player
---@return boolean
function Player:roleVisible(target)
local room = Fk:currentRoom()
local status_skills = room.status_skills[VisibilitySkill] or Util.DummyTable
for _, skill in ipairs(status_skills) do
local f = skill:roleVisible(self, target)
if f ~= nil then
return f
end
end
if room.observing == false and target == self then return true end
return target.role_shown
end
--- 比较两名角色的性别是否相同。 --- 比较两名角色的性别是否相同。
---@param other Player @ 另一名角色 ---@param other Player @ 另一名角色
---@param diff? bool @ 比较二者不同 ---@param diff? bool @ 比较二者不同
@ -1204,4 +1284,50 @@ function Player:isFemale()
return self.gender == General.Female or self.gender == General.Bigender return self.gender == General.Female or self.gender == General.Bigender
end end
function Player:toJsonObject()
local ptable = {}
for _, k in ipairs(self.property_keys) do
ptable[k] = self[k]
end
return {
properties = ptable,
card_history = self.cardUsedHistory,
skill_history = self.skillUsedHistory,
mark = self.mark,
skills = table.map(self.player_skills, Util.NameMapper),
player_cards = self.player_cards,
special_cards = self.special_cards,
buddy_list = self.buddy_list,
}
end
function Player:loadJsonObject(o)
for k, v in pairs(o.properties) do self[k] = v end
self.cardUsedHistory = o.card_history
self.skillUsedHistory = o.skill_history
self.mark = o.mark
for _, sname in ipairs(o.skills) do self:addSkill(sname) end
self.player_cards = o.player_cards
self.special_cards = o.special_cards
self.buddy_list = o.buddy_list
local pid = self.id
local room = Fk:currentRoom()
for _, id in ipairs(o.player_cards[Player.Hand]) do
room:setCardArea(id, Card.PlayerHand, pid)
end
for _, id in ipairs(o.player_cards[Player.Equip]) do
room:setCardArea(id, Card.PlayerEquip, pid)
end
for _, id in ipairs(o.player_cards[Player.Judge]) do
room:setCardArea(id, Card.PlayerJudge, pid)
end
for _, ids in ipairs(o.special_cards) do
for _, id in ipairs(ids) do
room:setCardArea(id, Card.PlayerSpecial, pid)
end
end
end
return Player return Player

View File

@ -0,0 +1,80 @@
--[[
RequestHandler是当一名Player收到Request信息时
Player是由真人控制还是由Bot控制
* Scene对象Request时
* Player与currentRoom
* setup():
RequestHandler创建于客户端时UI进行通信
* notifyUI(): qml发送所有与UI更新有关的信息
* update(): Qml向Lua发送UI事件后
notifyUI反馈更新信息
* self.change: update中UI变化notifyUI时
(*) QML中需定义applyChange函数以接收来自Lua的更改
RequestHandler创建于服务端时
RequestHandler进行交互的就是AI逻辑代码
--]]
--@field public data any 相关数据,需要子类自行定义一个类或者模拟类
-- 关于self.change:
-- * _new: 新创建的Item一开始的时候UI上没显示它们
-- * _delete: 删除新创建的Item
-- * _prompt: 提示信息。实践证明prompt值得单开一个key
-- * _misc: 其他乱七八糟的需要告诉UI的信息
-- * Item类名这类Item中某个Item发生的信息变动change的主体部分
---@class RequestHandler: Object
---@field public room AbstractRoom
---@field public scene Scene
---@field public player Player 需要应答的玩家
---@field public prompt string 提示信息
---@field public change { [string]: Item[] } 将会传递给UI的更新数据
local RequestHandler = class("RequestHandler")
function RequestHandler:initialize(player)
self.room = Fk:currentRoom()
self.player = player
-- finish只在Client执行 用于保证UI执行了某些必须执行的善后
if ClientInstance and ClientInstance.current_request_handler then
ClientInstance.current_request_handler:_finish()
end
self.room.current_request_handler = self
end
-- 进入Request之后需要做的第一步操作对应之前UI代码中state变换
function RequestHandler:setup() end
function RequestHandler:_finish()
if not self.finished then
self.finished = true
self.change = {}
self:finish()
self.scene:notifyUI()
end
end
-- 因为发送答复或者超时等原因导致UI进入notactive状态时调用。
-- 只会由UI调用且只执行一次意义主要在于清除那些传给了UI的半路新建的对象
function RequestHandler:finish() end
-- 产生UI事件后由UI触发
-- 需要实现各种合法性检验决定需要变更状态的UI并最终将变更反馈给真实的界面
---@param elemType string
---@param id string | integer
---@param action string
---@param data any
function RequestHandler:update(elemType, id, action, data) end
function RequestHandler:setPrompt(str)
if not self.change then return end
self.prompt = str
self.change["_prompt"] = str
end
return RequestHandler

View File

@ -0,0 +1,367 @@
local RoomScene = require 'ui_emu.roomscene'
local Interaction = require 'ui_emu.interaction'
local CardItem = (require 'ui_emu.common').CardItem
--[[
AskForUseActiveSkill的Handler
UI组件TODOexpand牌
TODOinteraction小组件
* enable
*
* (TODO) interaction
*
ViewAsSkill也考虑进去
--]]
---@class ReqActiveSkill: RequestHandler
---@field public skill_name string 当前响应的技能名
---@field public prompt string 提示信息
---@field public cancelable boolean 可否取消
---@field public extra_data UseExtraData 传入的额外信息
---@field public pendings integer[] 卡牌id数组
---@field public selected_targets integer[] 选择的目标
---@field public expanded_piles { [string]: integer[] } 用于展开/收起
local ReqActiveSkill = RequestHandler:subclass("ReqActiveSkill")
function ReqActiveSkill:initialize(player)
RequestHandler.initialize(self, player)
self.scene = RoomScene:new(self)
self.expanded_piles = {}
end
function ReqActiveSkill:setup(ignoreInteraction)
local scene = self.scene
-- FIXME: 偷懒了让修改interaction时的全局刷新功能复用setup 总之这里写的很垃圾
if not ignoreInteraction then
scene:removeItem("Interaction", "1")
self:setupInteraction()
end
self:setPrompt(self.prompt)
self.pendings = {}
self:retractAllPiles()
self:expandPiles()
scene:unselectAllCards()
self.selected_targets = {}
scene:unselectAllTargets()
self:updateUnselectedCards()
self:updateUnselectedTargets()
self:updateButtons()
end
function ReqActiveSkill:finish()
self:retractAllPiles()
end
function ReqActiveSkill:setSkillPrompt(skill, cid)
local prompt = skill.prompt
if type(skill.prompt) == "function" then
prompt = skill:prompt(cid or self.pendings, self.selected_targets)
end
if type(prompt) == "string" then
self:setPrompt(prompt)
else
self:setPrompt(self.original_prompt or "")
end
end
function ReqActiveSkill:setupInteraction()
local skill = Fk.skills[self.skill_name]
if skill and skill.interaction then
skill.interaction.data = nil -- FIXME
local interaction = skill:interaction()
-- 假设只有1个interaction (其实目前就是这样)
local i = Interaction:new(self.scene, "1", interaction)
i.skill_name = self.skill_name
self.scene:addItem(i)
end
end
function ReqActiveSkill:expandPile(pile, extra_ids, extra_footnote)
if self.expanded_piles[pile] ~= nil then return end
local ids, footnote
local player = self.player
if pile == "_equip" then
ids = player:getCardIds("e")
footnote = "$Equip"
elseif pile == "_extra" then
ids = extra_ids
footnote = extra_footnote
-- self.extra_cards = exira_ids
else
-- FIXME: 可能存在的浅拷贝
ids = table.simpleClone(player:getPile(pile))
footnote = pile
end
self.expanded_piles[pile] = ids
local scene = self.scene
for _, id in ipairs(ids) do
scene:addItem(CardItem:new(scene, id), {
reason = "expand",
footnote = footnote,
})
end
end
function ReqActiveSkill:retractPile(pile)
if self.expanded_piles[pile] == nil then return end
local ids = self.expanded_piles[pile]
self.expanded_piles[pile] = nil
local scene = self.scene
for _, id in ipairs(ids) do
scene:removeItem("CardItem", id, { reason = "retract" })
end
end
function ReqActiveSkill:retractAllPiles()
for k, v in pairs(self.expanded_piles) do
self:retractPile(k)
end
end
function ReqActiveSkill:expandPiles()
local skill = Fk.skills[self.skill_name]
local player = self.player
if not skill then return end
-- 特殊equips至少有一张能亮着的情况下才展开 且无视是否存在skill.expand_pile
for _, id in ipairs(player:getCardIds("e")) do
if self:cardValidity(id) then
self:expandPile("_equip")
break
end
end
if not skill.expand_pile then return end
local pile = skill.expand_pile
if type(pile) == "function" then
pile = pile(skill)
end
local ids = pile
if type(pile) == "string" then
ids = player:getPile(pile)
else -- if type(pile) == "table" then
pile = "_extra"
end
self:expandPile(pile, ids, self.skill_name)
end
function ReqActiveSkill:feasible()
local player = self.player
local skill = Fk.skills[self.skill_name]
if not skill then return false end
local ret
if skill:isInstanceOf(ActiveSkill) then
ret = skill:feasible(self.selected_targets, self.pendings, player)
elseif skill:isInstanceOf(ViewAsSkill) then
local card = skill:viewAs(self.pendings)
if card then
local card_skill = card.skill ---@type ActiveSkill
ret = card_skill:feasible(self.selected_targets, { card.id }, player, card)
end
end
return ret
end
function ReqActiveSkill:isCancelable()
return self.cancelable
end
function ReqActiveSkill:cardValidity(cid)
local skill = Fk.skills[self.skill_name]
if not skill then return false end
return skill:cardFilter(cid, self.pendings)
end
function ReqActiveSkill:extraDataValidity(pid)
local data = self.extra_data or {}
-- 逻辑块地狱
if data.must_targets then
-- must_targets: 必须先选择must_targets内的**所有**目标
if not (#data.must_targets <= #self.selected_targets or
table.contains(data.must_targets, pid)) then return false end
end
if data.include_targets then
-- include_targets: 必须先选择include_targets内的**其中一个**目标
if not (table.hasIntersection(data.include_targets, self.selected_targets) or
table.contains(data.include_targets, pid)) then return false end
end
if data.exclusive_targets then
-- exclusive_targets: **只能选择**exclusive_targets内的目标
if not table.contains(data.exclusive_targets, pid) then return false end
end
return true
end
function ReqActiveSkill:targetValidity(pid)
if not self:extraDataValidity(pid) then return false end
local skill = Fk.skills[self.skill_name] --- @type ActiveSkill | ViewAsSkill
if not skill then return false end
local card -- 姑且接一下(雾)
if skill:isInstanceOf(ViewAsSkill) then
card = skill:viewAs(self.pendings)
if not card or self.player:isProhibited(self.room:getPlayerById(pid), card) then return false end
skill = card.skill
end
return skill:targetFilter(pid, self.selected_targets, self.pendings, card, self.extra_data)
end
function ReqActiveSkill:updateButtons()
local scene = self.scene
scene:update("Button", "OK", { enabled = not not self:feasible() })
scene:update("Button", "Cancel", { enabled = not not self:isCancelable() })
end
function ReqActiveSkill:updateUnselectedCards()
local scene = self.scene
for cid, item in pairs(scene:getAllItems("CardItem")) do
if not item.selected then
scene:update("CardItem", cid, { enabled = not not self:cardValidity(cid) })
end
end
end
function ReqActiveSkill:updateUnselectedTargets()
local scene = self.scene
for pid, item in pairs(scene:getAllItems("Photo")) do
if not item.selected then
scene:updateTargetEnability(pid, self:targetValidity(pid))
end
end
end
function ReqActiveSkill:initiateTargets()
local room = self.room
local scene = self.scene
local skill = Fk.skills[self.skill_name]
if skill:isInstanceOf(ViewAsSkill) then
local card = skill:viewAs(self.pendings)
if card then skill = card.skill else skill = nil end
end
self.selected_targets = {}
scene:unselectAllTargets()
if skill then
self:updateUnselectedTargets()
else
scene:disableAllTargets()
end
self:updateButtons()
end
function ReqActiveSkill:updateInteraction(data)
local skill = Fk.skills[self.skill_name]
if skill and skill.interaction then
skill.interaction.data = data
self.scene:update("Interaction", "1", { data = data })
ReqActiveSkill.setup(self, true) -- interaction变动后需复原
end
end
function ReqActiveSkill:doOKButton()
local skill = Fk.skills[self.skill_name]
local cardstr = json.encode{
skill = self.skill_name,
subcards = self.pendings
}
local reply = {
card = cardstr,
targets = self.selected_targets,
--special_skill = roomScene.getCurrentCardUseMethod(),
interaction_data = skill and skill.interaction and skill.interaction.data,
}
if self.selected_card then
reply.special_skill = self.skill_name
end
ClientInstance:notifyUI("ReplyToServer", json.encode(reply))
end
function ReqActiveSkill:doCancelButton()
ClientInstance:notifyUI("ReplyToServer", "__cancel")
end
-- 对点击卡牌的处理。data中包含selected属性可能是选中或者取消选中分开考虑。
function ReqActiveSkill:selectCard(cardid, data)
local scene = self.scene
local selected = data.selected
scene:update("CardItem", cardid, data)
-- 若选中,则加入已选列表;若取消选中,则其他牌可能无法满足可选条件,需额外判断
-- 例如周善 选择包括“安”在内的任意张手牌交出
if selected then
table.insert(self.pendings, cardid)
else
local old_pendings = table.simpleClone(self.pendings)
self.pendings = {}
for _, cid in ipairs(old_pendings) do
local ret = cid ~= cardid and self:cardValidity(cid)
if ret then table.insert(self.pendings, cid) end
-- 因为这里而变成未选中的牌稍后将更新一次enable 但是存在着冗余的cardFilter调用
scene:update("CardItem", cid, { selected = not not ret })
end
end
-- 最后刷新未选牌的enable属性
self:updateUnselectedCards()
end
-- 对点击角色的处理。data中包含selected属性可能是选中或者取消选中。
function ReqActiveSkill:selectTarget(playerid, data)
local scene = self.scene
local selected = data.selected
local skill = Fk.skills[self.skill_name]
scene:update("Photo", playerid, data)
-- 发生以下Viewas判断时已经是因为选角色触发的了说明肯定有card了这么写不会出事吧
if skill:isInstanceOf(ViewAsSkill) then
skill = skill:viewAs(self.pendings).skill
end
-- 类似选卡
if selected then
table.insert(self.selected_targets, playerid)
else
local old_targets = table.simpleClone(self.selected_targets)
self.selected_targets = {}
scene:unselectAllTargets()
for _, pid in ipairs(old_targets) do
local ret = pid ~= playerid and self:targetValidity(pid)
if ret then table.insert(self.selected_targets, pid) end
scene:update("Photo", pid, { selected = not not ret })
end
end
self:updateUnselectedTargets()
self:updateButtons()
end
function ReqActiveSkill:update(elemType, id, action, data)
if elemType == "Button" then
if id == "OK" then self:doOKButton()
elseif id == "Cancel" then self:doCancelButton() end
return true
elseif elemType == "CardItem" then
self:selectCard(id, data)
self:initiateTargets()
elseif elemType == "Photo" then
self:selectTarget(id, data)
elseif elemType == "Interaction" then
self:updateInteraction(data)
end
end
return ReqActiveSkill

View File

@ -0,0 +1,36 @@
local OKScene = require 'ui_emu.okscene'
-- 极其简单的skillinvoke
---@class ReqInvoke: RequestHandler
local ReqInvoke = RequestHandler:subclass("ReqInvoke")
function ReqInvoke:initialize(player)
RequestHandler.initialize(self, player)
self.scene = OKScene:new(self)
end
function ReqInvoke:setup()
local scene = self.scene
scene:update("Button", "OK", { enabled = true })
scene:update("Button", "Cancel", { enabled = true })
end
function ReqInvoke:doOKButton()
ClientInstance:notifyUI("ReplyToServer", "1")
end
function ReqInvoke:doCancelButton()
ClientInstance:notifyUI("ReplyToServer", "__cancel")
end
function ReqInvoke:update(elemType, id, action, data)
if elemType == "Button" then
if id == "OK" then self:doOKButton()
elseif id == "Cancel" then self:doCancelButton() end
return true
end
end
return ReqInvoke

View File

@ -0,0 +1,132 @@
local ReqActiveSkill = require 'core.request_type.active_skill'
local ReqUseCard = require 'lua.core.request_type.use_card'
local SpecialSkills = require 'ui_emu.specialskills'
local Button = (require 'ui_emu.control').Button
---@class ReqPlayCard: ReqUseCard
local ReqPlayCard = ReqUseCard:subclass("ReqPlayCard")
function ReqPlayCard:initialize(player)
ReqUseCard.initialize(self, player)
self.original_prompt = "#PlayCard"
local scene = self.scene
-- 出牌阶段还要多模拟一个结束按钮
scene:addItem(Button:new(self.scene, "End"))
scene:addItem(SpecialSkills:new(self.scene, "1"))
end
function ReqPlayCard:setup()
ReqUseCard.setup(self)
self:setPrompt(self.original_prompt)
self.scene:update("Button", "End", { enabled = true })
end
function ReqPlayCard:cardValidity(cid)
if self.skill_name and not self.selected_card then return ReqActiveSkill.cardValidity(self, cid) end
local player = self.player
local card = cid
if type(cid) == "number" then card = Fk:getCardById(cid) end
local ret = player:canUse(card)
if ret then
local min_target = card.skill:getMinTargetNum()
if min_target > 0 then
for pid, _ in pairs(self.scene:getAllItems("Photo")) do
if card.skill:targetFilter(pid, {}, {}, card, self.extra_data) then
return true
end
end
return false
end
end
return ret
end
function ReqPlayCard:skillButtonValidity(name)
local player = self.player
local skill = Fk.skills[name]
if skill:isInstanceOf(ViewAsSkill) then
return skill:enabledAtPlay(player, true)
elseif skill:isInstanceOf(ActiveSkill) then
return skill:canUse(player, nil)
end
end
function ReqPlayCard:feasible()
local player = self.player
if self.skill_name then
return ReqActiveSkill.feasible(self)
end
local card = self.selected_card
local ret = false
if card then
local skill = card.skill ---@type ActiveSkill
ret = skill:feasible(self.selected_targets, { card.id }, player, card)
end
return ret
end
function ReqPlayCard:selectSpecialUse(data)
-- 相当于使用一个以已选牌为pendings的主动技
if not data or data == "_normal_use" then
self.skill_name = nil
self.pendings = nil
self:setSkillPrompt(self.selected_card.skill, self.selected_card:getEffectiveId())
else
self.skill_name = data
self.pendings = Card:getIdList(self.selected_card)
self:setSkillPrompt(Fk.skills[data], self.pendings)
end
self:initiateTargets()
end
function ReqPlayCard:doOKButton()
self.scene:update("SpecialSkills", "1", { skills = {} })
self.scene:notifyUI()
return ReqUseCard.doOKButton(self)
end
function ReqPlayCard:doCancelButton()
self.scene:update("SpecialSkills", "1", { skills = {} })
self.scene:notifyUI()
return ReqUseCard.doCancelButton(self)
end
function ReqPlayCard:doEndButton()
self.scene:update("SpecialSkills", "1", { skills = {} })
self.scene:notifyUI()
ClientInstance:notifyUI("ReplyToServer", "")
end
function ReqPlayCard:selectCard(cid, data)
ReqUseCard.selectCard(self, cid, data)
if self.skill_name and not self.selected_card then return end
if self.selected_card then
self:setSkillPrompt(self.selected_card.skill, self.selected_card:getEffectiveId())
local sp_skills = {}
if self.selected_card.special_skills then
sp_skills = table.simpleClone(self.selected_card.special_skills)
if self:cardValidity(self.selected_card) then
table.insert(sp_skills, 1, "_normal_use")
end
end
self.scene:update("SpecialSkills", "1", { skills = sp_skills })
else
self:setPrompt(self.original_prompt)
self.scene:update("SpecialSkills", "1", { skills = {} })
end
end
function ReqPlayCard:update(elemType, id, action, data)
if elemType == "Button" and id == "End" then
self:doEndButton()
return true
elseif elemType == "SpecialSkills" then
self:selectSpecialUse(data)
end
return ReqUseCard.update(self, elemType, id, action, data)
end
return ReqPlayCard

View File

@ -0,0 +1,164 @@
local RoomScene = require 'ui_emu.roomscene'
local ReqActiveSkill = require 'core.request_type.active_skill'
--[[
AskForResponseCard的Handler
UI组件
*
*
*
ActiveSkill合法性流程
--]]
---@class ReqResponseCard: ReqActiveSkill
---@field public selected_card? Card 使用一张牌时会用到 支持锁视技
---@field public pattern string 请求格式
---@field public original_prompt string 最开始的提示信息;这种涉及技能按钮的需要这样一下
local ReqResponseCard = ReqActiveSkill:subclass("ReqResponseCard")
function ReqResponseCard:setup()
if not self.original_prompt then
self.original_prompt = self.prompt or ""
end
ReqActiveSkill.setup(self)
self.selected_card = nil
self:updateSkillButtons()
end
-- FIXME: 关于&牌堆的可使用打出瞎jb写了点 来个懂哥优化一下
function ReqResponseCard:expandPiles()
if self.skill_name then return ReqActiveSkill.expandPiles(self) end
local player = self.player
for pile in pairs(player.special_cards) do
if pile:endsWith('&') then
self:expandPile(pile)
end
end
end
function ReqResponseCard:skillButtonValidity(name)
local player = self.player
local skill = Fk.skills[name]
return skill:isInstanceOf(ViewAsSkill) and skill:enabledAtResponse(player, true)
end
function ReqResponseCard:cardValidity(cid)
if self.skill_name then return ReqActiveSkill.cardValidity(self, cid) end
local card = cid
if type(cid) == "number" then card = Fk:getCardById(cid) end
return self:cardFeasible(card)
end
function ReqResponseCard:cardFeasible(card)
local exp = Exppattern:Parse(self.pattern)
local player = self.player
return not player:prohibitResponse(card) and exp:match(card)
end
function ReqResponseCard:feasible()
local skill = Fk.skills[self.skill_name]
local card = self.selected_card
if skill then
card = skill:viewAs(self.pendings)
end
return card and self:cardFeasible(card)
end
function ReqResponseCard:isCancelable()
if self.skill_name then return true end
return self.cancelable
end
function ReqResponseCard:updateSkillButtons()
local scene = self.scene
for name, item in pairs(scene:getAllItems("SkillButton")) do
local skill = Fk.skills[name]
local ret = self:skillButtonValidity(name)
if ret and skill:isInstanceOf(ViewAsSkill) then
local exp = Exppattern:Parse(skill.pattern)
local cnames = {}
for _, m in ipairs(exp.matchers) do
if m.name then table.insertTable(cnames, m.name) end
if m.trueName then table.insertTable(cnames, m.trueName) end
end
for _, n in ipairs(cnames) do
local c = Fk:cloneCard(n)
c.skillName = name
ret = self:cardValidity(c)
if ret then break end
end
end
scene:update("SkillButton", name, { enabled = not not ret })
end
end
function ReqResponseCard:doOKButton()
if self.skill_name then return ReqActiveSkill.doOKButton(self) end
local reply = {
card = self.selected_card:getEffectiveId(),
targets = self.selected_targets,
}
ClientInstance:notifyUI("ReplyToServer", json.encode(reply))
end
function ReqResponseCard:doCancelButton()
if self.skill_name then
self:selectSkill(self.skill_name, { selected = false })
self.scene:notifyUI()
return
end
return ReqActiveSkill:doCancelButton()
end
function ReqResponseCard:selectSkill(skill, data)
local scene = self.scene
local selected = data.selected
scene:update("SkillButton", skill, data)
if selected then
for name, item in pairs(scene:getAllItems("SkillButton")) do
scene:update("SkillButton", name, { enabled = item.selected })
end
self.skill_name = skill
self.selected_card = nil
self:setSkillPrompt(skill)
ReqActiveSkill.setup(self)
else
self.skill_name = nil
self.prompt = self.original_prompt
self:setup()
end
end
function ReqResponseCard:selectCard(cid, data)
if self.skill_name and not self.selected_card then
return ReqActiveSkill.selectCard(self, cid, data)
end
local scene = self.scene
local selected = data.selected
scene:update("CardItem", cid, data)
if selected then
self.skill_name = nil
self.selected_card = Fk:getCardById(cid)
scene:unselectOtherCards(cid)
else
self.selected_card = nil
end
end
function ReqResponseCard:update(elemType, id, action, data)
if elemType == "CardItem" then
self:selectCard(id, data)
self:updateButtons()
elseif elemType == "SkillButton" then
self:selectSkill(id, data)
else -- if elemType == "Button" or elemType == "Interaction" then
return ReqActiveSkill.update(self, elemType, id, action, data)
end
end
return ReqResponseCard

View File

@ -0,0 +1,107 @@
local ReqActiveSkill = require 'core.request_type.active_skill'
local ReqResponseCard = require 'core.request_type.response_card'
---@class ReqUseCard: ReqResponseCard
local ReqUseCard = ReqResponseCard:subclass("ReqUseCard")
function ReqUseCard:cardValidity(cid)
if self.skill_name then return ReqActiveSkill.cardValidity(self, cid) end
local card = cid
if type(cid) == "number" then card = Fk:getCardById(cid) end
return self:cardFeasible(card)
end
function ReqUseCard:targetValidity(pid)
if self.skill_name then return ReqActiveSkill.targetValidity(self, pid) end
local player = self.player
local room = self.room
local p = room:getPlayerById(pid)
local card = self.selected_card
local ret = card and not player:isProhibited(p, card) and
card.skill:targetFilter(pid, self.selected_targets, { card.id }, card, self.extra_data)
return ret
end
function ReqUseCard:cardFeasible(card)
local exp = Exppattern:Parse(self.pattern)
local player = self.player
return not player:prohibitUse(card) and exp:match(card)
end
function ReqUseCard:feasible()
local skill = Fk.skills[self.skill_name]
local card = self.selected_card
local ret = false
if card and self:cardFeasible(card) then
ret = card.skill:feasible(self.selected_targets,
skill and self.pendings or { card.id }, self.player, card)
end
return ret
end
function ReqUseCard:initiateTargets()
if self.skill_name then
return ReqActiveSkill.initiateTargets(self)
end
-- 重置
self.selected_targets = {}
self.scene:unselectAllTargets()
self:updateUnselectedTargets()
self:updateButtons()
end
function ReqUseCard:selectTarget(playerid, data)
if self.skill_name then
return ReqActiveSkill.selectTarget(self, playerid, data)
end
local player = self.player
local scene = self.scene
local selected = data.selected
local card = self.selected_card
scene:update("Photo", playerid, data)
if card then
local skill = card.skill
if selected then
table.insert(self.selected_targets, playerid)
else
-- 存储剩余目标
local previous_targets = table.filter(self.selected_targets, function(id)
return id ~= playerid
end)
self.selected_targets = {}
for _, pid in ipairs(previous_targets) do
local ret
ret = not player:isProhibited(pid, card) and skill and
skill:targetFilter(pid, self.selected_targets,
{ card.id }, card, data.extra_data)
-- 从头开始写目标
if ret then
table.insert(self.selected_targets, pid)
end
scene:update("Photo", pid, { selected = not not ret })
end
end
end
self:updateUnselectedTargets()
self:updateButtons()
end
function ReqUseCard:selectSkill(skill, data)
ReqResponseCard.selectSkill(self, skill, data)
self.selected_targets = {}
self.scene:unselectAllTargets()
self:updateUnselectedTargets()
end
function ReqUseCard:update(elemType, id, action, data)
if elemType == "CardItem" or elemType == "Photo" then
return ReqActiveSkill.update(self, elemType, id, action, data)
else --if elemType == "Button" or elemType == "SkillButton" then or interaction
return ReqResponseCard.update(self, elemType, id, action, data)
end
end
return ReqUseCard

View File

@ -0,0 +1,92 @@
-- 作Room和Client的基类这二者有不少共通之处
--
-- 子类纯属写给注释看,这个面向对象库没有实现多重继承
---@class AbstractRoom : CardManager
---@field public players Player[] @ 房内参战角色们
---@field public alive_players Player[] @ 所有存活玩家的数组
---@field public observers Player[] @ 看戏的
---@field public current Player @ 当前行动者
---@field public status_skills table<class, Skill[]> @ 这个房间中含有的状态技列表
---@field public skill_costs table<string, any> @ 用来存skill.cost_data
---@field public card_marks table<integer, any> @ 用来存实体卡的card.mark
---@field public banners table<string, any> @ 全局mark
---@field public current_request_handler RequestHandler @ 当前正处理的请求数据
---@field public timeout integer @ 出牌时长上限
---@field public settings table @ 房间的额外设置差不多是json对象
local AbstractRoom = class("AbstractRoom")
local CardManager = require 'core.room.card_manager'
AbstractRoom:include(CardManager)
function AbstractRoom:initialize()
self.players = {}
self.alive_players = {}
self.observers = {}
self.current = nil
self:initCardManager()
self.status_skills = {}
for class, skills in pairs(Fk.global_status_skill) do
self.status_skills[class] = {table.unpack(skills)}
end
self.skill_costs = {}
self.banners = {}
end
-- 仅供注释,其余空函数一样
---@param id integer
---@return Player
---@diagnostic disable-next-line: missing-return
function AbstractRoom:getPlayerById(id) end
--- 获得拥有某一张牌的玩家。
---@param cardId integer | Card @ 要获得主人的那张牌可以是Card实例或者id
---@return Player? @ 这张牌的主人可能返回nil
function AbstractRoom:getCardOwner(cardId)
local ret = CardManager.getCardOwner(self, cardId)
return ret and self:getPlayerById(ret)
end
function AbstractRoom:setBanner(name, value)
if value == 0 then value = nil end
self.banners[name] = value
end
function AbstractRoom:getBanner(name)
return self.banners[name]
end
function AbstractRoom:toJsonObject()
local card_manager = CardManager.toJsonObject(self)
local players = {}
for _, p in ipairs(self.players) do
players[tostring(p.id)] = p:toJsonObject()
end
return {
card_manager = card_manager,
circle = table.map(self.players, Util.IdMapper),
banners = self.banners,
timeout = self.timeout,
settings = self.settings,
players = players,
}
end
function AbstractRoom:loadJsonObject(o)
CardManager.loadJsonObject(self, o.card_manager)
-- 需要上层目前是Client自己根据circle添加玩家
self.banners = o.banners
self.timeout = o.timeout
self.settings = o.settings
for k, v in pairs(o.players) do
local pid = tonumber(k)
self:getPlayerById(pid):loadJsonObject(v)
end
end
return AbstractRoom

View File

@ -0,0 +1,355 @@
--- 负责管理AbstractRoom中所有Card的位置若在玩家的区域中则管理所属玩家
---@class CardManager : Object
---@field public draw_pile integer[] @ 摸牌堆这是卡牌id的数组
---@field public discard_pile integer[] @ 弃牌堆也是卡牌id的数组
---@field public processing_area integer[] @ 处理区依然是卡牌id数组
---@field public void integer[] @ 从游戏中除外区一样的是卡牌id数组
---@field public card_place table<integer, CardArea> @ 每个卡牌的id对应的区域一张表
---@field public owner_map table<integer, integer> @ 每个卡牌id对应的主人表的值是那个玩家的id可能是nil
---@field public filtered_cards table<integer, Card> @ 见于Engine其实在这
---@field public printed_cards table<integer, Card> @ 同上
---@field public next_print_card_id integer
---@field public card_marks table<integer, any> @ 用来存实体卡的card.mark
local CardManager = {} -- mixin
function CardManager:initCardManager()
self.draw_pile = {}
self.discard_pile = {}
self.processing_area = {}
self.void = {}
self.card_place = {}
self.owner_map = {}
self.filtered_cards = {}
self.printed_cards = {}
self.next_print_card_id = -2
self.card_marks = {}
end
--- 基本算是私有函数,别去用
---@param cardId integer
---@param cardArea CardArea
---@param owner? integer
function CardManager:setCardArea(cardId, cardArea, owner)
self.card_place[cardId] = cardArea
self.owner_map[cardId] = owner
end
--- 获取一张牌所处的区域。
---@param cardId integer | Card @ 要获得区域的那张牌可以是Card或者一个id
---@return CardArea @ 这张牌的区域
function CardManager:getCardArea(cardId)
local cardIds = {}
for _, cid in ipairs(Card:getIdList(cardId)) do
local place = self.card_place[cid] or Card.Unknown
table.insertIfNeed(cardIds, place)
end
return #cardIds == 1 and cardIds[1] or Card.Unknown
end
function CardManager:getCardOwner(cardId)
if type(cardId) ~= "number" then
assert(cardId and cardId:isInstanceOf(Card))
cardId = cardId:getEffectiveId()
end
return self.owner_map[cardId] or nil
end
local playerAreas = { Player.Hand, Player.Equip, Player.Judge, Player.Special }
--- 根据area获取相关的数组若为玩家的区域则需指定玩家
---
--- 若不存在这种区域需要返回nil
---@param area CardArea
---@param player Player?
---@param dup boolean? 是否返回复制 默认true
---@param special_name string?
function CardManager:getCardsByArea(area, player, dup, special_name)
local ret
dup = dup == nil and true or false
if area == Card.Processing then
ret = self.processing_area
elseif area == Card.DrawPile then
ret = self.draw_pile
elseif area == Card.DiscardPile then
ret = self.discard_pile
elseif area == Card.Void then
ret = self.void
elseif table.contains(playerAreas, area) then
assert(player ~= nil)
if area == Player.Special then
assert(special_name ~= nil)
ret = player.special_cards[special_name]
else
ret = player.player_cards[area]
end
end
if dup and ret then ret = table.simpleClone(ret) end
return ret
end
--- 根据moveInfo来移动牌先将牌从旧数组移动到新数组再更新两个map表
---@param data CardsMoveStruct
---@param info MoveInfo
function CardManager:applyMoveInfo(data, info)
local realFromArea = self:getCardArea(info.cardId)
local room = Fk:currentRoom()
local fromAreaIds = self:getCardsByArea(realFromArea,
data.from and room:getPlayerById(data.from), false, info.fromSpecialName)
table.removeOne(fromAreaIds, info.cardId)
local toAreaIds = self:getCardsByArea(data.toArea,
data.to and room:getPlayerById(data.to), false, data.specialName)
if data.toArea == Card.DrawPile then
local putIndex = data.drawPilePosition or 1
if putIndex == -1 then
putIndex = #self.draw_pile + 1
elseif putIndex < 1 or putIndex > #self.draw_pile + 1 then
putIndex = 1
end
table.insert(toAreaIds, putIndex, info.cardId)
else
table.insert(toAreaIds, info.cardId)
end
self:setCardArea(info.cardId, data.toArea, data.to)
end
--- 对那个id应用锁定视为技将它变成要被锁定视为的牌。
---@param id integer @ 要处理的id
---@param player Player @ 和这张牌扯上关系的那名玩家
---@param data any @ 随意目前只用到JudgeStruct为了影响判定牌
function CardManager:filterCard(id, player, data)
if player == nil then
self.filtered_cards[id] = nil
return
end
local card = Fk:getCardById(id, true)
local filters = Fk:currentRoom().status_skills[FilterSkill] or Util.DummyTable
if #filters == 0 then
self.filtered_cards[id] = nil
return
end
local modify = false
if data and type(data) == "table" and data.card
and type(data.card) == "table" and data.card:isInstanceOf(Card) then
modify = true
end
for _, f in ipairs(filters) do
if f:cardFilter(card, player, type(data) == "table" and data.isJudgeEvent) then
local _card = f:viewAs(card, player)
_card.id = id
_card.skillName = f.name
if modify and RoomInstance then
if not f.mute then
player:broadcastSkillInvoke(f.name)
RoomInstance:doAnimate("InvokeSkill", {
name = f.name,
player = player.id,
skill_type = f.anim_type,
})
end
RoomInstance:sendLog{
type = "#FilterCard",
arg = f.name,
from = player.id,
arg2 = card:toLogString(),
arg3 = _card:toLogString(),
}
end
card = _card
end
if card == nil then
card = Fk:getCardById(id)
end
self.filtered_cards[id] = card
end
if modify then
self.filtered_cards[id] = nil
data.card = card
return
end
end
function CardManager:printCard(name, suit, number)
local card = Fk:cloneCard(name, suit, number)
local id = self.next_print_card_id
card.id = id
self.printed_cards[id] = card
self.next_print_card_id = self.next_print_card_id - 1
table.insert(self.void, card.id)
self:setCardArea(card.id, Card.Void, nil)
return card
end
-- misc
function CardManager:prepareDrawPile(seed)
local allCardIds = Fk:getAllCardIds()
for i = #allCardIds, 1, -1 do
if Fk:getCardById(allCardIds[i]).is_derived then
local id = allCardIds[i]
table.removeOne(allCardIds, id)
table.insert(self.void, id)
self:setCardArea(id, Card.Void, nil)
end
end
table.shuffle(allCardIds, seed)
self.draw_pile = allCardIds
for _, id in ipairs(self.draw_pile) do
self:setCardArea(id, Card.DrawPile, nil)
end
end
function CardManager:shuffleDrawPile(seed)
if #self.draw_pile + #self.discard_pile == 0 then
return
end
table.insertTable(self.draw_pile, self.discard_pile)
for _, id in ipairs(self.discard_pile) do
self:setCardArea(id, Card.DrawPile, nil)
end
self.discard_pile = {}
table.shuffle(self.draw_pile, seed)
end
---@param card Card
---@param fromAreas? CardArea[]
---@return integer[]
function CardManager:getSubcardsByRule(card, fromAreas)
if card:isVirtual() and #card.subcards == 0 then
return {}
end
local cardIds = {}
fromAreas = fromAreas or Util.DummyTable
for _, cardId in ipairs(card:isVirtual() and card.subcards or { card.id }) do
if #fromAreas == 0 or table.contains(fromAreas, self:getCardArea(cardId)) then
table.insert(cardIds, cardId)
end
end
return cardIds
end
---@param pattern string
---@param num? number
---@param fromPile? string @ 查找的来源区域值为drawPile|discardPile|allPiles
---@return integer[] @ id列表 可能空
function CardManager:getCardsFromPileByRule(pattern, num, fromPile)
num = num or 1
local pileToSearch = self.draw_pile
if fromPile == "discardPile" then
pileToSearch = self.discard_pile
elseif fromPile == "allPiles" then
pileToSearch = table.simpleClone(self.draw_pile)
table.insertTable(pileToSearch, self.discard_pile)
end
if #pileToSearch == 0 then
return {}
end
local cardPack = {}
if num < 3 then
for i = 1, num do
local randomIndex = math.random(1, #pileToSearch)
local curIndex = randomIndex
repeat
local curCardId = pileToSearch[curIndex]
if Fk:getCardById(curCardId):matchPattern(pattern) and not table.contains(cardPack, curCardId) then
table.insert(cardPack, pileToSearch[curIndex])
break
end
curIndex = curIndex + 1
if curIndex > #pileToSearch then
curIndex = 1
end
until curIndex == randomIndex
if #cardPack == 0 then
break
end
end
else
local matchedIds = {}
for _, id in ipairs(pileToSearch) do
if Fk:getCardById(id):matchPattern(pattern) then
table.insert(matchedIds, id)
end
end
local loopTimes = math.min(num, #matchedIds)
for i = 1, loopTimes do
local randomCardId = matchedIds[math.random(1, #matchedIds)]
table.insert(cardPack, randomCardId)
table.removeOne(matchedIds, randomCardId)
end
end
return cardPack
end
function CardManager:toJsonObject()
local printed_cards = {}
for i = -2, -math.huge, -1 do
local c = self.printed_cards[i]
if not c then break end
table.insert(printed_cards, { c.name, c.suit, c.number })
end
local cmarks = {}
for k, v in pairs(self.card_marks) do
cmarks[tostring(k)] = v
end
return {
draw_pile = self.draw_pile,
discard_pile = self.discard_pile,
processing_area = self.processing_area,
void = self.void,
-- card_place和owner_map没必要载入时setCardArea
printed_cards = printed_cards,
card_marks = cmarks,
}
end
function CardManager:loadJsonObject(o)
self.draw_pile = o.draw_pile
self.discard_pile = o.discard_pile
self.processing_area = o.processing_area
self.void = o.void
for _, id in ipairs(o.draw_pile) do self:setCardArea(id, Card.DrawPile, nil) end
for _, id in ipairs(o.discard_pile) do self:setCardArea(id, Card.DiscardPile, nil) end
for _, id in ipairs(o.processing_area) do self:setCardArea(id, Card.Processing, nil) end
for _, id in ipairs(o.void) do self:setCardArea(id, Card.Void, nil) end
for _, data in ipairs(o.printed_cards) do self:printCard(table.unpack(data)) end
for cid, marks in pairs(o.card_marks) do
for k, v in pairs(marks) do
Fk:getCardById(tonumber(cid)):setMark(k, v)
end
end
end
return CardManager

View File

View File

View File

@ -16,6 +16,7 @@
---@field public attached_equip string @ 属于什么装备的技能? ---@field public attached_equip string @ 属于什么装备的技能?
---@field public relate_to_place string @ 主将技/副将技 ---@field public relate_to_place string @ 主将技/副将技
---@field public switchSkillName string @ 转换技名字 ---@field public switchSkillName string @ 转换技名字
---@field public times integer @ 技能剩余次数,负数不显示,正数显示
local Skill = class("Skill") local Skill = class("Skill")
---@alias Frequency integer ---@alias Frequency integer
@ -112,7 +113,7 @@ end
---@param player Player @ 玩家 ---@param player Player @ 玩家
---@return boolean ---@return boolean
function Skill:isEffectable(player) function Skill:isEffectable(player)
if self.cardSkill then if self.cardSkill or self.permanent_skill then
return true return true
end end
@ -144,4 +145,15 @@ function Skill:isPlayerSkill(player)
return not (self:isEquipmentSkill(player) or self.name:endsWith("&")) return not (self:isEquipmentSkill(player) or self.name:endsWith("&"))
end end
---@return integer
function Skill:getTimes()
local ret = self.times
if not ret then
return -1
elseif type(ret) == "function" then
ret = ret(self)
end
return ret
end
return Skill return Skill

View File

@ -26,8 +26,8 @@ end
-- 判断该技能是否可主动发动 -- 判断该技能是否可主动发动
---@param player Player @ 使用者 ---@param player Player @ 使用者
---@param card Card @ 牌 ---@param card? Card @ 牌,若该技能是卡牌的效果技能,需输入此值
---@param extra_data UseExtraData @ 额外数据 ---@param extra_data? UseExtraData @ 额外数据
---@return bool ---@return bool
function ActiveSkill:canUse(player, card, extra_data) function ActiveSkill:canUse(player, card, extra_data)
return self:isEffectable(player) return self:isEffectable(player)
@ -36,7 +36,7 @@ end
-- 判断一张牌是否可被此技能选中 -- 判断一张牌是否可被此技能选中
---@param to_select integer @ 待选牌 ---@param to_select integer @ 待选牌
---@param selected integer[] @ 已选牌 ---@param selected integer[] @ 已选牌
---@param selected_targets integer[] @ 已选目标 ---@param selected_targets? integer[] @ 已选目标
---@return bool ---@return bool
function ActiveSkill:cardFilter(to_select, selected, selected_targets) function ActiveSkill:cardFilter(to_select, selected, selected_targets)
return true return true
@ -46,8 +46,8 @@ end
---@param to_select integer @ 待选目标 ---@param to_select integer @ 待选目标
---@param selected integer[] @ 已选目标 ---@param selected integer[] @ 已选目标
---@param selected_cards integer[] @ 已选牌 ---@param selected_cards integer[] @ 已选牌
---@param card Card @ 牌 ---@param card? Card @ 牌
---@param extra_data UseExtraData @ 额外数据 ---@param extra_data? UseExtraData @ 额外数据
---@return bool ---@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
@ -83,8 +83,8 @@ function ActiveSkill:getMinTargetNum()
end end
-- 获得技能的最大目标数 -- 获得技能的最大目标数
---@param player Player @ 使用者 ---@param player? Player @ 使用者
---@param card Card @ 牌 ---@param card? Card @ 牌
---@return number @ 最大目标数 ---@return number @ 最大目标数
function ActiveSkill:getMaxTargetNum(player, card) function ActiveSkill:getMaxTargetNum(player, card)
local ret local ret
@ -99,12 +99,14 @@ function ActiveSkill:getMaxTargetNum(player, card)
ret = ret[#ret] ret = ret[#ret]
end end
if player and card then
local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or Util.DummyTable local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or Util.DummyTable
for _, skill in ipairs(status_skills) do for _, skill in ipairs(status_skills) do
local correct = skill:getExtraTargetNum(player, self, card) local correct = skill:getExtraTargetNum(player, self, card)
if correct == nil then correct = 0 end if correct == nil then correct = 0 end
ret = ret + correct ret = ret + correct
end end
end
return ret return ret
end end
@ -217,7 +219,7 @@ end
---@param selected integer[] @ 已选目标 ---@param selected integer[] @ 已选目标
---@param selected_cards integer[] @ 已选牌 ---@param selected_cards integer[] @ 已选牌
---@param player Player @ 使用者 ---@param player Player @ 使用者
---@param card Card @ 牌 ---@param card? Card @ 牌
---@return bool ---@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)
@ -254,4 +256,12 @@ 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 to_select integer @ id of the target
---@param selected integer[] @ ids of selected targets
---@param selected_cards integer[] @ ids of selected cards
---@param card Card @ helper
---@param selectable boolean @can be selected
---@param extra_data? any @ extra_data
function ActiveSkill:targetTip(to_select, selected, selected_cards, card, selectable, extra_data) end
return ActiveSkill return ActiveSkill

View File

@ -40,4 +40,13 @@ function TargetModSkill:getExtraTargetNum(player, card_skill, card)
return 0 return 0
end end
---@param player Player
---@param to_select integer @ id of the target
---@param selected integer[] @ ids of selected targets
---@param selected_cards integer[] @ ids of selected cards
---@param card Card @ helper
---@param selectable boolean @can be selected
---@param extra_data? any @ extra_data
function TargetModSkill:getTargetTip(player, to_select, selected, selected_cards, card, selectable, extra_data) end
return TargetModSkill return TargetModSkill

View File

@ -72,9 +72,17 @@ function TriggerSkill:doCost(event, target, player, data)
self.cost_data = cost_data_bak self.cost_data = cost_data_bak
if ret then if ret then
local skill_data = {cost_data = cost_data_bak, tos = {}, cards = {}}
if type(cost_data_bak) == "table" then
if type(cost_data_bak.tos) == "table" and #cost_data_bak.tos > 0 and type(cost_data_bak.tos[1]) == "number" and
room:getPlayerById(cost_data_bak.tos[1]) ~= nil then
skill_data.tos = cost_data_bak.tos
end
if type(cost_data_bak.cards) == "table" then skill_data.cards = cost_data_bak.cards end
end
return room:useSkill(player, self, function() return room:useSkill(player, self, function()
return self:use(event, target, player, data) return self:use(event, target, player, data)
end) end, skill_data)
end end
end end

View File

@ -0,0 +1,22 @@
-- SPDX-License-Identifier: GPL-3.0-or-later
-- UI专用状态技 表示某张牌、某人身份等可能需要在界面上隐藏的元素是否能被某人看到
-- 默认情况参见client_util.lua: CardVisibility 和player.role_shown
---@class VisibilitySkill : StatusSkill
local VisibilitySkill = StatusSkill:subclass("VisibilitySkill")
---@param player Player
---@param card Card
---@return bool
function VisibilitySkill:cardVisible(player, card)
return nil
end
---@param player Player
---@return bool
function VisibilitySkill:roleVisible(player, target)
return nil
end
return VisibilitySkill

View File

@ -7,6 +7,13 @@ Util.FalseFunc = function() return false end
Util.DummyTable = setmetatable({}, { Util.DummyTable = setmetatable({}, {
__newindex = function() error("Cannot assign to dummy table") end __newindex = function() error("Cannot assign to dummy table") end
}) })
Util.array2hash = function(t)
local ret = {}
for _, e in ipairs(t) do
ret[e] = true
end
return ret
end
local metamethods = { local metamethods = {
"__add", "__sub", "__mul", "__div", "__mod", "__pow", "__unm", "__idiv", "__add", "__sub", "__mul", "__div", "__mod", "__pow", "__unm", "__idiv",
@ -133,6 +140,23 @@ Util.Name2SkillMapper = function(e) return Fk.skills[e] end
--- 返回译文 --- 返回译文
Util.TranslateMapper = function(str) return Fk:translate(str) end Util.TranslateMapper = function(str) return Fk:translate(str) end
-- 阶段int型和string型互换
---@return string|integer
Util.PhaseStrMapper = function(phase)
local phase_table = {
[Player.RoundStart] = "phase_roundstart",
[Player.Start] = "phase_start",
[Player.Judge] = "phase_judge",
[Player.Draw] = "phase_draw",
[Player.Play] = "phase_play",
[Player.Discard] = "phase_discard",
[Player.Finish] = "phase_finish",
[Player.NotActive] = "phase_notactive",
[Player.PhaseNone] = "phase_phasenone",
}
return type(phase) == "string" and table.indexOf(phase_table, phase) or phase_table[phase]
end
-- for card preset -- for card preset
--- 全局卡牌(包括自己)的canUse --- 全局卡牌(包括自己)的canUse
@ -249,14 +273,16 @@ function table:contains(element)
end end
end end
function table:shuffle() function table:shuffle(seed)
seed = seed or math.random(2 << 32 - 1)
local rnd = fk.QRandomGenerator(seed)
if #self == 2 then if #self == 2 then
if math.random() < 0.5 then if rnd:random() < 0.5 then
self[1], self[2] = self[2], self[1] self[1], self[2] = self[2], self[1]
end end
else else
for i = #self, 2, -1 do for i = #self, 2, -1 do
local j = math.random(i) local j = rnd:random(i)
self[i], self[j] = self[j], self[i] self[i], self[j] = self[j], self[i]
end end
end end
@ -414,6 +440,17 @@ function table:assign(targetTbl)
end end
end end
function table:hasIntersection(table)
local hash = {}
for _, value in ipairs(self) do
hash[value] = true
end
for _, value in ipairs(table) do
if hash[value] then return true end
end
return false
end
function table.empty(t) function table.empty(t)
return next(t) == nil return next(t) == nil
end end

View File

@ -18,6 +18,7 @@ MaxCardsSkill = require "core.skill_type.max_cards"
TargetModSkill = require "core.skill_type.target_mod" TargetModSkill = require "core.skill_type.target_mod"
FilterSkill = require "core.skill_type.filter" FilterSkill = require "core.skill_type.filter"
InvaliditySkill = require "lua.core.skill_type.invalidity" InvaliditySkill = require "lua.core.skill_type.invalidity"
VisibilitySkill = require "lua.core.skill_type.visibility"
BasicCard = require "core.card_type.basic" BasicCard = require "core.card_type.basic"
local Trick = require "core.card_type.trick" local Trick = require "core.card_type.trick"
@ -71,6 +72,7 @@ local function readUsableSpecToSkill(skill, spec)
} }
skill.distance_limit = spec.distance_limit or skill.distance_limit skill.distance_limit = spec.distance_limit or skill.distance_limit
skill.expand_pile = spec.expand_pile skill.expand_pile = spec.expand_pile
skill.times = spec.times or skill.times
end end
local function readStatusSpecToSkill(skill, spec) local function readStatusSpecToSkill(skill, spec)
@ -85,6 +87,7 @@ end
---@field public max_turn_use_time? integer ---@field public max_turn_use_time? integer
---@field public max_round_use_time? integer ---@field public max_round_use_time? integer
---@field public max_game_use_time? integer ---@field public max_game_use_time? integer
---@field public times? integer | fun(self: UsableSkill): integer
---@class StatusSkillSpec: StatusSkill ---@class StatusSkillSpec: StatusSkill
@ -214,6 +217,7 @@ function fk.CreateActiveSkill(spec)
if spec.on_effect then skill.onEffect = spec.on_effect end if spec.on_effect then skill.onEffect = spec.on_effect end
if spec.on_nullified then skill.onNullified = spec.on_nullified end if spec.on_nullified then skill.onNullified = spec.on_nullified end
if spec.prompt then skill.prompt = spec.prompt end if spec.prompt then skill.prompt = spec.prompt end
if spec.target_tip then skill.targetTip = spec.target_tip end
if spec.interaction then if spec.interaction then
skill.interaction = setmetatable({}, { skill.interaction = setmetatable({}, {
@ -237,7 +241,7 @@ end
---@field public enabled_at_response? fun(self: ViewAsSkill, player: Player, response: boolean): boolean? ---@field public enabled_at_response? fun(self: ViewAsSkill, player: Player, response: boolean): boolean?
---@field public before_use? fun(self: ViewAsSkill, player: ServerPlayer, use: CardUseStruct): string? ---@field public before_use? fun(self: ViewAsSkill, player: ServerPlayer, use: CardUseStruct): string?
---@field public after_use? fun(self: ViewAsSkill, player: ServerPlayer, use: CardUseStruct): string? ---@field public after_use? fun(self: ViewAsSkill, player: ServerPlayer, use: CardUseStruct): string?
---@field public prompt? string|fun(self: ActiveSkill, selected: integer[], selected_cards: integer[]): string ---@field public prompt? string|fun(self: ActiveSkill, selected_cards: integer[], selected: integer[]): string
---@param spec ViewAsSkillSpec ---@param spec ViewAsSkillSpec
---@return ViewAsSkill ---@return ViewAsSkill
@ -392,6 +396,7 @@ end
---@field public bypass_distances? fun(self: TargetModSkill, player: Player, skill: ActiveSkill, card: Card, to: Player): boolean? ---@field public bypass_distances? fun(self: TargetModSkill, player: Player, skill: ActiveSkill, card: Card, to: Player): boolean?
---@field public distance_limit_func? fun(self: TargetModSkill, player: Player, skill: ActiveSkill, card: Card, to: Player): number? ---@field public distance_limit_func? fun(self: TargetModSkill, player: Player, skill: ActiveSkill, card: Card, to: Player): number?
---@field public extra_target_func? fun(self: TargetModSkill, player: Player, skill: ActiveSkill, card: Card): number? ---@field public extra_target_func? fun(self: TargetModSkill, player: Player, skill: ActiveSkill, card: Card): number?
---@field public target_tip_func? fun(self: TargetModSkill, player: Player, to_select: integer, selected: integer[], selected_cards: integer[], card: Card, selectable: boolean, extra_data: any): string|TargetTipDataSpec?
---@param spec TargetModSpec ---@param spec TargetModSpec
---@return TargetModSkill ---@return TargetModSkill
@ -415,6 +420,9 @@ function fk.CreateTargetModSkill(spec)
if spec.extra_target_func then if spec.extra_target_func then
skill.getExtraTargetNum = spec.extra_target_func skill.getExtraTargetNum = spec.extra_target_func
end end
if spec.target_tip_func then
skill.getTargetTip = spec.target_tip_func
end
return skill return skill
end end
@ -453,6 +461,22 @@ function fk.CreateInvaliditySkill(spec)
return skill return skill
end end
---@class VisibilitySpec: StatusSkillSpec
---@field public card_visible? fun(self: VisibilitySkill, player: Player, card: Card): boolean?
---@field public role_visible? fun(self: VisibilitySkill, player: Player, target: Player): boolean?
---@param spec VisibilitySpec
function fk.CreateVisibilitySkill(spec)
assert(type(spec.name) == "string")
local skill = VisibilitySkill:new(spec.name)
readStatusSpecToSkill(skill, spec)
if spec.card_visible then skill.cardVisible = spec.card_visible end
if spec.role_visible then skill.roleVisible = spec.role_visible end
return skill
end
---@class CardSpec: Card ---@class CardSpec: Card
---@field public skill? Skill ---@field public skill? Skill
---@field public equip_skill? Skill ---@field public equip_skill? Skill
@ -663,3 +687,11 @@ end
---@field qml_path string | fun(player: Player, data: any): string ---@field qml_path string | fun(player: Player, data: any): string
---@field update_func? fun(player: ServerPlayer, data: any) ---@field update_func? fun(player: ServerPlayer, data: any)
---@field default_choice? fun(player: ServerPlayer, data: any): any ---@field default_choice? fun(player: ServerPlayer, data: any): any
---@class TargetTipDataSpec
---@field content string
---@field type "normal"|"warning"
---@class TargetTipSpec
---@field name string
---@field target_tip fun(self: ActiveSkill, to_select: integer, selected: integer[], selected_cards: integer[], card: Card, selectable: boolean, extra_data: any): string|TargetTipDataSpec?

View File

@ -55,7 +55,8 @@ UsableSkill = require "core.skill_type.usable_skill"
StatusSkill = require "core.skill_type.status_skill" StatusSkill = require "core.skill_type.status_skill"
Player = require "core.player" Player = require "core.player"
GameMode = require "core.game_mode" GameMode = require "core.game_mode"
AbstractRoom = require "core.abstract_room" AbstractRoom = require "core.room.abstract_room"
RequestHandler = require "core.request_handler"
UI = require "ui-util" UI = require "ui-util"
-- 读取配置文件。 -- 读取配置文件。

View File

@ -244,6 +244,9 @@ local function where(info, context_lines)
source = {} source = {}
local filename = info.source:match("@(.*)") local filename = info.source:match("@(.*)")
if filename then if filename then
if UsingNewCore and (filename:startsWith("./lua/") or filename:startsWith("lua/")) then
filename = "./packages/freekill-core/" .. filename
end
pcall(function() for line in io.lines(filename) do table.insert(source, line) end end) pcall(function() for line in io.lines(filename) do table.insert(source, line) end end)
elseif info.source then elseif info.source then
for line in info.source:gmatch("(.-)\n") do table.insert(source, line) end for line in info.source:gmatch("(.-)\n") do table.insert(source, line) end

View File

@ -35,6 +35,8 @@ function Object:isInstanceOf(class) end
---@return boolean ---@return boolean
function Object:isSubclassOf(class) end function Object:isSubclassOf(class) end
function Object:include(e) end
---@class json ---@class json
json = {} json = {}

View File

@ -34,11 +34,12 @@ end
function AI:makeReply() function AI:makeReply()
Self = self.player Self = self.player
local start = os.getms() -- local start = os.getms()
local ret = self.cb_table[self.command] and self.cb_table[self.command](self, self.jsonData) or "__cancel" local ret = self.cb_table[self.command] and self.cb_table[self.command](self, self.jsonData) or "__cancel"
local to_delay = 500 - (os.getms() - start) / 1000 if ret == "" then ret = "__cancel" end
-- local to_delay = 500 - (os.getms() - start) / 1000
-- print(to_delay) -- print(to_delay)
self.room:delay(to_delay) -- self.room:delay(to_delay)
return ret return ret
end end

View File

@ -6,75 +6,118 @@ local RandomAI = AI:subclass("RandomAI")
---@param self RandomAI ---@param self RandomAI
---@param skill ActiveSkill ---@param skill ActiveSkill
---@param card? Card ---@param card? Card
function RandomAI:useActiveSkill(skill, card) ---@param extra_data? table
function RandomAI:useActiveSkill(skill, card, extra_data)
local room = self.room local room = self.room
local player = self.player local player = self.player
extra_data = extra_data or Util.DummyTable
if skill:isInstanceOf(ViewAsSkill) then return "" end if skill:isInstanceOf(ViewAsSkill) then return "" end
local filter_func = skill.cardFilter if self.command == "PlayCard" and (not skill:canUse(player, card) or (card and player:prohibitUse(card))) then
if card then return ""
filter_func = Util.FalseFunc
end end
if self.command == "PlayCard" and (not skill:canUse(player, card) or player:prohibitUse(card)) then local interaction_data
return "" if skill and skill.interaction then
skill.interaction.data = nil
interaction_data = skill:interaction()
if type(interaction_data) == "table" then
if interaction_data.type == "spin" then
interaction_data = math.random(interaction_data.from, interaction_data.to)
elseif interaction_data.type == "combo" then
interaction_data = interaction_data.default
else
-- use default data when handling custom interaction
interaction_data = interaction_data.default or interaction_data.default_choice or nil
end
end
if interaction_data == nil then return "" end
skill.interaction.data = interaction_data
end end
local max_try_times = 100 local max_try_times = 100
local selected_targets = {} local selected_targets = {}
local selected_cards = {} local selected_cards = {}
-- FIXME: ...
-- local min = skill:getMinTargetNum()
-- local max = skill:getMaxTargetNum(player, card)
-- local min_card = skill:getMinCardNum()
-- local max_card = skill:getMaxCardNum()
-- FIXME: ViewAsSkill can be buggy here
for _ = 0, max_try_times do
if skill:feasible(selected_targets, selected_cards, self.player, card) then break end
local avail_targets = table.filter(room:getAlivePlayers(), function(p)
local ret = skill:targetFilter(p.id, selected_targets, selected_cards, card or Fk:cloneCard'zixing')
if ret and card then
if player:isProhibited(p, card) then
ret = false
end
end
return ret
end)
avail_targets = table.map(avail_targets, function(p) return p.id end)
local avail_cards = table.filter(player:getCardIds{ Player.Hand, Player.Equip }, function(id)
return filter_func(skill, id, selected_cards, selected_targets)
end)
if #avail_targets == 0 and #avail_cards == 0 then break end -- TODO: ng that 'must_targets' & 'exclusive_targets' should be rebuilt later
table.insertIfNeed(selected_targets, table.random(avail_targets)) local limited_targets = {}
table.insertIfNeed(selected_cards, table.random(avail_cards)) for _, name in ipairs({"must_targets","exclusive_targets","include_targets"}) do
if type(extra_data[name]) == "table" then
table.insertTableIfNeed(limited_targets, extra_data[name])
end end
if skill:feasible(selected_targets, selected_cards, self.player, card) then end
local all_cards = player:getCardIds{ Player.Hand, Player.Equip }
if skill.expand_pile then
if type(skill.expand_pile) == "string" then
table.insertTableIfNeed(all_cards, player.special_cards[skill.expand_pile] or {})
elseif type(skill.expand_pile) == "table" then
table.insertTableIfNeed(all_cards, skill.expand_pile)
end
end
--local max_target_num = skill:getMaxTargetNum(player, card)
local card_filter_func = card and Util.FalseFunc or skill.cardFilter
local firstTry
for _ = 0, max_try_times do
if not firstTry and skill:feasible(selected_targets, selected_cards, self.player, card) then
firstTry = {table.simpleClone(selected_targets), table.simpleClone(selected_cards)}
end
if firstTry and math.random() < 0.1 then break end
local avail_targets = table.filter(room.alive_players, function(p)
return not table.contains(selected_targets, p.id) and (#limited_targets == 0 or table.contains(limited_targets, p.id))
and skill:targetFilter(p.id, selected_targets, selected_cards, card)
and (not card or not player:isProhibited(p, card))
end)
local avail_cards = table.filter(all_cards, function(id)
return not table.contains(selected_cards, id) and card_filter_func(skill, id, selected_cards, selected_targets)
end)
local random_list = table.connect(avail_targets, avail_cards)
if #random_list == 0 then break end
local randomIndex = math.random(#random_list)
if randomIndex <= #avail_targets then
table.insertIfNeed(selected_targets, random_list[randomIndex].id)
else
table.insertIfNeed(selected_cards, random_list[randomIndex])
end
end
local feasibleCheck = function () return skill:feasible(selected_targets, selected_cards, self.player, card) end
if firstTry and not feasibleCheck() then
selected_targets = firstTry[1]
selected_cards = firstTry[2]
end
if feasibleCheck() then
local ret = json.encode{ local ret = json.encode{
card = card and card.id or json.encode{ card = card and card.id or json.encode{
skill = skill.name, skill = skill.name,
subcards = selected_cards, subcards = selected_cards,
}, },
targets = selected_targets, targets = selected_targets,
interaction_data = interaction_data,
} }
-- print(ret)
return ret return ret
end end
return "" return ""
end end
---@param self RandomAI
---@param skill ViewAsSkill ---@param skill ViewAsSkill
function RandomAI:useVSSkill(skill, pattern, cancelable, extra_data) ---@param pattern? string @ no 'pattern' means it needs to pass the 'canUse' check
---@param cancelable? bool
---@param extra_data? table
---@param cardResponsing? bool
function RandomAI:useVSSkill(skill, pattern, cancelable, extra_data, cardResponsing)
local player = self.player local player = self.player
local room = self.room local room = self.room
local precondition local precondition
if not skill then return nil end cancelable = cancelable or (cancelable == nil)
extra_data = extra_data or Util.DummyTable
if not skill then return "" end
if self.command == "PlayCard" then if not pattern then
precondition = skill:enabledAtPlay(player) precondition = skill:enabledAtPlay(player)
if not precondition then return nil end if not precondition then return "" end
local exp = Exppattern:Parse(skill.pattern) local exp = Exppattern:Parse(skill.pattern)
local cnames = {} local cnames = {}
for _, m in ipairs(exp.matchers) do for _, m in ipairs(exp.matchers) do
@ -82,35 +125,107 @@ function RandomAI:useVSSkill(skill, pattern, cancelable, extra_data)
end end
for _, n in ipairs(cnames) do for _, n in ipairs(cnames) do
local c = Fk:cloneCard(n) local c = Fk:cloneCard(n)
precondition = c.skill:canUse(Self, c) precondition = c.skill:canUse(player, c, extra_data)
if precondition then break end if precondition then break end
end end
else else
precondition = skill:enabledAtResponse(player) precondition = skill:enabledAtResponse(player, cardResponsing) and Exppattern:Parse(pattern):matchExp(skill.pattern)
if not precondition then return nil end
local exp = Exppattern:Parse(pattern)
precondition = exp:matchExp(skill.pattern)
end end
if (not precondition) or math.random() < 0.2 then return nil end if (not precondition) or (cancelable and math.random() < 0.2) then return "" end
local interaction_data
if skill.interaction then
skill.interaction.data = nil
interaction_data = skill:interaction()
if type(interaction_data) == "table" then
if interaction_data.type == "spin" then
interaction_data = math.random(interaction_data.from, interaction_data.to)
elseif interaction_data.type == "combo" then
interaction_data = interaction_data.default
else
-- use default data when handling custom interaction
interaction_data = interaction_data.default or interaction_data.default_choice or nil
end
end
if interaction_data == nil then return "" end
skill.interaction.data = interaction_data
end
local selected_cards = {} local selected_cards = {}
local selected_targets = {}
local card
local max_try_time = 100 local max_try_time = 100
local all_cards = player:getCardIds{ Player.Hand, Player.Equip }
if skill.expand_pile then
if type(skill.expand_pile) == "string" then
table.insertTableIfNeed(all_cards, player.special_cards[skill.expand_pile] or {})
elseif type(skill.expand_pile) == "table" then
table.insertTableIfNeed(all_cards, skill.expand_pile)
end
end
for _ = 0, max_try_time do for _ = 0, max_try_time do
local avail_cards = table.filter(player:getCardIds{ Player.Hand, Player.Equip }, function(id) card = skill:viewAs(selected_cards)
return skill:cardFilter(id, selected_cards) if card then break end
local avail_cards = table.filter(all_cards, function(id)
return not table.contains(selected_cards, id) and skill:cardFilter(id, selected_cards)
end) end)
if #avail_cards == 0 then break end if #avail_cards == 0 then break end
table.insert(selected_cards, table.random(avail_cards)) table.insert(selected_cards, table.random(avail_cards))
if skill:viewAs(selected_cards) then end
return {
if not card then return "" end
if cardResponsing then
if not player:prohibitResponse(card) then
return json.encode{
card = json.encode{
skill = skill.name, skill = skill.name,
subcards = selected_cards, subcards = selected_cards,
},
targets = {},
interaction_data = interaction_data,
} }
end end
return ""
end end
return nil
if player:prohibitUse(card) then return "" end
if pattern or player:canUse(card, extra_data) then
local limited_targets = {}
for _, name in ipairs({"must_targets","exclusive_targets","include_targets"}) do
if type(extra_data[name]) == "table" then
table.insertTableIfNeed(limited_targets, extra_data[name])
end
end
for _ = 0, max_try_time do
if card.skill:feasible(selected_targets, selected_cards, player, card) then break end
local avail_targets = table.filter(room.alive_players, function(p)
return not table.contains(selected_targets, p.id) and (#limited_targets == 0 or table.contains(limited_targets, p.id))
and card.skill:targetFilter(p.id, selected_targets, selected_cards, card, extra_data)
and not player:isProhibited(p, card)
end)
if #avail_targets == 0 then break end
table.insert(selected_targets, table.random(avail_targets).id)
end
if card.skill:feasible(selected_targets, selected_cards, player, card, extra_data) then
local ret = json.encode{
card = json.encode{
skill = skill.name,
subcards = selected_cards,
},
targets = selected_targets,
interaction_data = interaction_data,
}
return ret
end
end
return ""
end end
---@type table<string, fun(self: RandomAI, jsonData: string): string> ---@type table<string, fun(self: RandomAI, jsonData: string): string>
@ -119,6 +234,7 @@ local random_cb = {}
random_cb["AskForUseActiveSkill"] = function(self, jsonData) random_cb["AskForUseActiveSkill"] = function(self, jsonData)
local data = json.decode(jsonData) local data = json.decode(jsonData)
local skill = Fk.skills[data[1]] local skill = Fk.skills[data[1]]
if not skill then return "" end
local cancelable = data[3] local cancelable = data[3]
if cancelable and math.random() < 0.25 then return "" end if cancelable and math.random() < 0.25 then return "" end
local extra_data = data[4] local extra_data = data[4]
@ -126,13 +242,47 @@ random_cb["AskForUseActiveSkill"] = function(self, jsonData)
skill[k] = v skill[k] = v
end end
if skill:isInstanceOf(ViewAsSkill) then if skill:isInstanceOf(ViewAsSkill) then
return RandomAI.useVSSkill(skill) return self:useVSSkill(skill, nil, cancelable, extra_data)
end end
return RandomAI.useActiveSkill(self, skill) local player = self.player
if skill.name == "choose_cards_skill" then
local exp = Exppattern:Parse(extra_data.pattern)
local cards = table.filter(player:getCardIds(extra_data.include_equip and "he" or "h"), function(cid)
return exp:match(Fk:getCardById(cid))
end)
local maxNum = extra_data.num
local minNum = extra_data.min_num
cards = table.random(cards, math.random(minNum, maxNum))
return json.encode{
card = json.encode{
skill = skill.name,
subcards = cards,
},
targets = {},
}
end
return self:useActiveSkill(skill)
end end
random_cb["AskForSkillInvoke"] = function(self, jsonData) random_cb["AskForSkillInvoke"] = function(self, jsonData)
return table.random{"1", ""} local skill_name, prompt = table.unpack(json.decode(jsonData))
local chance = 0.55
if Fk.skills[skill_name] ~= nil and self.player:hasSkill(skill_name) then
chance = 0.8
end
if math.random() < chance then
return "1"
end
return ""
end
random_cb["AskForChoice"] = function(self, jsonData)
local data = json.decode(jsonData)
local choices = data[1]
if table.contains(choices, "Cancel") and #choices > 1 and math.random() < 0.6 then
table.removeOne(choices, "Cancel")
end
return table.random(choices)
end end
random_cb["AskForUseCard"] = function(self, jsonData) random_cb["AskForUseCard"] = function(self, jsonData)
@ -140,61 +290,74 @@ random_cb["AskForUseCard"] = function(self, jsonData)
local data = json.decode(jsonData) local data = json.decode(jsonData)
local card_name = data[1] local card_name = data[1]
local pattern = data[2] or card_name local pattern = data[2] or card_name
local cancelable = data[4] or true local prompt = data[3]
local exp = Exppattern:Parse(pattern) local cancelable = data[4]
local extra_data = data[5] or Util.DummyTable
local avail_cards = table.map(player:getCardIds("he"), Util.Id2CardMapper) if card_name == "peach" then
avail_cards = table.filter(avail_cards, function(c) if type(extra_data.must_targets) == "table" and extra_data.must_targets[1] ~= player.id and math.random() < 0.8 then
return ""
end
end
if (cancelable and math.random() < 0.15) then return "" end
local cards = table.map(self.player:getCardIds("he&"), Util.Id2CardMapper)
local exp = Exppattern:Parse(pattern)
cards = table.filter(cards, function(c)
return exp:match(c) and not player:prohibitUse(c) return exp:match(c) and not player:prohibitUse(c)
end) end)
if #avail_cards > 0 then local vss = table.filter(player:getAllSkills(), function(s)
if math.random() < 0.25 then return "" end return s:isInstanceOf(ViewAsSkill)
for _, card in ipairs(avail_cards) do
local skill = card.skill
local max_try_times = 100
local selected_targets = {}
local min = skill:getMinTargetNum()
local max = skill:getMaxTargetNum(player, card)
local min_card = skill:getMinCardNum()
local max_card = skill:getMaxCardNum()
for _ = 0, max_try_times do
if skill:feasible(selected_targets, { card.id }, self.player, card) then
return json.encode{
card = table.random(avail_cards).id,
targets = selected_targets,
}
end
local avail_targets = table.filter(self.room:getAlivePlayers(), function(p)
return skill:targetFilter(p.id, selected_targets, {card.id}, card or Fk:cloneCard'zixing')
end) end)
avail_targets = table.map(avail_targets, function(p) return p.id end) table.insertTable(cards, vss)
if #avail_targets == 0 and #avail_cards == 0 then break end while #cards > 0 do
table.insertIfNeed(selected_targets, table.random(avail_targets)) local sth = table.remove(cards, math.random(#cards))
end if sth:isInstanceOf(Card) then
local ret = self:useActiveSkill(sth.skill, sth, extra_data)
if ret ~= "" then return ret end
else
local ret = self:useVSSkill(sth, pattern, cancelable, extra_data)
if ret ~= "" then return ret end
end end
end end
return "" return ""
end end
random_cb["AskForResponseCard"] = function(self, jsonData) random_cb["AskForResponseCard"] = function(self, jsonData)
local data = json.decode(jsonData) local data = json.decode(jsonData)
local pattern = data[2] local pattern = data[2]
local cancelable = true local cancelable = data[4] or true
local extra_data = data[5] or Util.DummyTable
local player = self.player
local cards = table.map(self.player:getCardIds("he&"), Util.Id2CardMapper)
local exp = Exppattern:Parse(pattern) local exp = Exppattern:Parse(pattern)
local avail_cards = table.filter(self.player:getCardIds{ Player.Hand, Player.Equip }, function(id) cards = table.filter(cards, function(c)
return exp:match(Fk:getCardById(id)) return exp:match(c) and not player:prohibitResponse(c)
end) end)
if #avail_cards > 0 then return json.encode{
card = table.random(avail_cards), local vss = table.filter(player:getAllSkills(), function(s)
targets = {}, return s:isInstanceOf(ViewAsSkill)
} end end)
-- TODO: vs skill table.insertTable(cards, vss)
while #cards > 0 do
local sth = table.remove(cards, math.random(#cards))
if sth:isInstanceOf(Card) then
return json.encode{ card = sth.id, targets = {} }
else
local ret = self:useVSSkill(sth, pattern, cancelable, extra_data, true)
if ret ~= "" then return ret end
end
end
return "" return ""
end end
random_cb["PlayCard"] = function(self, jsonData) random_cb["PlayCard"] = function(self, jsonData)
local cards = table.map(self.player:getCardIds(Player.Hand), Util.Id2CardMapper) local cards = table.map(self.player:getCardIds("h&"), Util.Id2CardMapper)
local actives = table.filter(self.player:getAllSkills(), function(s) local actives = table.filter(self.player:getAllSkills(), function(s)
return s:isInstanceOf(ActiveSkill) return s:isInstanceOf(ActiveSkill)
end) end)
@ -205,16 +368,13 @@ random_cb["PlayCard"] = function(self, jsonData)
table.insertTable(cards, vss) table.insertTable(cards, vss)
while #cards > 0 do while #cards > 0 do
local sth = table.random(cards) local sth = table.remove(cards, math.random(#cards))
if sth:isInstanceOf(Card) then if sth:isInstanceOf(Card) then
local card = sth local card = sth
local skill = card.skill ---@type ActiveSkill local skill = card.skill ---@type ActiveSkill
if math.random() > 0.15 then if math.random() > 0.15 then
local ret = RandomAI.useActiveSkill(self, skill, card) local ret = RandomAI.useActiveSkill(self, skill, card)
if ret ~= "" then return ret end if ret ~= "" then return ret end
table.removeOne(cards, card)
else
table.removeOne(cards, card)
end end
elseif sth:isInstanceOf(ActiveSkill) then elseif sth:isInstanceOf(ActiveSkill) then
local active = sth local active = sth
@ -222,14 +382,12 @@ random_cb["PlayCard"] = function(self, jsonData)
local ret = RandomAI.useActiveSkill(self, active, nil) local ret = RandomAI.useActiveSkill(self, active, nil)
if ret ~= "" then return ret end if ret ~= "" then return ret end
end end
table.removeOne(cards, active)
else else
local vs = sth local vs = sth
if math.random() > 0.20 then if math.random() > 0.20 then
local ret = self:useVSSkill(vs) local ret = self:useVSSkill(vs)
-- TODO: handle vs result if ret ~= "" then return ret end
end end
table.removeOne(cards, vs)
end end
end end

View File

@ -12,6 +12,7 @@ fk.BeforeTurnStart = 83
fk.TurnStart = 3 fk.TurnStart = 3
fk.TurnEnd = 73 fk.TurnEnd = 73
fk.AfterTurnEnd = 84 fk.AfterTurnEnd = 84
fk.EventTurnChanging = 96
fk.EventPhaseStart = 4 fk.EventPhaseStart = 4
fk.EventPhaseProceeding = 5 fk.EventPhaseProceeding = 5
fk.EventPhaseEnd = 6 fk.EventPhaseEnd = 6
@ -95,6 +96,10 @@ fk.GameFinished = 66
fk.AskForCardUse = 67 fk.AskForCardUse = 67
fk.AskForCardResponse = 68 fk.AskForCardResponse = 68
fk.HandleAskForPlayCard = 97
fk.AfterAskForCardUse = 98
fk.AfterAskForCardResponse = 99
fk.AfterAskForNullification = 100
fk.StartPindian = 69 fk.StartPindian = 69
fk.PindianCardsDisplayed = 70 fk.PindianCardsDisplayed = 70
@ -138,4 +143,10 @@ fk.AfterPropertyChange = 94
fk.AfterPlayerRevived = 95 fk.AfterPlayerRevived = 95
fk.NumOfEvents = 96 -- 96 = EventTurnChanging
-- 97 = HandleAskForPlayCard
-- 98 = AfterAskForCardUse
-- 99 = AfterAskForCardResponse
-- 100 = AfterAskForNullification
fk.NumOfEvents = 101

View File

@ -1,5 +1,15 @@
-- SPDX-License-Identifier: GPL-3.0-or-later -- SPDX-License-Identifier: GPL-3.0-or-later
---@class DeathEventWrappers: Object
local DeathEventWrappers = {} -- mixin
---@return boolean
local function exec(tp, ...)
local event = tp:create(...)
local _, ret = event:exec()
return ret
end
---@class GameEvent.Dying : GameEvent ---@class GameEvent.Dying : GameEvent
local Dying = GameEvent:subclass("GameEvent.Dying") local Dying = GameEvent:subclass("GameEvent.Dying")
function Dying:main() function Dying:main()
@ -43,6 +53,12 @@ function Dying:exit()
logic:trigger(fk.AfterDying, dyingPlayer, dyingStruct, self.interrupted) logic:trigger(fk.AfterDying, dyingPlayer, dyingStruct, self.interrupted)
end end
--- 根据濒死数据让人进入濒死。
---@param dyingStruct DyingStruct
function DeathEventWrappers:enterDying(dyingStruct)
return exec(Dying, dyingStruct)
end
---@class GameEvent.Death : GameEvent ---@class GameEvent.Death : GameEvent
local Death = GameEvent:subclass("GameEvent.Death") local Death = GameEvent:subclass("GameEvent.Death")
function Death:prepare() function Death:prepare()
@ -103,6 +119,12 @@ function Death:main()
logic:trigger(fk.Deathed, victim, deathStruct) logic:trigger(fk.Deathed, victim, deathStruct)
end end
--- 根据死亡数据杀死角色。
---@param deathStruct DeathStruct
function DeathEventWrappers:killPlayer(deathStruct)
return exec(Death, deathStruct)
end
---@class GameEvent.Revive : GameEvent ---@class GameEvent.Revive : GameEvent
local Revive = GameEvent:subclass("GameEvent.Revive") local Revive = GameEvent:subclass("GameEvent.Revive")
function Revive:main() function Revive:main()
@ -125,4 +147,10 @@ function Revive:main()
room.logic:trigger(fk.AfterPlayerRevived, player, { reason = reason }) room.logic:trigger(fk.AfterPlayerRevived, player, { reason = reason })
end end
return { Dying, Death, Revive } ---@param player ServerPlayer
---@param sendLog? bool
function DeathEventWrappers:revivePlayer(player, sendLog, reason)
return exec(Revive, player, sendLog, reason)
end
return { Dying, Death, Revive, DeathEventWrappers }

View File

@ -80,61 +80,20 @@ function DrawInitial:main()
return return
end end
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) local request = Request:new("AskForSkillInvoke", room.alive_players)
room.room:setRequestTimer(room.timeout * 1000 + 1000) for _, p in ipairs(room.alive_players) do
request:setData(p, { "AskForLuckCard", "#AskForLuckCard:::" .. room.settings.luckTime })
local remainTime = room.timeout + 1
local currentTime = os.time()
local elapsed = 0
for _, id in ipairs(luck_data.playerList) do
local pl = room:getPlayerById(id)
if luck_data[id].luckTime > 0 then
pl.serverplayer:setThinking(true)
end end
end request.luck_data = luck_data
request.accept_cancel = true
while true do request:ask()
elapsed = os.time() - currentTime
if remainTime - elapsed <= 0 then
break
end
-- local ldata = room:getTag("LuckCardData")
local ldata = luck_data
if table.every(ldata.playerList, function(id)
return ldata[id].luckTime == 0
end) then
break
end
for _, id in ipairs(ldata.playerList) do
local pl = room:getPlayerById(id)
if pl._splayer:getState() ~= fk.Player_Online then
ldata[id].luckTime = 0
pl.serverplayer:setThinking(false)
end
end
-- room:setTag("LuckCardData", ldata)
room:checkNoHuman()
coroutine.yield("__handleRequest", (remainTime - elapsed) * 1000)
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
room.logic:trigger(fk.AfterDrawInitialCards, player, draw_data) room.logic:trigger(fk.AfterDrawInitialCards, player, draw_data)
end end
room:removeTag("LuckCardData")
end end
---@class GameEvent.Round : GameEvent ---@class GameEvent.Round : GameEvent
@ -143,12 +102,27 @@ local Round = GameEvent:subclass("GameEvent.Round")
function Round:action() function Round:action()
local room = self.room local room = self.room
local p local p
local nextTurnOwner
local skipRoundPlus = false
repeat repeat
nextTurnOwner = nil
skipRoundPlus = false
p = room.current p = room.current
GameEvent.Turn:create(p):exec() GameEvent.Turn:create(p):exec()
if room.game_finished then break end if room.game_finished then break end
room.current = room.current:getNextAlive(true, nil, true)
until p.seat >= p:getNextAlive(true, nil, true).seat local changingData = { from = room.current, to = room.current:getNextAlive(true, nil, true), skipRoundPlus = false }
room.logic:trigger(fk.EventTurnChanging, room.current, changingData, true)
skipRoundPlus = changingData.skipRoundPlus
local nextAlive = room.current:getNextAlive(true, nil, true)
if nextAlive ~= changingData.to and not changingData.to.dead then
room.current = changingData.to
nextTurnOwner = changingData.to
else
room.current = nextAlive
end
until p.seat >= (nextTurnOwner or p:getNextAlive(true, nil, true)).seat and not skipRoundPlus
end end
function Round:main() function Round:main()
@ -269,6 +243,8 @@ function Turn:clear()
logic:trigger(fk.AfterTurnEnd, current, nil, self.interrupted) logic:trigger(fk.AfterTurnEnd, current, nil, self.interrupted)
current.phase = Player.NotActive current.phase = Player.NotActive
room:setTag("endTurn", false)
for _, p in ipairs(room.players) do for _, p in ipairs(room.players) do
p:setCardUseHistory("", 0, Player.HistoryTurn) p:setCardUseHistory("", 0, Player.HistoryTurn)
p:setSkillUseHistory("", 0, Player.HistoryTurn) p:setSkillUseHistory("", 0, Player.HistoryTurn)
@ -317,6 +293,7 @@ function Phase:main()
[Player.Judge] = function() [Player.Judge] = function()
local cards = player:getCardIds(Player.Judge) local cards = player:getCardIds(Player.Judge)
while #cards > 0 do while #cards > 0 do
if player._phase_end then break end
local cid = table.remove(cards) local cid = table.remove(cards)
if not cid then return end if not cid then return end
local card = player:removeVirtualEquip(cid) local card = player:removeVirtualEquip(cid)
@ -344,12 +321,17 @@ function Phase:main()
n = 2 n = 2
} }
room.logic:trigger(fk.DrawNCards, player, data) room.logic:trigger(fk.DrawNCards, player, data)
if not player._phase_end then
room:drawCards(player, data.n, "game_rule") room:drawCards(player, data.n, "game_rule")
end
room.logic:trigger(fk.AfterDrawNCards, player, data) room.logic:trigger(fk.AfterDrawNCards, player, data)
end, end,
[Player.Play] = function() [Player.Play] = function()
player._play_phase_end = false player._play_phase_end = false
room:doBroadcastNotify("UpdateSkill", "", {player})
while not player.dead do while not player.dead do
if player._phase_end then break end
logic:trigger(fk.StartPlayCard, player, nil, true) logic:trigger(fk.StartPlayCard, player, nil, true)
room:notifyMoveFocus(player, "PlayCard") room:notifyMoveFocus(player, "PlayCard")
local result = room:doRequest(player, "PlayCard", player.id) local result = room:doRequest(player, "PlayCard", player.id)
@ -359,14 +341,10 @@ function Phase:main()
if type(useResult) == "table" then if type(useResult) == "table" then
room:useCard(useResult) room:useCard(useResult)
end end
if player._play_phase_end then
player._play_phase_end = false
break
end
end end
end, end,
[Player.Discard] = function() [Player.Discard] = function()
if player._phase_end then return end
local discardNum = #table.filter( local discardNum = #table.filter(
player:getCardIds(Player.Hand), function(id) player:getCardIds(Player.Hand), function(id)
local card = Fk:getCardById(id) local card = Fk:getCardById(id)
@ -408,6 +386,7 @@ function Phase:clear()
room:setPlayerMark(p, name, 0) room:setPlayerMark(p, name, 0)
end end
end end
p._phase_end = false
end end
for cid, cmark in pairs(room.card_marks) do for cid, cmark in pairs(room.card_marks) do

View File

@ -1,32 +1,43 @@
-- SPDX-License-Identifier: GPL-3.0-or-later -- SPDX-License-Identifier: GPL-3.0-or-later
local damage_nature_table = { ---@class HpEventWrappers: Object
[fk.NormalDamage] = "normal_damage", local HpEventWrappers = {} -- mixin
[fk.FireDamage] = "fire_damage",
[fk.ThunderDamage] = "thunder_damage", ---@return boolean
[fk.IceDamage] = "ice_damage", local function exec(tp, ...)
} local event = tp:create(...)
local _, ret = event:exec()
return ret
end
-- local damage_nature_table = {
-- [fk.NormalDamage] = "normal_damage",
-- [fk.FireDamage] = "fire_damage",
-- [fk.ThunderDamage] = "thunder_damage",
-- [fk.IceDamage] = "ice_damage",
-- }
local function sendDamageLog(room, damageStruct) local function sendDamageLog(room, damageStruct)
local damageName = Fk:getDamageNatureName(damageStruct.damageType)
if damageStruct.from then if damageStruct.from then
room:sendLog{ room:sendLog{
type = "#Damage", type = "#Damage",
to = {damageStruct.from.id}, to = {damageStruct.from.id},
from = damageStruct.to.id, from = damageStruct.to.id,
arg = damageStruct.damage, arg = damageStruct.damage,
arg2 = damage_nature_table[damageStruct.damageType], arg2 = damageName,
} }
else else
room:sendLog{ room:sendLog{
type = "#DamageWithNoFrom", type = "#DamageWithNoFrom",
from = damageStruct.to.id, from = damageStruct.to.id,
arg = damageStruct.damage, arg = damageStruct.damage,
arg2 = damage_nature_table[damageStruct.damageType], arg2 = damageName,
} }
end end
room:sendLogEvent("Damage", { room:sendLogEvent("Damage", {
to = damageStruct.to.id, to = damageStruct.to.id,
damageType = damage_nature_table[damageStruct.damageType], damageType = damageName,
damageNum = damageStruct.damage, damageNum = damageStruct.damage,
}) })
end end
@ -51,6 +62,17 @@ function ChangeHp:main()
} }
if reason == "damage" then if reason == "damage" then
if damageStruct then
if Fk:canChain(damageStruct.damageType) and damageStruct.to.chained then
damageStruct.to:setChainState(false)
if not damageStruct.chain then
damageStruct.beginnerOfTheDamage = true
damageStruct.chain_table = table.filter(room:getOtherPlayers(damageStruct.to), function(p)
return p.chained
end)
end
end
end
data.shield_lost = math.min(-num, player.shield) data.shield_lost = math.min(-num, player.shield)
data.num = num + data.shield_lost data.num = num + data.shield_lost
end end
@ -114,6 +136,17 @@ function ChangeHp:main()
return true return true
end end
--- 改变一名玩家的体力。
---@param player ServerPlayer @ 玩家
---@param num integer @ 变化量
---@param reason? string @ 原因
---@param skillName? string @ 技能名
---@param damageStruct? DamageStruct @ 伤害数据
---@return boolean
function HpEventWrappers:changeHp(player, num, reason, skillName, damageStruct)
return exec(ChangeHp, player, num, reason, skillName, damageStruct)
end
---@class GameEvent.Damage : GameEvent ---@class GameEvent.Damage : GameEvent
local Damage = GameEvent:subclass("GameEvent.Damage") local Damage = GameEvent:subclass("GameEvent.Damage")
function Damage:main() function Damage:main()
@ -178,11 +211,6 @@ function Damage:main()
end end
end end
if damageStruct.damageType ~= fk.NormalDamage and damageStruct.to.chained then
if not damageStruct.chain then damageStruct.beginnerOfTheDamage = true end
damageStruct.to:setChainState(false)
end
if not room:changeHp( if not room:changeHp(
damageStruct.to, damageStruct.to,
-damageStruct.damage, -damageStruct.damage,
@ -213,11 +241,11 @@ function Damage:exit()
logic:trigger(fk.DamageFinished, damageStruct.to, damageStruct) logic:trigger(fk.DamageFinished, damageStruct.to, damageStruct)
if damageStruct.beginnerOfTheDamage and not damageStruct.chain then if damageStruct.chain_table and #damageStruct.chain_table > 0 then
local targets = table.filter(room:getOtherPlayers(damageStruct.to), function(p) damageStruct.chain_table = table.filter(damageStruct.chain_table, function(p)
return p.chained return p:isAlive() and p.chained
end) end)
for _, p in ipairs(targets) do for _, p in ipairs(damageStruct.chain_table) do
room:sendLog{ room:sendLog{
type = "#ChainDamage", type = "#ChainDamage",
from = p.id from = p.id
@ -238,6 +266,13 @@ function Damage:exit()
end end
end end
--- 根据伤害数据造成伤害。
---@param damageStruct DamageStruct
---@return boolean
function HpEventWrappers:damage(damageStruct)
return exec(Damage, damageStruct)
end
---@class GameEvent.LoseHp : GameEvent ---@class GameEvent.LoseHp : GameEvent
local LoseHp = GameEvent:subclass("GameEvent.LoseHp") local LoseHp = GameEvent:subclass("GameEvent.LoseHp")
function LoseHp:main() function LoseHp:main()
@ -268,6 +303,15 @@ function LoseHp:main()
return true return true
end end
--- 令一名玩家失去体力。
---@param player ServerPlayer @ 玩家
---@param num integer @ 失去的数量
---@param skillName? string @ 技能名
---@return boolean
function HpEventWrappers:loseHp(player, num, skillName)
return exec(LoseHp, player, num, skillName)
end
---@class GameEvent.Recover : GameEvent ---@class GameEvent.Recover : GameEvent
local Recover = GameEvent:subclass("GameEvent.Recover") local Recover = GameEvent:subclass("GameEvent.Recover")
function Recover:prepare() function Recover:prepare()
@ -316,6 +360,13 @@ function Recover:main()
return true return true
end end
--- 根据回复数据回复体力。
---@param recoverStruct RecoverStruct
---@return boolean
function HpEventWrappers:recover(recoverStruct)
return exec(Recover, recoverStruct)
end
---@class GameEvent.ChangeMaxHp : GameEvent ---@class GameEvent.ChangeMaxHp : GameEvent
local ChangeMaxHp = GameEvent:subclass("GameEvent.ChangeMaxHp") local ChangeMaxHp = GameEvent:subclass("GameEvent.ChangeMaxHp")
function ChangeMaxHp:main() function ChangeMaxHp:main()
@ -374,4 +425,12 @@ function ChangeMaxHp:main()
return true return true
end end
return { ChangeHp, Damage, LoseHp, Recover, ChangeMaxHp } --- 改变一名玩家的体力上限。
---@param player ServerPlayer @ 玩家
---@param num integer @ 变化量
---@return boolean
function HpEventWrappers:changeMaxHp(player, num)
return exec(ChangeMaxHp, player, num)
end
return { ChangeHp, Damage, LoseHp, Recover, ChangeMaxHp, HpEventWrappers }

View File

@ -1,3 +1,4 @@
---@diagnostic disable
-- SPDX-License-Identifier: GPL-3.0-or-later -- SPDX-License-Identifier: GPL-3.0-or-later
-- Definitions of game events -- Definitions of game events
@ -5,11 +6,16 @@
-- 某类事件对应的结束事件其id刚好就是那个事件的相反数 -- 某类事件对应的结束事件其id刚好就是那个事件的相反数
-- GameEvent.EventFinish = -1 -- GameEvent.EventFinish = -1
--- 给Room.lua瘦身一系列GameEvent的封装
---@class GameEventWrappers: MiscEventWrappers, HpEventWrappers, DeathEventWrappers, MoveEventWrappers, UseCardEventWrappers, SkillEventWrappers, JudgeEventWrappers, PindianEventWrappers
local GameEventWrappers = {} -- mixin
local tmp local tmp
tmp = require "server.events.misc" tmp = require "server.events.misc"
GameEvent.Game = tmp[1] GameEvent.Game = tmp[1]
GameEvent.ChangeProperty = tmp[2] GameEvent.ChangeProperty = tmp[2]
GameEvent.ClearEvent = tmp[3] GameEvent.ClearEvent = tmp[3]
table.assign(GameEventWrappers, tmp[4])
tmp = require "server.events.hp" tmp = require "server.events.hp"
GameEvent.ChangeHp = tmp[1] GameEvent.ChangeHp = tmp[1]
@ -17,25 +23,31 @@ GameEvent.Damage = tmp[2]
GameEvent.LoseHp = tmp[3] GameEvent.LoseHp = tmp[3]
GameEvent.Recover = tmp[4] GameEvent.Recover = tmp[4]
GameEvent.ChangeMaxHp = tmp[5] GameEvent.ChangeMaxHp = tmp[5]
table.assign(GameEventWrappers, tmp[6])
tmp = require "server.events.death" tmp = require "server.events.death"
GameEvent.Dying = tmp[1] GameEvent.Dying = tmp[1]
GameEvent.Death = tmp[2] GameEvent.Death = tmp[2]
GameEvent.Revive = tmp[3] GameEvent.Revive = tmp[3]
table.assign(GameEventWrappers, tmp[4])
tmp = require "server.events.movecard" tmp = require "server.events.movecard"
GameEvent.MoveCards = tmp GameEvent.MoveCards = tmp[1]
table.assign(GameEventWrappers, tmp[2])
tmp = require "server.events.usecard" tmp = require "server.events.usecard"
GameEvent.UseCard = tmp[1] GameEvent.UseCard = tmp[1]
GameEvent.RespondCard = tmp[2] GameEvent.RespondCard = tmp[2]
GameEvent.CardEffect = tmp[3] GameEvent.CardEffect = tmp[3]
table.assign(GameEventWrappers, tmp[4])
tmp = require "server.events.skill" tmp = require "server.events.skill"
GameEvent.SkillEffect = tmp GameEvent.SkillEffect = tmp[1]
table.assign(GameEventWrappers, tmp[2])
tmp = require "server.events.judge" tmp = require "server.events.judge"
GameEvent.Judge = tmp GameEvent.Judge = tmp[1]
table.assign(GameEventWrappers, tmp[2])
tmp = require "server.events.gameflow" tmp = require "server.events.gameflow"
GameEvent.DrawInitial = tmp[1] GameEvent.DrawInitial = tmp[1]
@ -44,7 +56,8 @@ GameEvent.Turn = tmp[3]
GameEvent.Phase = tmp[4] GameEvent.Phase = tmp[4]
tmp = require "server.events.pindian" tmp = require "server.events.pindian"
GameEvent.Pindian = tmp GameEvent.Pindian = tmp[1]
table.assign(GameEventWrappers, tmp[2])
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
@ -53,3 +66,5 @@ 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
return GameEventWrappers

View File

@ -1,5 +1,15 @@
-- SPDX-License-Identifier: GPL-3.0-or-later -- SPDX-License-Identifier: GPL-3.0-or-later
---@class JudgeEventWrappers: Object
local JudgeEventWrappers = {} -- mixin
---@return boolean
local function exec(tp, ...)
local event = tp:create(...)
local _, ret = event:exec()
return ret
end
---@class GameEvent.Judge : GameEvent ---@class GameEvent.Judge : GameEvent
local Judge = GameEvent:subclass("GameEvent.Judge") local Judge = GameEvent:subclass("GameEvent.Judge")
function Judge:main() function Judge:main()
@ -74,4 +84,66 @@ function Judge:clear()
}) })
end end
return Judge -- 判定
--- 根据判定数据进行判定。判定的结果直接保存在这个数据中。
---@param data JudgeStruct
function JudgeEventWrappers:judge(data)
return exec(Judge, data)
end
--- 改判。
---@param card Card @ 改判的牌
---@param player ServerPlayer @ 改判的玩家
---@param judge JudgeStruct @ 要被改判的判定数据
---@param skillName? string @ 技能名
---@param exchange? boolean @ 是否要替换原有判定牌(即类似鬼道那样)
function JudgeEventWrappers:retrial(card, player, judge, skillName, exchange)
if not card then return end
local triggerResponded = self.owner_map[card:getEffectiveId()] == player
local isHandcard = (triggerResponded and self:getCardArea(card:getEffectiveId()) == Card.PlayerHand)
if triggerResponded then
local resp = {} ---@type CardResponseEvent
resp.from = player.id
resp.card = card
resp.skipDrop = true
self:responseCard(resp)
else
local move1 = {} ---@type CardsMoveInfo
move1.ids = { card:getEffectiveId() }
move1.from = player.id
move1.toArea = Card.Processing
move1.moveReason = fk.ReasonJustMove
move1.skillName = skillName
self:moveCards(move1)
end
local oldJudge = judge.card
judge.card = card
local rebyre = judge.retrial_by_response
judge.retrial_by_response = player
self:sendLog{
type = "#ChangedJudge",
from = player.id,
to = { judge.who.id },
arg2 = card:toLogString(),
arg = skillName,
}
Fk:filterCard(judge.card.id, judge.who, judge)
exchange = exchange and not player.dead
local move2 = {} ---@type CardsMoveInfo
move2.ids = { oldJudge:getEffectiveId() }
move2.toArea = exchange and Card.PlayerHand or Card.DiscardPile
move2.moveReason = exchange and fk.ReasonJustMove or fk.ReasonJudge
move2.to = exchange and player.id or nil
move2.skillName = skillName
self:moveCards(move2)
end
return { Judge, JudgeEventWrappers }

View File

@ -1,5 +1,8 @@
-- SPDX-License-Identifier: GPL-3.0-or-later -- SPDX-License-Identifier: GPL-3.0-or-later
---@class MiscEventWrappers: Object
local MiscEventWrappers = {} -- mixin
---@class GameEvent.Game : GameEvent ---@class GameEvent.Game : GameEvent
local Game = GameEvent:subclass("GameEvent.Game") local Game = GameEvent:subclass("GameEvent.Game")
function Game:main() function Game:main()
@ -129,6 +132,63 @@ function ChangeProperty:main()
logic:trigger(fk.AfterPropertyChange, player, data) logic:trigger(fk.AfterPropertyChange, player, data)
end end
---@param player ServerPlayer @ 要换将的玩家
---@param new_general string @ 要变更的武将,若不存在则变身为孙策,孙策不存在变身为士兵
---@param full? boolean @ 是否血量满状态变身
---@param isDeputy? boolean @ 是否变的是副将
---@param sendLog? boolean @ 是否发Log
---@param maxHpChange? boolean @ 是否改变体力上限,默认改变
function MiscEventWrappers:changeHero(player, new_general, full, isDeputy, sendLog, maxHpChange, kingdomChange)
local new = Fk.generals[new_general] or Fk.generals["sunce"] or Fk.generals["blank_shibing"]
kingdomChange = (kingdomChange == nil) and true or kingdomChange
local kingdom = (isDeputy or not kingdomChange) and player.kingdom or new.kingdom
if not isDeputy and kingdomChange then
local allKingdoms = {}
if new.subkingdom then
allKingdoms = { new.kingdom, new.subkingdom }
else
allKingdoms = Fk:getKingdomMap(new.kingdom)
end
if #allKingdoms > 0 then
kingdom = self:askForChoice(player, allKingdoms, "AskForKingdom", "#ChooseInitialKingdom")
end
end
ChangeProperty:create({
from = player,
general = not isDeputy and new_general or nil,
deputyGeneral = isDeputy and new_general or nil,
gender = isDeputy and player.gender or new.gender,
kingdom = kingdom,
sendLog = sendLog,
results = {},
}):exec()
maxHpChange = (maxHpChange == nil) and true or maxHpChange
if maxHpChange then
self:setPlayerProperty(player, "maxHp", player:getGeneralMaxHp())
end
if full or player.hp > player.maxHp then
self:setPlayerProperty(player, "hp", player.maxHp)
end
end
---@param player ServerPlayer @ 要变更势力的玩家
---@param kingdom string @ 要变更的势力
---@param sendLog? boolean @ 是否发Log
function MiscEventWrappers:changeKingdom(player, kingdom, sendLog)
if kingdom == player.kingdom then return end
sendLog = sendLog or false
ChangeProperty:create({
from = player,
kingdom = kingdom,
sendLog = sendLog,
results = {},
}):exec()
end
---@class GameEvent.ClearEvent : GameEvent ---@class GameEvent.ClearEvent : GameEvent
local ClearEvent = GameEvent:subclass("GameEvent.ClearEvent") local ClearEvent = GameEvent:subclass("GameEvent.ClearEvent")
function ClearEvent:main() function ClearEvent:main()
@ -154,4 +214,4 @@ function ClearEvent:main()
logic.cleaner_stack:pop() logic.cleaner_stack:pop()
end end
return { Game, ChangeProperty, ClearEvent } return { Game, ChangeProperty, ClearEvent, MiscEventWrappers }

View File

@ -1,5 +1,15 @@
-- SPDX-License-Identifier: GPL-3.0-or-later -- SPDX-License-Identifier: GPL-3.0-or-later
---@class MoveEventWrappers: Object
local MoveEventWrappers = {} -- mixin
---@return boolean
local function exec(tp, ...)
local event = tp:create(...)
local _, ret = event:exec()
return ret
end
---@class GameEvent.MoveCards : GameEvent ---@class GameEvent.MoveCards : GameEvent
local MoveCards = GameEvent:subclass("GameEvent.MoveCards") local MoveCards = GameEvent:subclass("GameEvent.MoveCards")
function MoveCards:main() function MoveCards:main()
@ -102,58 +112,7 @@ function MoveCards:main()
---@param info MoveInfo ---@param info MoveInfo
for _, info in ipairs(data.moveInfo) do for _, info in ipairs(data.moveInfo) do
local realFromArea = room:getCardArea(info.cardId) room:applyMoveInfo(data, info)
local playerAreas = { Player.Hand, Player.Equip, Player.Judge, Player.Special }
if table.contains(playerAreas, realFromArea) and data.from then
local from = room:getPlayerById(data.from)
from:removeCards(realFromArea, { info.cardId }, info.fromSpecialName)
elseif realFromArea ~= Card.Unknown then
local fromAreaIds = {}
if realFromArea == Card.Processing then
fromAreaIds = room.processing_area
elseif realFromArea == Card.DrawPile then
fromAreaIds = room.draw_pile
elseif realFromArea == Card.DiscardPile then
fromAreaIds = room.discard_pile
elseif realFromArea == Card.Void then
fromAreaIds = room.void
end
table.removeOne(fromAreaIds, info.cardId)
end
if table.contains(playerAreas, data.toArea) and data.to then
local to = room:getPlayerById(data.to)
to:addCards(data.toArea, { info.cardId }, data.specialName)
else
local toAreaIds = {}
if data.toArea == Card.Processing then
toAreaIds = room.processing_area
elseif data.toArea == Card.DrawPile then
toAreaIds = room.draw_pile
elseif data.toArea == Card.DiscardPile then
toAreaIds = room.discard_pile
elseif data.toArea == Card.Void then
toAreaIds = room.void
end
if data.toArea == Card.DrawPile then
local putIndex = data.drawPilePosition or 1
if putIndex == -1 then
putIndex = #room.draw_pile + 1
elseif putIndex < 1 or putIndex > #room.draw_pile + 1 then
putIndex = 1
end
table.insert(toAreaIds, putIndex, info.cardId)
else
table.insert(toAreaIds, info.cardId)
end
end
room:setCardArea(info.cardId, data.toArea, data.to)
if data.toArea == Card.DrawPile or realFromArea == Card.DrawPile then if data.toArea == Card.DrawPile or realFromArea == Card.DrawPile then
room:doBroadcastNotify("UpdateDrawPile", #room.draw_pile) room:doBroadcastNotify("UpdateDrawPile", #room.draw_pile)
end end
@ -212,4 +171,259 @@ function MoveCards:main()
return true return true
end end
return MoveCards --- 传入一系列移牌信息,去实际移动这些牌
---@vararg CardsMoveInfo
---@return boolean?
function MoveEventWrappers:moveCards(...)
return exec(MoveCards, ...)
end
--- 让一名玩家获得一张牌
---@param player integer|ServerPlayer @ 要拿牌的玩家
---@param card integer|integer[]|Card|Card[] @ 要拿到的卡牌
---@param unhide? boolean @ 是否明着拿
---@param reason? CardMoveReason @ 卡牌移动的原因
---@param proposer? integer @ 移动操作者的id
---@param skill_name? string @ 技能名
---@param moveMark? table|string @ 移动后自动赋予标记,格式:{标记名(支持-inarea后缀移出值代表区域后清除), 值}
---@param visiblePlayers? integer|integer[] @ 控制移动对特定角色可见在moveVisible为false时生效
function MoveEventWrappers:obtainCard(player, card, unhide, reason, proposer, skill_name, moveMark, visiblePlayers)
local pid = type(player) == "number" and player or player.id
self:moveCardTo(card, Card.PlayerHand, player, reason, skill_name, nil, unhide, proposer or pid, moveMark, visiblePlayers)
end
--- 让玩家摸牌
---@param player ServerPlayer @ 摸牌的玩家
---@param num integer @ 摸牌数
---@param skillName? string @ 技能名
---@param fromPlace? string @ 摸牌的位置,"top" 或者 "bottom"
---@param moveMark? table|string @ 移动后自动赋予标记,格式:{标记名(支持-inarea后缀移出值代表区域后清除), 值}
---@return integer[] @ 摸到的牌
function MoveEventWrappers:drawCards(player, num, skillName, fromPlace, moveMark)
local drawData = {
who = player,
num = num,
skillName = skillName,
fromPlace = fromPlace,
}
if self.logic:trigger(fk.BeforeDrawCard, player, drawData) then
return {}
end
num = drawData.num
fromPlace = drawData.fromPlace
player = drawData.who
local topCards = self:getNCards(num, fromPlace)
self:moveCards({
ids = topCards,
to = player.id,
toArea = Card.PlayerHand,
moveReason = fk.ReasonDraw,
proposer = player.id,
skillName = skillName,
moveMark = moveMark,
})
return { table.unpack(topCards) }
end
--- 将一张或多张牌移动到某处
---@param card integer | integer[] | Card | Card[] @ 要移动的牌
---@param to_place integer @ 移动的目标位置
---@param target? ServerPlayer|integer @ 移动的目标角色
---@param reason? integer @ 移动时使用的移牌原因
---@param skill_name? string @ 技能名
---@param special_name? string @ 私人牌堆名
---@param visible? boolean @ 是否明置
---@param proposer? integer @ 移动操作者的id
---@param moveMark? table|string @ 移动后自动赋予标记,格式:{标记名(支持-inarea后缀移出值代表区域后清除), 值}
---@param visiblePlayers? integer|integer[] @ 控制移动对特定角色可见在moveVisible为false时生效
function MoveEventWrappers:moveCardTo(card, to_place, target, reason, skill_name, special_name, visible, proposer, moveMark, visiblePlayers)
reason = reason or fk.ReasonJustMove
skill_name = skill_name or ""
special_name = special_name or ""
local ids = Card:getIdList(card)
local to
if table.contains(
{Card.PlayerEquip, Card.PlayerHand,
Card.PlayerJudge, Card.PlayerSpecial}, to_place) then
assert(target)
if type(target) == "number" then
to = target
else
to = target.id
end
end
local movesSplitedByOwner = {}
for _, cardId in ipairs(ids) do
local moveFound = table.find(movesSplitedByOwner, function(move)
return move.from == self.owner_map[cardId]
end)
if moveFound then
table.insert(moveFound.ids, cardId)
else
table.insert(movesSplitedByOwner, {
ids = { cardId },
from = self.owner_map[cardId],
to = to,
toArea = to_place,
moveReason = reason,
skillName = skill_name,
specialName = special_name,
moveVisible = visible,
proposer = proposer,
moveMark = moveMark,
visiblePlayers = visiblePlayers,
})
end
end
self:moveCards(table.unpack(movesSplitedByOwner))
end
--- 弃置一名角色的牌。
---@param card_ids integer[]|integer|Card|Card[] @ 被弃掉的牌
---@param skillName? string @ 技能名
---@param who ServerPlayer @ 被弃牌的人
---@param thrower? ServerPlayer @ 弃别人牌的人
function MoveEventWrappers:throwCard(card_ids, skillName, who, thrower)
skillName = skillName or ""
thrower = thrower or who
self:moveCards({
ids = Card:getIdList(card_ids),
from = who.id,
toArea = Card.DiscardPile,
moveReason = fk.ReasonDiscard,
proposer = thrower.id,
skillName = skillName
})
end
--- 重铸一名角色的牌。
---@param card_ids integer[] @ 被重铸的牌
---@param who ServerPlayer @ 重铸的角色
---@param skillName? string @ 技能名,默认为“重铸”
---@return integer[] @ 摸到的牌
function MoveEventWrappers:recastCard(card_ids, who, skillName)
if type(card_ids) == "number" then
card_ids = {card_ids}
end
skillName = skillName or "recast"
self:moveCards({
ids = card_ids,
from = who.id,
toArea = Card.DiscardPile,
skillName = skillName,
moveReason = fk.ReasonRecast,
proposer = who.id
})
self:sendFootnote(card_ids, {
type = "##RecastCard",
from = who.id,
})
self:broadcastPlaySound("./audio/system/recast")
self:sendLog{
type = skillName == "recast" and "#Recast" or "#RecastBySkill",
from = who.id,
card = card_ids,
arg = skillName,
}
return self:drawCards(who, #card_ids, skillName)
end
--- 将一些卡牌同时分配给一些角色。
---@param room Room @ 房间
---@param list table<integer[]> @ 分配牌和角色的数据表键为角色id值为分配给其的牌id数组
---@param proposer? integer @ 操作者的id。默认为空
---@param skillName? string @ 技能名。默认为“分配”
---@return table<integer[]> @ 返回成功分配的卡牌
function MoveEventWrappers:doYiji(room, list, proposer, skillName)
skillName = skillName or "distribution_skill"
local moveInfos = {}
local move_ids = {}
for to, cards in pairs(list) do
local toP = room:getPlayerById(to)
local handcards = toP:getCardIds("h")
cards = table.filter(cards, function (id) return not table.contains(handcards, id) end)
if #cards > 0 then
table.insertTable(move_ids, cards)
local moveMap = {}
local noFrom = {}
for _, id in ipairs(cards) do
local from = room.owner_map[id]
if from then
moveMap[from] = moveMap[from] or {}
table.insert(moveMap[from], id)
else
table.insert(noFrom, id)
end
end
for from, _cards in pairs(moveMap) do
table.insert(moveInfos, {
ids = _cards,
moveInfo = table.map(_cards, function(id)
return {cardId = id, fromArea = room:getCardArea(id), fromSpecialName = room:getPlayerById(from):getPileNameOfId(id)}
end),
from = from,
to = to,
toArea = Card.PlayerHand,
moveReason = fk.ReasonGive,
proposer = proposer,
skillName = skillName,
})
end
if #noFrom > 0 then
table.insert(moveInfos, {
ids = noFrom,
to = to,
toArea = Card.PlayerHand,
moveReason = fk.ReasonGive,
proposer = proposer,
skillName = skillName,
})
end
end
end
if #moveInfos > 0 then
room:moveCards(table.unpack(moveInfos))
end
return move_ids
end
--- 将一张牌移动至某角色的装备区,若不合法则置入弃牌堆。目前没做相同副类别装备同时置入的适配(甘露神典韦)
---@param target ServerPlayer @ 接受牌的角色
---@param cards integer|integer[] @ 移动的牌
---@param skillName? string @ 技能名
---@param convert? boolean @ 是否可以替换装备(默认可以)
---@param proposer? ServerPlayer @ 操作者
function MoveEventWrappers:moveCardIntoEquip(target, cards, skillName, convert, proposer)
convert = (convert == nil) and true or convert
skillName = skillName or ""
cards = type(cards) == "table" and cards or {cards}
local moves = {}
for _, cardId in ipairs(cards) do
local card = Fk:getCardById(cardId)
local fromId = self.owner_map[cardId]
local proposerId = proposer and proposer.id or nil
if target:canMoveCardIntoEquip(cardId, convert) then
if target:hasEmptyEquipSlot(card.sub_type) then
table.insert(moves,{ids = {cardId}, from = fromId, to = target.id, toArea = Card.PlayerEquip, moveReason = fk.ReasonPut,skillName = skillName,proposer = proposerId})
else
local existingEquip = target:getEquipments(card.sub_type)
local throw = #existingEquip == 1 and existingEquip[1] or
self:askForCardChosen(proposer or target, target, {card_data = { {Util.convertSubtypeAndEquipSlot(card.sub_type),existingEquip} } }, "replaceEquip","#replaceEquip")
table.insert(moves,{ids = {throw}, from = target.id, toArea = Card.DiscardPile, moveReason = fk.ReasonPutIntoDiscardPile, skillName = skillName,proposer = proposerId})
table.insert(moves,{ids = {cardId}, from = fromId, to = target.id, toArea = Card.PlayerEquip, moveReason = fk.ReasonPut,skillName = skillName,proposer = proposerId})
end
else
table.insert(moves,{ids = {cardId}, from = fromId, toArea = Card.DiscardPile, moveReason = fk.ReasonPutIntoDiscardPile,skillName = skillName})
end
end
self:moveCards(table.unpack(moves))
end
return { MoveCards, MoveEventWrappers }

View File

@ -1,5 +1,15 @@
-- SPDX-License-Identifier: GPL-3.0-or-later -- SPDX-License-Identifier: GPL-3.0-or-later
---@class PindianEventWrappers: Object
local PindianEventWrappers = {} -- mixin
---@return boolean
local function exec(tp, ...)
local event = tp:create(...)
local _, ret = event:exec()
return ret
end
---@class GameEvent.Pindian : GameEvent ---@class GameEvent.Pindian : GameEvent
local Pindian = GameEvent:subclass("GameEvent.Pindian") local Pindian = GameEvent:subclass("GameEvent.Pindian")
function Pindian:main() function Pindian:main()
@ -190,4 +200,11 @@ function Pindian:clear()
if not self.interrupted then return end if not self.interrupted then return end
end end
return Pindian
--- 根据拼点信息开始拼点。
---@param pindianData PindianStruct
function PindianEventWrappers:pindian(pindianData)
return exec(Pindian, pindianData)
end
return { Pindian, PindianEventWrappers }

View File

@ -1,25 +1,187 @@
-- SPDX-License-Identifier: GPL-3.0-or-later -- SPDX-License-Identifier: GPL-3.0-or-later
---@class SkillEventWrappers: Object
local SkillEventWrappers = {} -- mixin
---@return boolean
local function exec(tp, ...)
local event = tp:create(...)
local _, ret = event:exec()
return ret
end
---@class GameEvent.SkillEffect : GameEvent ---@class GameEvent.SkillEffect : GameEvent
local SkillEffect = GameEvent:subclass("GameEvent.SkillEffect") local SkillEffect = GameEvent:subclass("GameEvent.SkillEffect")
function SkillEffect:main() function SkillEffect:main()
local effect_cb, player, _skill = table.unpack(self.data) local effect_cb, player, skill, skill_data = table.unpack(self.data)
local room = self.room local room = self.room
local logic = room.logic local logic = room.logic
local skill = _skill.main_skill and _skill.main_skill or _skill local main_skill = skill.main_skill and skill.main_skill or skill
skill_data = skill_data or Util.DummyTable
local cost_data = skill_data.cost_data or Util.DummyTable
if player then if player and not skill.cardSkill then
player:addSkillUseHistory(skill.name) player:revealBySkillName(main_skill.name)
local tos = skill_data.tos or {}
local mute, no_indicate = skill.mute, skill.no_indicate
if type(cost_data) == "table" then
if cost_data.mute then mute = cost_data.mute end
if cost_data.no_indicate then no_indicate = cost_data.no_indicate end
end
if not mute then
if skill.attached_equip then
local equip = Fk.all_card_types[skill.attached_equip]
if equip then
local pkgPath = "./packages/" .. equip.package.extensionName
local soundName = pkgPath .. "/audio/card/" .. equip.name
room:broadcastPlaySound(soundName)
if not no_indicate and #tos > 0 then
room:sendLog{
type = "#InvokeSkillTo",
from = player.id,
arg = skill.name,
to = tos,
}
else
room:sendLog{
type = "#InvokeSkill",
from = player.id,
arg = skill.name,
}
end
room:setEmotion(player, pkgPath .. "/image/anim/" .. equip.name)
end
else
player:broadcastSkillInvoke(skill.name)
room:notifySkillInvoked(player, skill.name, nil, no_indicate and {} or tos)
end
end
if not no_indicate then
room:doIndicate(player.id, tos)
end
if skill:isSwitchSkill() then
local switchSkillName = skill.switchSkillName
room:setPlayerMark(
player,
MarkEnum.SwithSkillPreName .. switchSkillName,
player:getSwitchSkillState(switchSkillName, true)
)
end
player:addSkillUseHistory(main_skill.name)
end end
local cost_data_bak = skill.cost_data local cost_data_bak = skill.cost_data
logic:trigger(fk.SkillEffect, player, skill) logic:trigger(fk.SkillEffect, player, main_skill)
skill.cost_data = cost_data_bak skill.cost_data = cost_data_bak
local ret = effect_cb() local ret = effect_cb and effect_cb() or false
logic:trigger(fk.AfterSkillEffect, player, main_skill)
logic:trigger(fk.AfterSkillEffect, player, skill)
return ret return ret
end end
return SkillEffect --- 使用技能。先增加技能发动次数,再执行相应的函数。
---@param player ServerPlayer @ 发动技能的玩家
---@param skill Skill @ 发动的技能
---@param effect_cb fun() @ 实际要调用的函数
---@param skill_data? table @ 技能的信息
function SkillEventWrappers:useSkill(player, skill, effect_cb, skill_data)
return exec(SkillEffect, effect_cb, player, skill, skill_data or Util.DummyTable)
end
--- 令一名玩家获得/失去技能。
---
--- skill_names 是字符串数组或者用管道符号(|)分割的字符串。
---
--- 每个skill_name都是要获得的技能的名。如果在skill_name前面加上"-",那就是失去技能。
---@param player ServerPlayer @ 玩家
---@param skill_names string[] | string @ 要获得/失去的技能
---@param source_skill? string | Skill @ 源技能
---@param no_trigger? boolean @ 是否不触发相关时机
function SkillEventWrappers:handleAddLoseSkills(player, skill_names, source_skill, sendlog, no_trigger)
if type(skill_names) == "string" then
skill_names = skill_names:split("|")
end
if sendlog == nil then sendlog = true end
if #skill_names == 0 then return end
local losts = {} ---@type boolean[]
local triggers = {} ---@type Skill[]
local lost_piles = {} ---@type integer[]
for _, skill in ipairs(skill_names) do
if string.sub(skill, 1, 1) == "-" then
local actual_skill = string.sub(skill, 2, #skill)
if player:hasSkill(actual_skill, true, true) then
local lost_skills = player:loseSkill(actual_skill, source_skill)
for _, s in ipairs(lost_skills) do
self:doBroadcastNotify("LoseSkill", json.encode{
player.id,
s.name
})
if sendlog and s.visible then
self:sendLog{
type = "#LoseSkill",
from = player.id,
arg = s.name
}
end
table.insert(losts, true)
table.insert(triggers, s)
if s.derived_piles then
for _, pile_name in ipairs(s.derived_piles) do
table.insertTableIfNeed(lost_piles, player:getPile(pile_name))
end
end
end
end
else
local sk = Fk.skills[skill]
if sk and not player:hasSkill(sk, true, true) then
local got_skills = player:addSkill(sk, source_skill)
for _, s in ipairs(got_skills) do
-- TODO: limit skill mark
self:doBroadcastNotify("AddSkill", json.encode{
player.id,
s.name
})
if sendlog and s.visible then
self:sendLog{
type = "#AcquireSkill",
from = player.id,
arg = s.name
}
end
table.insert(losts, false)
table.insert(triggers, s)
end
end
end
end
if (not no_trigger) and #triggers > 0 then
for i = 1, #triggers do
local event = losts[i] and fk.EventLoseSkill or fk.EventAcquireSkill
self.logic:trigger(event, player, triggers[i])
end
end
if #lost_piles > 0 then
self:moveCards({
ids = lost_piles,
from = player.id,
toArea = Card.DiscardPile,
moveReason = fk.ReasonPutIntoDiscardPile,
})
end
end
return { SkillEffect, SkillEventWrappers }

View File

@ -1,5 +1,15 @@
-- SPDX-License-Identifier: GPL-3.0-or-later -- SPDX-License-Identifier: GPL-3.0-or-later
---@class UseCardEventWrappers: Object
local UseCardEventWrappers = {} -- mixin
---@return boolean
local function exec(tp, ...)
local event = tp:create(...)
local _, ret = event:exec()
return ret
end
local playCardEmotionAndSound = function(room, player, card) local playCardEmotionAndSound = function(room, player, card)
if card.type ~= Card.TypeEquip then if card.type ~= Card.TypeEquip then
local anim_path = "./packages/" .. card.package.extensionName .. "/image/anim/" .. card.name local anim_path = "./packages/" .. card.package.extensionName .. "/image/anim/" .. card.name
@ -397,4 +407,512 @@ function CardEffect:main()
end end
end end
return { UseCard, RespondCard, CardEffect }
--- 根据卡牌使用数据,去实际使用这个卡牌。
---@param cardUseEvent CardUseStruct @ 使用数据
---@return boolean
function UseCardEventWrappers:useCard(cardUseEvent)
return exec(UseCard, cardUseEvent)
end
---@param room Room
---@param cardUseEvent CardUseStruct
---@param aimEventCollaborators table<string, AimStruct[]>
---@return boolean
local onAim = function(room, cardUseEvent, aimEventCollaborators)
local eventStages = { fk.TargetSpecifying, fk.TargetConfirming, fk.TargetSpecified, fk.TargetConfirmed }
for _, stage in ipairs(eventStages) do
if (not cardUseEvent.tos) or #cardUseEvent.tos == 0 then
return false
end
room:sortPlayersByAction(cardUseEvent.tos, true)
local aimGroup = AimGroup:initAimGroup(TargetGroup:getRealTargets(cardUseEvent.tos))
local collaboratorsIndex = {}
local firstTarget = true
repeat
local toId = AimGroup:getUndoneOrDoneTargets(aimGroup)[1]
---@type AimStruct
local aimStruct
local initialEvent = false
collaboratorsIndex[toId] = collaboratorsIndex[toId] or 1
if not aimEventCollaborators[toId] or collaboratorsIndex[toId] > #aimEventCollaborators[toId] then
aimStruct = {
from = cardUseEvent.from,
card = cardUseEvent.card,
to = toId,
targetGroup = cardUseEvent.tos,
nullifiedTargets = cardUseEvent.nullifiedTargets or {},
tos = aimGroup,
firstTarget = firstTarget,
additionalDamage = cardUseEvent.additionalDamage,
additionalRecover = cardUseEvent.additionalRecover,
additionalEffect = cardUseEvent.additionalEffect,
extra_data = cardUseEvent.extra_data,
}
local index = 1
for _, targets in ipairs(cardUseEvent.tos) do
if index > collaboratorsIndex[toId] then
break
end
if #targets > 1 then
for i = 2, #targets do
aimStruct.subTargets = {}
table.insert(aimStruct.subTargets, targets[i])
end
end
end
collaboratorsIndex[toId] = 1
initialEvent = true
else
aimStruct = aimEventCollaborators[toId][collaboratorsIndex[toId]]
aimStruct.from = cardUseEvent.from
aimStruct.card = cardUseEvent.card
aimStruct.tos = aimGroup
aimStruct.targetGroup = cardUseEvent.tos
aimStruct.nullifiedTargets = cardUseEvent.nullifiedTargets or {}
aimStruct.firstTarget = firstTarget
aimStruct.additionalEffect = cardUseEvent.additionalEffect
aimStruct.extra_data = cardUseEvent.extra_data
end
firstTarget = false
room.logic:trigger(stage, (stage == fk.TargetSpecifying or stage == fk.TargetSpecified) and room:getPlayerById(aimStruct.from) or room:getPlayerById(aimStruct.to), aimStruct)
AimGroup:removeDeadTargets(room, aimStruct)
local aimEventTargetGroup = aimStruct.targetGroup
if aimEventTargetGroup then
room:sortPlayersByAction(aimEventTargetGroup, true)
end
cardUseEvent.from = aimStruct.from
cardUseEvent.tos = aimEventTargetGroup
cardUseEvent.nullifiedTargets = aimStruct.nullifiedTargets
cardUseEvent.additionalEffect = aimStruct.additionalEffect
cardUseEvent.extra_data = aimStruct.extra_data
if #AimGroup:getAllTargets(aimStruct.tos) == 0 then
return false
end
local cancelledTargets = AimGroup:getCancelledTargets(aimStruct.tos)
if #cancelledTargets > 0 then
for _, target in ipairs(cancelledTargets) do
aimEventCollaborators[target] = {}
collaboratorsIndex[target] = 1
end
end
aimStruct.tos[AimGroup.Cancelled] = {}
aimEventCollaborators[toId] = aimEventCollaborators[toId] or {}
if room:getPlayerById(toId):isAlive() then
if initialEvent then
table.insert(aimEventCollaborators[toId], aimStruct)
else
aimEventCollaborators[toId][collaboratorsIndex[toId]] = aimStruct
end
collaboratorsIndex[toId] = collaboratorsIndex[toId] + 1
end
AimGroup:setTargetDone(aimStruct.tos, toId)
aimGroup = aimStruct.tos
until #AimGroup:getUndoneOrDoneTargets(aimGroup) == 0
end
return true
end
--- 对卡牌使用数据进行生效
---@param cardUseEvent CardUseStruct
function UseCardEventWrappers:doCardUseEffect(cardUseEvent)
---@type table<string, AimStruct>
local aimEventCollaborators = {}
if cardUseEvent.tos and not onAim(self, cardUseEvent, aimEventCollaborators) then
return
end
local realCardIds = self:getSubcardsByRule(cardUseEvent.card, { Card.Processing })
self.logic:trigger(fk.BeforeCardUseEffect, self:getPlayerById(cardUseEvent.from), cardUseEvent)
-- If using Equip or Delayed trick, move them to the area and return
if cardUseEvent.card.type == Card.TypeEquip then
if #realCardIds == 0 then
return
end
local target = TargetGroup:getRealTargets(cardUseEvent.tos)[1]
if not (self:getPlayerById(target).dead or table.contains((cardUseEvent.nullifiedTargets or Util.DummyTable), target)) then
local existingEquipId
if cardUseEvent.toPutSlot and cardUseEvent.toPutSlot:startsWith("#EquipmentChoice") then
local index = cardUseEvent.toPutSlot:split(":")[2]
existingEquipId = self:getPlayerById(target):getEquipments(cardUseEvent.card.sub_type)[tonumber(index)]
elseif not self:getPlayerById(target):hasEmptyEquipSlot(cardUseEvent.card.sub_type) then
existingEquipId = self:getPlayerById(target):getEquipment(cardUseEvent.card.sub_type)
end
if existingEquipId then
self:moveCards(
{
ids = { existingEquipId },
from = target,
toArea = Card.DiscardPile,
moveReason = fk.ReasonPutIntoDiscardPile,
},
{
ids = realCardIds,
to = target,
toArea = Card.PlayerEquip,
moveReason = fk.ReasonUse,
}
)
else
self:moveCards({
ids = realCardIds,
to = target,
toArea = Card.PlayerEquip,
moveReason = fk.ReasonUse,
})
end
end
return
elseif cardUseEvent.card.sub_type == Card.SubtypeDelayedTrick then
if #realCardIds == 0 then
return
end
local target = TargetGroup:getRealTargets(cardUseEvent.tos)[1]
if not (self:getPlayerById(target).dead or table.contains((cardUseEvent.nullifiedTargets or Util.DummyTable), target)) then
local findSameCard = false
for _, cardId in ipairs(self:getPlayerById(target):getCardIds(Player.Judge)) do
if Fk:getCardById(cardId).trueName == cardUseEvent.card.trueName then
findSameCard = true
end
end
if not findSameCard then
if cardUseEvent.card:isVirtual() then
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
self:moveCards({
ids = realCardIds,
to = target,
toArea = Card.PlayerJudge,
moveReason = fk.ReasonUse,
})
return
end
end
return
end
if not cardUseEvent.card.skill then
return
end
-- If using card to other card (like jink or nullification), simply effect and return
if cardUseEvent.toCard ~= nil then
---@class CardEffectEvent
local cardEffectEvent = {
from = cardUseEvent.from,
tos = cardUseEvent.tos,
card = cardUseEvent.card,
toCard = cardUseEvent.toCard,
responseToEvent = cardUseEvent.responseToEvent,
nullifiedTargets = cardUseEvent.nullifiedTargets,
disresponsiveList = cardUseEvent.disresponsiveList,
unoffsetableList = cardUseEvent.unoffsetableList,
additionalDamage = cardUseEvent.additionalDamage,
additionalRecover = cardUseEvent.additionalRecover,
cardsResponded = cardUseEvent.cardsResponded,
prohibitedCardNames = cardUseEvent.prohibitedCardNames,
extra_data = cardUseEvent.extra_data,
}
self:doCardEffect(cardEffectEvent)
if cardEffectEvent.cardsResponded then
cardUseEvent.cardsResponded = cardUseEvent.cardsResponded or {}
for _, card in ipairs(cardEffectEvent.cardsResponded) do
table.insertIfNeed(cardUseEvent.cardsResponded, card)
end
end
return
end
local i = 0
while i < (cardUseEvent.additionalEffect or 0) + 1 do
if #TargetGroup:getRealTargets(cardUseEvent.tos) > 0 and cardUseEvent.card.skill.onAction then
cardUseEvent.card.skill:onAction(self, cardUseEvent)
end
-- Else: do effect to all targets
local collaboratorsIndex = {}
for _, toId in ipairs(TargetGroup:getRealTargets(cardUseEvent.tos)) do
if not table.contains(cardUseEvent.nullifiedTargets, toId) and self:getPlayerById(toId):isAlive() then
---@class CardEffectEvent
local cardEffectEvent = {
from = cardUseEvent.from,
tos = cardUseEvent.tos,
card = cardUseEvent.card,
toCard = cardUseEvent.toCard,
responseToEvent = cardUseEvent.responseToEvent,
nullifiedTargets = cardUseEvent.nullifiedTargets,
disresponsiveList = cardUseEvent.disresponsiveList,
unoffsetableList = cardUseEvent.unoffsetableList,
additionalDamage = cardUseEvent.additionalDamage,
additionalRecover = cardUseEvent.additionalRecover,
cardsResponded = cardUseEvent.cardsResponded,
prohibitedCardNames = cardUseEvent.prohibitedCardNames,
extra_data = cardUseEvent.extra_data,
}
if aimEventCollaborators[toId] then
cardEffectEvent.to = toId
collaboratorsIndex[toId] = collaboratorsIndex[toId] or 1
local curAimEvent = aimEventCollaborators[toId][collaboratorsIndex[toId]]
cardEffectEvent.subTargets = curAimEvent.subTargets
cardEffectEvent.additionalDamage = curAimEvent.additionalDamage
cardEffectEvent.additionalRecover = curAimEvent.additionalRecover
if curAimEvent.disresponsiveList then
cardEffectEvent.disresponsiveList = cardEffectEvent.disresponsiveList or {}
for _, disresponsivePlayer in ipairs(curAimEvent.disresponsiveList) do
if not table.contains(cardEffectEvent.disresponsiveList, disresponsivePlayer) then
table.insert(cardEffectEvent.disresponsiveList, disresponsivePlayer)
end
end
end
if curAimEvent.unoffsetableList then
cardEffectEvent.unoffsetableList = cardEffectEvent.unoffsetableList or {}
for _, unoffsetablePlayer in ipairs(curAimEvent.unoffsetableList) do
if not table.contains(cardEffectEvent.unoffsetableList, unoffsetablePlayer) then
table.insert(cardEffectEvent.unoffsetableList, unoffsetablePlayer)
end
end
end
cardEffectEvent.disresponsive = curAimEvent.disresponsive
cardEffectEvent.unoffsetable = curAimEvent.unoffsetable
cardEffectEvent.fixedResponseTimes = curAimEvent.fixedResponseTimes
cardEffectEvent.fixedAddTimesResponsors = curAimEvent.fixedAddTimesResponsors
collaboratorsIndex[toId] = collaboratorsIndex[toId] + 1
local curCardEffectEvent = table.simpleClone(cardEffectEvent)
self:doCardEffect(curCardEffectEvent)
if curCardEffectEvent.cardsResponded then
cardUseEvent.cardsResponded = cardUseEvent.cardsResponded or {}
for _, card in ipairs(curCardEffectEvent.cardsResponded) do
table.insertIfNeed(cardUseEvent.cardsResponded, card)
end
end
if type(curCardEffectEvent.nullifiedTargets) == 'table' then
table.insertTableIfNeed(cardUseEvent.nullifiedTargets, curCardEffectEvent.nullifiedTargets)
end
end
end
end
if #TargetGroup:getRealTargets(cardUseEvent.tos) > 0 and cardUseEvent.card.skill.onAction then
cardUseEvent.card.skill:onAction(self, cardUseEvent, true)
end
i = i + 1
end
end
--- 对卡牌效果数据进行生效
---@param cardEffectEvent CardEffectEvent
function UseCardEventWrappers:doCardEffect(cardEffectEvent)
return exec(CardEffect, cardEffectEvent)
end
---@param cardEffectEvent CardEffectEvent
function UseCardEventWrappers:handleCardEffect(event, cardEffectEvent)
if event == fk.PreCardEffect then
if cardEffectEvent.card.skill:aboutToEffect(self, cardEffectEvent) then return end
if
cardEffectEvent.card.trueName == "slash" and
not (cardEffectEvent.unoffsetable or table.contains(cardEffectEvent.unoffsetableList or Util.DummyTable, cardEffectEvent.to))
then
local loopTimes = 1
if cardEffectEvent.fixedResponseTimes then
if type(cardEffectEvent.fixedResponseTimes) == "table" then
loopTimes = cardEffectEvent.fixedResponseTimes["jink"] or 1
elseif type(cardEffectEvent.fixedResponseTimes) == "number" then
loopTimes = cardEffectEvent.fixedResponseTimes
end
end
Fk.currentResponsePattern = "jink"
for i = 1, loopTimes do
local to = self:getPlayerById(cardEffectEvent.to)
local prompt = ""
if cardEffectEvent.from then
if loopTimes == 1 then
prompt = "#slash-jink:" .. cardEffectEvent.from
else
prompt = "#slash-jink-multi:" .. cardEffectEvent.from .. "::" .. i .. ":" .. loopTimes
end
end
local use = self:askForUseCard(
to,
"jink",
nil,
prompt,
true,
nil,
cardEffectEvent
)
if use then
use.toCard = cardEffectEvent.card
use.responseToEvent = cardEffectEvent
self:useCard(use)
end
if not cardEffectEvent.isCancellOut then
break
end
cardEffectEvent.isCancellOut = i == loopTimes
end
elseif
cardEffectEvent.card.type == Card.TypeTrick and
not (cardEffectEvent.disresponsive or cardEffectEvent.unoffsetable) and
not table.contains(cardEffectEvent.prohibitedCardNames or Util.DummyTable, "nullification")
then
local players = {}
Fk.currentResponsePattern = "nullification"
local cardCloned = Fk:cloneCard("nullification")
for _, p in ipairs(self.alive_players) do
if not p:prohibitUse(cardCloned) then
local cards = p:getHandlyIds()
for _, cid in ipairs(cards) do
if
Fk:getCardById(cid).trueName == "nullification" and
not (
table.contains(cardEffectEvent.disresponsiveList or Util.DummyTable, p.id) or
table.contains(cardEffectEvent.unoffsetableList or Util.DummyTable, p.id)
)
then
table.insert(players, p)
break
end
end
if not table.contains(players, p) then
Self = p -- for enabledAtResponse
for _, s in ipairs(table.connect(p.player_skills, p._fake_skills)) do
if
s.pattern and
Exppattern:Parse("nullification"):matchExp(s.pattern) and
not (s.enabledAtResponse and not s:enabledAtResponse(p)) and
not (
table.contains(cardEffectEvent.disresponsiveList or Util.DummyTable, p.id) or
table.contains(cardEffectEvent.unoffsetableList or Util.DummyTable, p.id)
)
then
table.insert(players, p)
break
end
end
end
end
end
local prompt = ""
if cardEffectEvent.to then
prompt = "#AskForNullification::" .. cardEffectEvent.to .. ":" .. cardEffectEvent.card.name
elseif cardEffectEvent.from then
prompt = "#AskForNullificationWithoutTo:" .. cardEffectEvent.from .. "::" .. cardEffectEvent.card.name
end
local extra_data
if #TargetGroup:getRealTargets(cardEffectEvent.tos) > 1 then
local parentUseEvent = self.logic:getCurrentEvent():findParent(GameEvent.UseCard)
if parentUseEvent then
extra_data = { useEventId = parentUseEvent.id, effectTo = cardEffectEvent.to }
end
end
local use = self:askForNullification(players, nil, nil, prompt, true, extra_data, cardEffectEvent)
if use then
use.toCard = cardEffectEvent.card
use.responseToEvent = cardEffectEvent
self:useCard(use)
end
end
Fk.currentResponsePattern = nil
elseif event == fk.CardEffecting then
if cardEffectEvent.card.skill then
exec(GameEvent.SkillEffect, function ()
cardEffectEvent.card.skill:onEffect(self, cardEffectEvent)
end, self:getPlayerById(cardEffectEvent.from), cardEffectEvent.card.skill)
end
end
end
--- 对“打出牌”进行处理
---@param cardResponseEvent CardResponseEvent
function UseCardEventWrappers:responseCard(cardResponseEvent)
return exec(RespondCard, cardResponseEvent)
end
---@param card_name string @ 想要视为使用的牌名
---@param subcards? integer[] @ 子卡可以留空或者直接nil
---@param from ServerPlayer @ 使用来源
---@param tos ServerPlayer | ServerPlayer[] @ 目标角色(列表)
---@param skillName? string @ 技能名
---@param extra? boolean @ 是否不计入次数
---@return CardUseStruct
function UseCardEventWrappers:useVirtualCard(card_name, subcards, from, tos, skillName, extra)
local card = Fk:cloneCard(card_name)
card.skillName = skillName
if from:prohibitUse(card) then return false end
if tos.class then tos = { tos } end
for i, p in ipairs(tos) do
if from:isProhibited(p, card) then
table.remove(tos, i)
end
end
if #tos == 0 then return false end
if subcards then card:addSubcards(Card:getIdList(subcards)) end
local use = {} ---@type CardUseStruct
use.from = from.id
use.tos = table.map(tos, function(p) return { p.id } end)
use.card = card
use.extraUse = extra
self:useCard(use)
return use
end
return { UseCard, RespondCard, CardEffect, UseCardEventWrappers }

View File

@ -96,11 +96,9 @@ function GameLogic:assignRoles()
local p = room.players[i] local p = room.players[i]
p.role = roles[i] p.role = roles[i]
if p.role == "lord" then if p.role == "lord" then
p.role_shown = true room:setPlayerProperty(p, "role_shown", true)
room:broadcastProperty(p, "role")
else
room:notifyProperty(p, p, "role")
end end
room:broadcastProperty(p, "role")
end end
end end
@ -212,22 +210,9 @@ end
function GameLogic:prepareDrawPile() function GameLogic:prepareDrawPile()
local room = self.room local room = self.room
local allCardIds = Fk:getAllCardIds() local seed = math.random(2 << 32 - 1)
room:prepareDrawPile(seed)
for i = #allCardIds, 1, -1 do room:doBroadcastNotify("PrepareDrawPile", seed)
if Fk:getCardById(allCardIds[i]).is_derived then
local id = allCardIds[i]
table.removeOne(allCardIds, id)
table.insert(room.void, id)
room:setCardArea(id, Card.Void, nil)
end
end
table.shuffle(allCardIds)
room.draw_pile = allCardIds
for _, id in ipairs(room.draw_pile) do
self.room:setCardArea(id, Card.DrawPile, nil)
end
end end
function GameLogic:attachSkillToPlayers() function GameLogic:attachSkillToPlayers()
@ -774,7 +759,7 @@ function GameLogic:damageByCardEffect(is_exact)
local c_event = d_event:findParent(GameEvent.CardEffect, false, 2) local c_event = d_event:findParent(GameEvent.CardEffect, false, 2)
if c_event == nil then return false end if c_event == nil then return false end
return damage.card == c_event.data[1].card and return damage.card == c_event.data[1].card and
(not is_exact or d_event.data[1].from.id == c_event.data[1].from) (not is_exact or (damage.from or {}).id == c_event.data[1].from)
end end
function GameLogic:dumpEventStack(detailed) function GameLogic:dumpEventStack(detailed)

View File

@ -29,10 +29,14 @@ MarkEnum.BypassTimesLimitTo = "BypassTimesLimitTo"
MarkEnum.BypassDistancesLimitTo = "BypassDistancesLimitTo" MarkEnum.BypassDistancesLimitTo = "BypassDistancesLimitTo"
---非锁定技失效 ---非锁定技失效
MarkEnum.UncompulsoryInvalidity = "UncompulsoryInvalidity" MarkEnum.UncompulsoryInvalidity = "UncompulsoryInvalidity"
---失效技能表
MarkEnum.InvalidSkills = "InvalidSkills"
---不可明置值为表m - 主将, d - 副将) ---不可明置值为表m - 主将, d - 副将)
MarkEnum.RevealProhibited = "RevealProhibited" MarkEnum.RevealProhibited = "RevealProhibited"
---不计入距离、座次后缀 ---不计入距离、座次后缀
MarkEnum.PlayerRemoved = "PlayerRemoved" MarkEnum.PlayerRemoved = "PlayerRemoved"
---不能调整手牌
MarkEnum.SortProhibited = "SortProhibited"
---各种清除标记后缀 ---各种清除标记后缀
--- ---

316
lua/server/network.lua Normal file
View File

@ -0,0 +1,316 @@
---@class Request : Object
---@field public room Room
---@field public players ServerPlayer[]
---@field public n integer @ n个人做出回复后询问结束
---@field public accept_cancel? boolean @ 是否将取消也算作是收到回复
---@field public ai_start_time integer? @ 只剩AI思考开始的时间微秒delay专用
---@field public timeout? integer @ 本次耗时(秒),默认为房间内配置的出手时间
---@field public command string @ command自然就是command
---@field public data table<integer, any> @ 每个player对应的询问数据
---@field public default_reply table<integer, any> @ 玩家id - 默认回复内容
---@field public send_json boolean? @ 是否需要对data使用json.encode默认true
---@field public receive_json boolean? @ 是否需要对reply使用json.decode默认true
---@field private send_success table<fk.ServerPlayer, boolean> @ 数据是否发送成功不成功的后面全部视为AI
---@field public result table<integer, any> @ 玩家id - 回复内容 nil表示完全未回复
---@field public luck_data any? @ 是否是询问手气卡 TODO: 有需求的话把这个通用化一点
---@field private pending_requests table<fk.ServerPlayer, integer[]> @ 一控多时暂存的请求
local Request = class("Request")
-- TODO: 懒得思考了
-- 手气卡用:目前暂时写死一个属性而不是给函数参数;
-- player有个属性保存自己剩余手气卡次数
-- request有个luck_data属性来处理OK消息
-- 若还能再用一次那就重新发Request并继续等
---@param command string
---@param players ServerPlayer[]
---@param n? integer
function Request:initialize(command, players, n)
assert(#players > 0)
self.command = command
self.players = players
self.n = n or #players
-- 剩下的需要自己构造后修改相关值,构造函数只给四个
local room = players[1].room
self.room = room
self.data = {}
self.default_reply = {}
for _, p in ipairs(players) do self.default_reply[p.id] = "__cancel" end
self.timestamp = math.ceil(os.getms() / 1000)
self.timeout = room.timeout
self.send_json = true
self.receive_json = true -- 除了几个特殊字符串之外都decode
self.pending_requests = setmetatable({}, { __mode = "k" })
self.send_success = setmetatable({}, { __mode = "k" })
self.result = {}
end
function Request:__tostring()
return "<Request>"
end
function Request:setData(player, data)
self.data[player.id] = data
end
function Request:setDefaultReply(player, data)
self.default_reply[player.id] = data
end
-- 将相应请求数据发给player
-- 不能向thinking中的玩家发送这种情况下暂存起来等待收到答复后
---@param player ServerPlayer
function Request:_sendPacket(player)
local controller = player.serverplayer
-- 若正在烧条则不发将这个需要请求的玩家id存起来后面用
if controller:thinking() then
self.pending_requests[controller] = self.pending_requests[controller] or {}
table.insert(self.pending_requests[controller], player.id)
return
end
-- 若控制者目前的视角不是player先发个数据指示切换视角
if not table.contains(player._observers, controller) then
local from = table.find(self.room.players, function(p)
return table.contains(p._observers, controller)
end)
-- 切换视角
table.removeOne(from._observers, controller)
table.insert(player._observers, controller)
controller:doNotify("ChangeSelf", tostring(player.id))
end
-- 发送请求数据并将控制者标记为烧条中
local jsonData = self.data[player.id]
if self.send_json then jsonData = json.encode(jsonData) end
-- FIXME: 这里确认数据是否发送的环节一定要写在C++代码中
self.send_success[controller] = controller:getState() == fk.Player_Online
controller:doRequest(self.command, jsonData, self.timeout, self.timestamp)
controller:setThinking(true)
end
-- 检查一下该玩家是否已经有答复了若为AI则直接计算出回复
-- 一般来说在一次同时询问中需要人类玩家全部回复完了AI才进行回复
---@param player ServerPlayer
---@param use_ai boolean
function Request:_checkReply(player, use_ai)
local room = self.room
-- 若被人类玩家控制着,靠人类玩家自己分析了
-- 否则交给该Player自己的AI进行考虑换言之AI控人没有效果不会故意杀队友
local controller = player.serverplayer
local state = controller:getState()
local reply
if state == fk.Player_Online and self.send_success[controller] then
if not table.contains(player._observers, controller) then
-- 若控制者的视角不在自己,那就不管了
reply = "__notready"
else
reply = controller:waitForReply(0)
if reply ~= "__notready" then
controller:setThinking(false)
-- FIXME: 写的依托且不考虑控制 后面看情况改!
if self.luck_data then
local luck_data = self.luck_data
if reply ~= "__cancel" then
local pdata = luck_data[player.id]
pdata.luckTime = pdata.luckTime - 1
luck_data.discardInit(room, player)
luck_data.drawInit(room, player, pdata.num)
if pdata.luckTime > 0 then
self:setData(player, { "AskForLuckCard", "#AskForLuckCard:::" .. pdata.luckTime })
self:_sendPacket(player)
reply = "__notready"
end
end
else
local pending_list = self.pending_requests[controller]
if pending_list and #pending_list > 0 then
local pid = table.remove(pending_list, 1)
self:_sendPacket(room:getPlayerById(pid))
end
end
end
end
else
room:checkNoHuman()
if use_ai then
player.ai.command = self.command
-- FIXME: 后面进行SmartAI的时候准备爆破此处
-- player.ai.data = self.data[player.id]
player.ai.jsonData = self.data[player.id]
if player.ai:isInstanceOf(RandomAI) then
reply = "__cancel"
else
reply = player.ai:makeReply()
end
else
-- 还没轮到AI呢所以需要标记为未答复
reply = "__notready"
end
end
return reply
end
function Request:getWinners()
local ret = {}
for _, p in ipairs(self.players) do
local result = self.result[p.id]
if result and result ~= "" then
table.insert(ret, p)
end
end
return ret
end
function Request:ask()
local room = self.room
-- 0. 设置计时器,防止因无人回复一直等下去
room.room:setRequestTimer(self.timeout * 1000 + 500)
local players = table.simpleClone(self.players)
local currentTime = os.time()
-- 1. 向所有人发送询问请求
for _, p in ipairs(players) do
self:_sendPacket(p)
end
-- 2. 进入循环等待结束条件为已有n个回复或者超时或者有人点了
-- 若很多人都取消了导致最多回复数达不到n了那么也结束
local replied_players = 0
local ready_players = 0
while true do
local changed = false
-- 判断1若投降则直接结束全部询问若超时则踢掉所有人类玩家这样AI还可计算
if room.hasSurrendered then break end
local elapsed = os.time() - currentTime
if self.timeout - elapsed <= 0 then
for i = #players, 1, -1 do
if self.send_success[players[i].serverplayer] then
table.remove(players, i)
end
end
end
if table.every(players, function(p)
return p.serverplayer:getState() ~= fk.Player_Online or not
self.send_success[p.serverplayer]
end) then
self.ai_start_time = os.getms()
end
-- 判断2收到足够多回复了
local use_ai = self.ai_start_time ~= nil
for i = #players, 1, -1 do
local player = players[i]
local reply = self:_checkReply(player, use_ai)
if reply ~= "__notready" then
if reply ~= "__cancel" and self.receive_json then
reply = json.decode(reply)
end
self.result[player.id] = reply
table.remove(players, i)
replied_players = replied_players + 1
changed = true
if reply ~= "__cancel" or self.accept_cancel then
ready_players = ready_players + 1
if ready_players >= self.n then
for _, p in ipairs(self.players) do
-- 避免触发后续的烧条检测
if self.result[p.id] == nil then
self.result[p.id] = "__failed_in_race"
end
end
break -- 注意外面还有一层循环
end
end
end
end
if #players + ready_players < self.n then break end
if ready_players >= self.n then break end
-- 防止万一如果AI算完后还是有机器人notready的话也别等了
-- 不然就永远别想被唤醒了
if self.ai_start_time then break end
-- 需要等待呢,等待被唤醒吧
if not changed then
coroutine.yield("__handleRequest")
end
end
room.room:destroyRequestTimer()
self:finish()
end
local function surrenderCheck(room)
if not room.hasSurrendered then return end
local player = table.find(room.players, function(p)
return p.surrendered
end)
if not player then
room.hasSurrendered = false
return
end
room:broadcastProperty(player, "surrendered")
local mode = Fk.game_modes[room.settings.gameMode]
local winner = Pcall(mode.getWinner, mode, player)
if winner ~= "" then
room:gameOver(winner)
end
-- 以防万一
player.surrendered = false
room:broadcastProperty(player, "surrendered")
room.hasSurrendered = false
end
-- 善后工作主要是result规范化、投降检测等
function Request:finish()
local room = self.room
surrenderCheck(room)
-- FIXME: 这里QML中有个bug这个命令应该是用来暗掉玩家面板的
-- room:doBroadcastNotify("CancelRequest", "")
for _, p in ipairs(self.players) do
p.serverplayer:setThinking(false)
if self.result[p.id] == nil then
self.result[p.id] = self.default_reply[p.id]
p._timewaste_count = p._timewaste_count + 1
if p._timewaste_count >= 3 and p.serverplayer:getState() == fk.Player_Online then
p._timewaste_count = 0
p.serverplayer:emitKick()
end
else
p._timewaste_count = 0
end
if self.result[p.id] == "__cancel" then
self.result[p.id] = ""
end
if self.result[p.id] == "__failed_in_race" then
self.result[p.id] = nil
end
end
room.last_request = self
for _, isHuman in pairs(self.send_success) do
if not self.ai_start_time then break end
if not isHuman then
local to_delay = 500 - (os.getms() - self.ai_start_time) / 1000
room:delay(to_delay)
break
end
end
end
return Request

View File

@ -1,9 +1,12 @@
-- SPDX-License-Identifier: GPL-3.0-or-later -- SPDX-License-Identifier: GPL-3.0-or-later
-- 本文件是用来处理各种异步请求的
-- 与游戏中常见的请求-答复没有什么联系
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 start_time = os.getms()
local summary = self:getSummary(observee, true) local summary = self:toJsonObject(observee)
player:doNotify("Observe", json.encode(summary)) player:doNotify("Observe", json.encode(summary))
fk.qInfo(string.format("[Observe] %d, %s, in %.3fms", fk.qInfo(string.format("[Observe] %d, %s, in %.3fms",
@ -61,54 +64,6 @@ request_handlers["prelight"] = function(room, id, reqlist)
end end
end end
request_handlers["luckcard"] = function(room, id, reqlist)
local p = room:getPlayerById(id)
local cancel = reqlist[3] == "false"
local luck_data = room:getTag("LuckCardData")
if not (p and luck_data) then return end
local pdata = luck_data[id]
if not cancel then
pdata.luckTime = pdata.luckTime - 1
luck_data.discardInit(room, p)
luck_data.drawInit(room, p, pdata.num)
else
pdata.luckTime = 0
end
if pdata.luckTime > 0 then
p:doNotify("AskForLuckCard", pdata.luckTime)
else
p.serverplayer:setThinking(false)
ResumeRoom(room.id)
end
room:setTag("LuckCardData", luck_data)
end
request_handlers["changeself"] = function(room, id, reqlist)
local p = room:getPlayerById(id)
local toId = tonumber(reqlist[3])
local from = p
local to = room:getPlayerById(toId)
local from_sp = from._splayer
-- 注意发来信息的玩家的主视角可能已经不是自己了
-- 先换成正确的玩家
from = table.find(room.players, function(p)
return table.contains(p._observers, from_sp)
end)
-- 切换视角
table.removeOne(from._observers, from_sp)
table.insert(to._observers, from_sp)
from_sp:doNotify("ChangeSelf", json.encode {
id = toId,
handcards = to:getCardIds(Player.Hand),
special_cards = to.special_cards,
})
end
request_handlers["surrender"] = function(room, id, reqlist) request_handlers["surrender"] = function(room, id, reqlist)
local player = room:getPlayerById(id) local player = room:getPlayerById(id)
if not player then return end if not player then return end

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,6 @@
---@field public skipped_phases Phase[] ---@field public skipped_phases Phase[]
---@field public phase_state table[] ---@field public phase_state table[]
---@field public phase_index integer ---@field public phase_index integer
---@field public role_shown boolean
---@field private _fake_skills Skill[] ---@field private _fake_skills Skill[]
---@field private _manually_fake_skills Skill[] ---@field private _manually_fake_skills Skill[]
---@field public prelighted_skills Skill[] ---@field public prelighted_skills Skill[]
@ -32,11 +31,12 @@ function ServerPlayer:initialize(_self)
self.room = nil self.room = nil
-- Below are for doBroadcastRequest -- Below are for doBroadcastRequest
-- 但是几乎全部被船新request杀了
self.request_data = "" self.request_data = ""
self.client_reply = "" --self.client_reply = ""
self.default_reply = "" self.default_reply = ""
self.reply_ready = false --self.reply_ready = false
self.reply_cancel = false --self.reply_cancel = false
self.phases = {} self.phases = {}
self.skipped_phases = {} self.skipped_phases = {}
self.phase_state = {} self.phase_state = {}
@ -74,92 +74,14 @@ function ServerPlayer:doNotify(command, jsonData)
end end
end end
--- Send a request to client, and allow client to reply within *timeout* seconds. -- FIXME: 基本都改成新写法后删了这个兼容玩意
--- function ServerPlayer:__index(k)
--- *timeout* must not be negative. If nil, room.timeout is used. local request = self.room.last_request
---@param command string if not request then return nil end
---@param jsonData string if k == "client_reply" then
---@param timeout? integer return request.result[self.id]
function ServerPlayer:doRequest(command, jsonData, timeout) elseif k == "reply_ready" then
self.client_reply = "" return request.result[self.id] and request.result[self.id] ~= ""
self.reply_ready = false
self.reply_cancel = false
if self.serverplayer:busy() then
self.room.request_queue[self.serverplayer] = self.room.request_queue[self.serverplayer] or {}
table.insert(self.room.request_queue[self.serverplayer], { self.id, command, jsonData, timeout })
return
end
self.room.request_self[self.serverplayer:getId()] = self.id
if not table.contains(self._observers, self.serverplayer) then
self.serverplayer:doNotify("StartChangeSelf", tostring(self.id))
end
timeout = timeout or self.room.timeout
self.serverplayer:setBusy(true)
self.ai_data = {
command = command,
jsonData = jsonData,
}
self.serverplayer:doRequest(command, jsonData, timeout)
end
local function _waitForReply(player, timeout)
local result
local start = os.getms()
local state = player.serverplayer:getState()
player.request_timeout = timeout
player.request_start = start
if state ~= fk.Player_Online then
if player.room.hasSurrendered then
return "__cancel"
end
if state ~= fk.Player_Robot then
player.room:checkNoHuman()
player.room:delay(500)
return "__cancel"
end
-- Let AI make reply. First handle request
-- coroutine.yield("__handleRequest", 0)
player.room:checkNoHuman()
player.ai:readRequestData()
local reply = player.ai:makeReply()
if reply == "" then reply = "__cancel" end
return reply
end
while true do
player.serverplayer:setThinking(true)
result = player.serverplayer:waitForReply(0)
if result ~= "__notready" then
player._timewaste_count = 0
player.serverplayer:setThinking(false)
return result
end
local rest = timeout * 1000 - (os.getms() - start) / 1000
if timeout and rest <= 0 then
if timeout >= 15 then
player._timewaste_count = player._timewaste_count + 1
end
player.serverplayer:setThinking(false)
if player._timewaste_count >= 3 then
player._timewaste_count = 0
player.serverplayer:emitKick()
end
return ""
end
if player.room.hasSurrendered then
player.serverplayer:setThinking(false)
return ""
end
coroutine.yield("__handleRequest", rest)
end end
end end
@ -173,128 +95,27 @@ function ServerPlayer:chat(msg)
}) })
end end
--- Wait for at most *timeout* seconds for reply from client. function ServerPlayer:toJsonObject()
--- local o = Player.toJsonObject(self)
--- If *timeout* is negative or **nil**, the function will wait forever until get reply.
---@param timeout integer @ seconds to wait
---@return string @ JSON data
function ServerPlayer:waitForReply(timeout)
local result = _waitForReply(self, timeout)
local sid = self.serverplayer:getId()
local id = self.id
if self.room.request_self[sid] ~= id then
result = ""
end
self.request_data = ""
self.client_reply = result
if result == "__cancel" then
result = ""
self.reply_cancel = true
self.serverplayer:setBusy(false)
self.serverplayer:setThinking(false)
end
if result ~= "" then
self.reply_ready = true
self.serverplayer:setBusy(false)
self.serverplayer:setThinking(false)
end
-- FIXME: 一控多求无懈
local queue = self.room.request_queue[self.serverplayer]
if queue and #queue > 0 and not self.serverplayer:busy() then
local i, c, j, t = table.unpack(table.remove(queue, 1))
self.room:getPlayerById(i):doRequest(c, j, t)
end
return result
end
local function assign(t1, t2, k)
t1[k] = t2[k]
end
-- 获取摘要信息。供重连/旁观使用
-- 根据参数,返回一个大表保存自己的信息,客户端自行分析
---@param player ServerPlayer
---@param observe? boolean
function ServerPlayer:getSummary(player, observe)
local room = self.room
if not room.game_started then
local ret = { p = {} }
-- If game does not starts, that mean we are entering room that
-- all players are choosing their generals.
-- Note that when we are in this function, the main thread must be
-- calling delay() or waiting for reply.
if self.role_shown then
-- room:notifyProperty(player, self, "role")
ret.p.general = self.general
ret.p.deputyGeneral = self.deputyGeneral
ret.p.role = self.role
end
return ret
end
local properties = {}
assign(properties, self, "general")
assign(properties, self, "deputyGeneral")
assign(properties, self, "maxHp")
assign(properties, self, "hp")
assign(properties, self, "shield")
assign(properties, self, "gender")
assign(properties, self, "kingdom")
if self.dead then
assign(properties, self, "dead")
assign(properties, self, self.rest > 0 and "rest" or "role")
else
assign(properties, self, "seat")
assign(properties, self, "phase")
end
if not self.faceup then
assign(properties, self, "faceup")
end
if self.chained then
assign(properties, self, "chained")
end
if self.role_shown then
assign(properties, self, "role")
end
if #self.sealedSlots > 0 then
assign(properties, self, "sealedSlots")
end
local sp = self._splayer local sp = self._splayer
o.setup_data = {
return {
-- data for Setup/AddPlayer
d = {
self.id, self.id,
sp:getScreenName(), sp:getScreenName(),
sp:getAvatar(), sp:getAvatar(),
false, false,
sp:getTotalGameTime(), sp:getTotalGameTime(),
},
p = properties,
ch = self.cardUsedHistory,
sh = self.skillUsedHistory,
m = self.mark,
s = table.map(self.player_skills, Util.NameMapper),
c = self.player_cards,
sc = self.special_cards,
} }
return o
end end
-- 似乎没有必要
-- function ServerPlayer:loadJsonObject() end
function ServerPlayer:reconnect() function ServerPlayer:reconnect()
local room = self.room local room = self.room
self.serverplayer:setState(fk.Player_Online) self.serverplayer:setState(fk.Player_Online)
local summary = room:getSummary(self, false) local summary = room:toJsonObject(self)
self:doNotify("Reconnect", json.encode(summary)) self:doNotify("Reconnect", json.encode(summary))
room:notifyProperty(self, self, "role") room:notifyProperty(self, self, "role")
self:doNotify("RoomOwner", json.encode{ room.room:getOwner():getId() }) self:doNotify("RoomOwner", json.encode{ room.room:getOwner():getId() })
@ -331,6 +152,7 @@ function ServerPlayer:turnOver()
self.room.logic:trigger(fk.TurnedOver, self) self.room.logic:trigger(fk.TurnedOver, self)
end end
---@param cards integer|integer[]|Card|Card[]
function ServerPlayer:showCards(cards) function ServerPlayer:showCards(cards)
cards = Card:getIdList(cards) cards = Card:getIdList(cards)
for _, id in ipairs(cards) do for _, id in ipairs(cards) do
@ -355,12 +177,7 @@ function ServerPlayer:showCards(cards)
room.logic:trigger(fk.CardShown, self, { cardIds = cards }) room.logic:trigger(fk.CardShown, self, { cardIds = cards })
end end
local phase_name_table = {
[Player.Judge] = "phase_judge",
[Player.Draw] = "phase_draw",
[Player.Play] = "phase_play",
[Player.Discard] = "phase_discard",
}
---@param from_phase Phase ---@param from_phase Phase
---@param to_phase Phase ---@param to_phase Phase
@ -427,7 +244,7 @@ function ServerPlayer:gainAnExtraPhase(phase, delay)
room:sendLog{ room:sendLog{
type = "#GainAnExtraPhase", type = "#GainAnExtraPhase",
from = self.id, from = self.id,
arg = phase_name_table[phase], arg = Util.PhaseStrMapper(phase),
} }
GameEvent.Phase:create(self, self.phase):exec() GameEvent.Phase:create(self, self.phase):exec()
@ -441,7 +258,7 @@ function ServerPlayer:gainAnExtraPhase(phase, delay)
room:sendLog{ room:sendLog{
type = "#PhaseSkipped", type = "#PhaseSkipped",
from = self.id, from = self.id,
arg = phase_name_table[phase], arg = Util.PhaseStrMapper(phase),
} }
end end
@ -479,7 +296,7 @@ function ServerPlayer:play(phase_table)
end end
for i = 1, #phases do for i = 1, #phases do
if self.dead then if self.dead or room:getTag("endTurn") or phases[i] == nil then
self:changePhase(self.phase, Player.NotActive) self:changePhase(self.phase, Player.NotActive)
break break
end end
@ -514,7 +331,7 @@ function ServerPlayer:play(phase_table)
room:sendLog{ room:sendLog{
type = "#PhaseSkipped", type = "#PhaseSkipped",
from = self.id, from = self.id,
arg = phase_name_table[self.phase], arg = Util.PhaseStrMapper(self.phase),
} }
end end
end end
@ -540,10 +357,17 @@ end
--- 当进行到出牌阶段空闲点时,结束出牌阶段。 --- 当进行到出牌阶段空闲点时,结束出牌阶段。
function ServerPlayer:endPlayPhase() function ServerPlayer:endPlayPhase()
self._play_phase_end = true if self.phase == Player.Play then
self._phase_end = true
end
-- TODO: send log -- TODO: send log
end end
--- 结束当前阶段。
function ServerPlayer:endCurrentPhase()
self._phase_end = true
end
--- 获得一个额外回合 --- 获得一个额外回合
---@param delay? boolean ---@param delay? boolean
---@param skillName? string ---@param skillName? string
@ -1014,7 +838,7 @@ function ServerPlayer:addBuddy(other)
other = self.room:getPlayerById(other) other = self.room:getPlayerById(other)
end end
Player.addBuddy(self, other) Player.addBuddy(self, other)
self:doNotify("AddBuddy", json.encode{ other.id, other.player_cards[Player.Hand] }) self.room:doBroadcastNotify("AddBuddy", json.encode{ self.id, other.id })
end end
function ServerPlayer:removeBuddy(other) function ServerPlayer:removeBuddy(other)
@ -1022,7 +846,7 @@ function ServerPlayer:removeBuddy(other)
other = self.room:getPlayerById(other) other = self.room:getPlayerById(other)
end end
Player.removeBuddy(self, other) Player.removeBuddy(self, other)
self:doNotify("RmBuddy", tostring(other.id)) self.room:doBroadcastNotify("RmBuddy", json.encode{ self.id, other.id })
end end
return ServerPlayer return ServerPlayer

View File

@ -82,6 +82,7 @@ fk.IceDamage = 4
---@field public skillName? string @ 造成本次伤害的技能名 ---@field public skillName? string @ 造成本次伤害的技能名
---@field public beginnerOfTheDamage? boolean @ 是否是本次铁索传导的起点 ---@field public beginnerOfTheDamage? boolean @ 是否是本次铁索传导的起点
---@field public by_user? boolean @ 是否由卡牌直接生效造成的伤害 ---@field public by_user? boolean @ 是否由卡牌直接生效造成的伤害
---@field public chain_table? ServerPlayer[] @ 铁索连环表
--- RecoverStruct 描述和回复体力有关的数据。 --- RecoverStruct 描述和回复体力有关的数据。
---@class RecoverStruct ---@class RecoverStruct

152
lua/ui_emu/base.lua Normal file
View File

@ -0,0 +1,152 @@
-- 模拟一套UI操作并在具体子类中实现相应操作逻辑。分为UI组件和UI场景两种类。
-- 在客户端与Qml直接同步在服务端中用于AI。
-- 模拟UI组件。最基本的属性为enabled表示是否可以进行交互。
-- 注意在编写逻辑时不要直接修改Item的属性。用scene:update。
---@class Item: Object
---@field public parent Scene
---@field public enabled boolean
---@field public id string | integer
local Item = class("Item")
---@parant scene Scene
function Item:initialize(scene, id)
self.parent = scene
self.enabled = false
self.id = id
end
function Item:toData()
return {
enabled = self.enabled,
id = self.id,
}
end
---@return boolean 是否发生改变
function Item:setData(newData)
local changed
for k, v in pairs(newData) do
changed = changed or (self[k] ~= v)
self[k] = v
end
return changed
end
---@class SelectableItem: Item
---@field public selected boolean
local SelectableItem = Item:subclass("SelectableItem")
---@parant scene Scene
function SelectableItem:initialize(scene, id)
Item.initialize(self, scene, id)
self.selected = false
end
function SelectableItem:toData()
local ret = Item.toData(self)
ret.selected = self.selected
return ret
end
-- 最基本的“交互”对应到UI中就是一次点击。
-- 在派生类中视情况可能要为其传入参数表示修改后的值。
function Item:interact() end
--[[
UI场景UI组件UI进行信息交换
Scene与Handler进行开发时Scene只需要创建Item并管理就行了
RequestHandler及其子类中编写
UI组件的是Scene
1. 使scene:update方法来更新Item的属性
-------------------------------
[QML] cardItem.enabled = true;
[Lua] scene:update("CardItem", cid, { enabled = true })
QMLQML的
update方法来记录相关的属性变动
2. 使Scene提供的方法来访问元素
---------------------------------
RoomScene中已经创建了表达卡牌和技能的ItemHandler的逻辑编写中
使getCards或者getSkills这样获取原始属性的函数访Item
--]]
---@class Scene: Object
---@field public parent RequestHandler
---@field public scene_name string
---@field public items { [string]: { [string|integer]: Item } }
local Scene = class("Scene")
function Scene:initialize(parent)
self.parent = parent
self.items = {}
end
---@param item Item
function Scene:addItem(item, ui_data)
local key = item.class.name
self.items[key] = self.items[key] or {}
self.items[key][item.id] = item
local changeData = self.parent.change
if changeData then
local k = "_new"
changeData[k] = changeData[k] or {}
table.insert(changeData[k], {
type = key,
data = item:toData(),
ui_data = ui_data,
})
end
end
function Scene:removeItem(elemType, id, ui_data)
local tab = self.items[elemType]
if type(tab) ~= "table" then return end
if not (tab and tab[id]) then return end
tab[id] = nil
local changeData = self.parent.change
if changeData then
local k = "_delete"
changeData[k] = changeData[k] or {}
table.insert(changeData[k], {
type = elemType,
id = id,
ui_data = ui_data,
})
end
end
function Scene:getAllItems(elemType)
return self.items[elemType] or Util.DummyTable
end
-- 模拟一次UI交互修改相关item的属性即可
-- 同时修改自己parent的changeData
function Scene:update(elemType, id, newData)
local item = self.items[elemType][id]
local changed = item:setData(newData)
local changeData = self.parent.change
if changed and changeData then
changeData[elemType] = changeData[elemType] or {}
table.insert(changeData[elemType], item:toData())
end
end
-- 一般由RequestHandler或者其他上层部分调用
-- 调用者需要维护changeData确保传给UI的数据最少
function Scene:notifyUI()
if not ClientInstance then return nil end
self.parent.change["_type"] = self.scene_name
ClientInstance:notifyUI("UpdateRequestUI", self.parent.change)
end
return {
Item = Item,
SelectableItem = SelectableItem,
Scene = Scene,
}

View File

@ -0,0 +1,34 @@
local PopupBox = require 'ui_emu.popupbox'
local common = require 'ui_emu.common'
local CardItem = common.CardItem
-- 过河拆桥、顺手牵羊使用的选卡包
---@class ChooseCardBox: PopupBox
local ChooseCardBox = PopupBox:subclass("ChooseCardBox")
function ChooseCardBox:initialize(player)
self.room = Fk:currentRoom()
self.player = player
end
-- 打开qml框后的初始化对应request打开qml框的操作
---@param data any @ 数据
function ChooseCardBox:setup(data)
for _, cid in ipairs(data.cards) do
self:addItem(CardItem:new(self, cid))
end
end
-- 父场景将UI应有的变化传至此处
-- 需要实现各种合法性检验决定需要变更状态的UI并最终将变更反馈给真实的界面
---@param elemType string @ 元素类型
---@param id any @ 元素ID
---@param action any @ 动作
---@param data any @ 数据
---@return { [string]: Item[] }
function ChooseCardBox:update(elemType, id, action, data)
-- 返回自己的变化
return self.change
end
return PopupBox

29
lua/ui_emu/common.lua Normal file
View File

@ -0,0 +1,29 @@
local base = require 'ui_emu.base'
local SelectableItem = base.SelectableItem
---@class CardItem: SelectableItem
local CardItem = SelectableItem:subclass("CardItem")
---@class Photo: SelectableItem
---@field public state string
local Photo = SelectableItem:subclass("Photo")
function Photo:initialize(scene, id)
SelectableItem.initialize(self, scene, id)
self.state = "normal"
end
function Photo:toData()
local ret = SelectableItem.toData(self)
ret.state = self.state
return ret
end
---@class SkillButton: SelectableItem
local SkillButton = SelectableItem:subclass("SkillButton")
return {
CardItem = CardItem,
Photo = Photo,
SkillButton = SkillButton,
}

11
lua/ui_emu/control.lua Normal file
View File

@ -0,0 +1,11 @@
-- 对应QtQuick.Controls里面的组件 或者相对应的
-- 以后可能还有更多需要模拟的组件吧
local base = require 'ui_emu.base'
local Item = base.Item
---@class Button: SelectableItem
local Button = Item:subclass("Button")
return {
Button = Button,
}

View File

@ -0,0 +1,23 @@
local Item = (require 'ui_emu.base').Item
-- 用来表明interaction的Item格式大约可比照ui-util.lua中的表定义
---@class Interaction: Item
---@field public spec any 弹出的东西
---@field public skill_name string 技能名
---@field public data any skill.interaction.data
local Interaction = Item:subclass("Interaction")
function Interaction:initialize(scene, id, spec)
Item.initialize(self, scene, id)
self.spec = spec
self.enabled = true
end
function Interaction:toData()
local ret = Item.toData(self)
ret.spec = self.spec
ret.skill_name = self.skill_name
return ret
end
return Interaction

18
lua/ui_emu/okscene.lua Normal file
View File

@ -0,0 +1,18 @@
local base = require 'ui_emu.base'
local control = require 'ui_emu.control'
local Scene = base.Scene
local Button = control.Button
---@class OKScene: Scene
local OKScene = Scene:subclass("OKScene")
OKScene.scene_name = "Room"
---@param parent RequestHandler
function OKScene:initialize(parent)
Scene.initialize(self, parent)
self:addItem(Button:new(self, "OK"))
self:addItem(Button:new(self, "Cancel"))
end
return OKScene

42
lua/ui_emu/popupbox.lua Normal file
View File

@ -0,0 +1,42 @@
local base = require 'ui_emu.base'
local control = require 'ui_emu.control'
local Scene = base.Scene
-- 一种模拟具体qml框的处理机构
-- 具体是什么东西全靠继承子类处理
-- 理论上来说这就是一个小scene
-- 会向其父场景传输其应有的变化
-- 同理UI改变也由父场景传输至这里
---@class PopupBox: Scene
local PopupBox = Scene:subclass("PopupBox")
-- 打开qml框后的初始化对应request打开qml框的操作
function PopupBox:initialize(parent, data)
Scene.initialize(self, parent)
self.data = data
self.change = {}
end
-- 模拟一次UI交互修改相关item的属性即可
-- 同时修改自己parent的changeData
function PopupBox:update(elemType, id, newData)
local item = self.items[elemType][id]
local changed = item:setData(newData)
local changeData = self.change
if changed and changeData then
changeData[elemType] = changeData[elemType] or {}
table.insert(changeData[elemType], item:toData())
end
end
-- 由父RequestHandler调用用以将本qml变化传至父RequestHandler
-- 调用者需要维护changeData确保传给UI的数据最少
function PopupBox:notifyUI()
if not ClientInstance then return nil end
self.parent.change["_type"] = self.class.name
ClientInstance:notifyUI("UpdateRequestUI", self.parent.change)
end
return PopupBox

0
lua/ui_emu/poxi_box.lua Normal file
View File

86
lua/ui_emu/roomscene.lua Normal file
View File

@ -0,0 +1,86 @@
local common = require 'ui_emu.common'
local OKScene = require 'ui_emu.okscene'
local CardItem = common.CardItem
local Photo = common.Photo
local SkillButton = common.SkillButton
---@class RoomScene: OKScene
local RoomScene = OKScene:subclass("RoomScene")
RoomScene.scene_name = "Room"
---@param parent RequestHandler
function RoomScene:initialize(parent)
OKScene.initialize(self, parent)
local player = parent.player
for _, p in ipairs(parent.room.alive_players) do
self:addItem(Photo:new(self, p.id))
end
for _, cid in ipairs(player:getCardIds("h")) do
self:addItem(CardItem:new(self, cid))
end
for _, skill in ipairs(player:getAllSkills()) do
if skill:isInstanceOf(ActiveSkill) or skill:isInstanceOf(ViewAsSkill) then
self:addItem(SkillButton:new(self, skill.name))
end
end
end
function RoomScene:unselectOtherCards(cid)
local dat = { selected = false }
for id, _ in pairs(self:getAllItems("CardItem")) do
if id ~= cid then
self:update("CardItem", id, dat)
end
end
end
function RoomScene:unselectOtherTargets(pid)
local dat = { selected = false }
for id, _ in pairs(self:getAllItems("Photo")) do
if id ~= pid then
self:update("Photo", id, dat)
end
end
end
RoomScene.unselectAllCards = RoomScene.unselectOtherCards
RoomScene.unselectAllTargets = RoomScene.unselectOtherTargets
-- 若所有角色都不可选则将state设为normal; 反之只要有可选的就设candidate
-- 这样更美观
function RoomScene:updateTargetEnability(pid, enabled)
local photoTab = self.items["Photo"]
local photo = photoTab[pid]
self:update("Photo", pid, { enabled = not not enabled })
if enabled then
if photo.state == "normal" then
for id, _ in pairs(photoTab) do
self:update("Photo", id, { state = "candidate" })
end
end
else
local allDisabled = true
for _, v in pairs(photoTab) do
if v.enabled then
allDisabled = false
break
end
end
if allDisabled then
for id, _ in pairs(photoTab) do
self:update("Photo", id, { state = "normal" })
end
end
end
end
function RoomScene:disableAllTargets()
for id, _ in pairs(self.items["Photo"]) do
self:update("Photo", id, {
state = "normal",
selected = false,
enabled = false,
})
end
end
return RoomScene

View File

@ -0,0 +1,21 @@
local Item = (require 'ui_emu.base').Item
-- 用来表明SpecialSkills的Item本质是一个单选框
---@class SpecialSkills: Item
---@field public skills string[] 技能名
---@field public data any orig_text
local SpecialSkills = Item:subclass("SpecialSkills")
function SpecialSkills:initialize(scene, id)
Item.initialize(self, scene, id)
self.skills = {}
self.enabled = true
end
function SpecialSkills:toData()
local ret = Item.toData(self)
ret.skills = self.skills
return ret
end
return SpecialSkills

View File

@ -4,6 +4,9 @@ local extension = Package:new("maneuvering", Package.CardPack)
local slash = Fk:cloneCard("slash") local slash = Fk:cloneCard("slash")
Fk:addDamageNature(fk.FireDamage, "fire_damage")
Fk:addDamageNature(fk.ThunderDamage, "thunder_damage")
local thunderSlashSkill = fk.CreateActiveSkill{ local thunderSlashSkill = fk.CreateActiveSkill{
name = "thunder__slash_skill", name = "thunder__slash_skill",
prompt = function(self, selected_cards) prompt = function(self, selected_cards)
@ -462,10 +465,21 @@ local silverLion = fk.CreateArmor{
} }
extension:addCard(silverLion) extension:addCard(silverLion)
local hualiuSkill = fk.CreateDistanceSkill{
name = "#hualiu_skill",
attached_equip = "hualiu",
correct_func = function(self, from, to)
if to:hasSkill(self) then
return 1
end
end,
}
Fk:addSkill(hualiuSkill)
local huaLiu = fk.CreateDefensiveRide{ local huaLiu = fk.CreateDefensiveRide{
name = "hualiu", name = "hualiu",
suit = Card.Diamond, suit = Card.Diamond,
number = 13, number = 13,
equip_skill = hualiuSkill,
} }
extension:addCards({ extension:addCards({
@ -549,6 +563,8 @@ Fk:loadTranslationTable{
[":hualiu"] = "装备牌·坐骑<br /><b>坐骑技能</b>:其他角色与你的距离+1。", [":hualiu"] = "装备牌·坐骑<br /><b>坐骑技能</b>:其他角色与你的距离+1。",
} }
Fk:loadTranslationTable(require 'packages/maneuvering/i18n/en_US', 'en_US') local pkgprefix = "packages/"
if UsingNewCore then pkgprefix = "packages/freekill-core/" end
Fk:loadTranslationTable(require(pkgprefix .. 'maneuvering/i18n/en_US'), 'en_US')
return extension return extension

View File

@ -87,6 +87,13 @@ local choosePlayersSkill = fk.CreateActiveSkill{
return table.contains(self.targets, to_select) return table.contains(self.targets, to_select)
end end
end, end,
target_tip = function(self, to_select, selected, selected_cards, card, selectable, extra_data)
if self.targetTipName then
local targetTip = Fk.target_tips[self.targetTipName]
assert(targetTip)
return targetTip.target_tip(self, to_select, selected, selected_cards, card, selectable, extra_data)
end
end,
card_num = function(self) return self.pattern ~= "" and 1 or 0 end, card_num = function(self) return self.pattern ~= "" and 1 or 0 end,
min_target_num = function(self) return self.min_num end, min_target_num = function(self) return self.min_num end,
max_target_num = function(self) return self.num end, max_target_num = function(self) return self.num end,
@ -104,9 +111,6 @@ local exChooseSkill = fk.CreateActiveSkill{
local checkpoint = true local checkpoint = true
local card = Fk:getCardById(to_select) local card = Fk:getCardById(to_select)
if not self.include_equip then
checkpoint = checkpoint and (Fk:currentRoom():getCardArea(to_select) ~= Player.Equip)
end
if self.pattern and self.pattern ~= "" then if self.pattern and self.pattern ~= "" then
checkpoint = checkpoint and (Exppattern:Parse(self.pattern):match(card)) checkpoint = checkpoint and (Exppattern:Parse(self.pattern):match(card))
@ -119,6 +123,13 @@ local exChooseSkill = fk.CreateActiveSkill{
return table.contains(self.targets, to_select) return table.contains(self.targets, to_select)
end end
end, end,
target_tip = function(self, to_select, selected, selected_cards, card, selectable, extra_data)
if self.targetTipName then
local targetTip = Fk.target_tips[self.targetTipName]
assert(targetTip)
return targetTip.target_tip(self, to_select, selected, selected_cards, card, selectable, extra_data)
end
end,
min_target_num = function(self) return self.min_t_num end, min_target_num = function(self) return self.min_t_num end,
max_target_num = function(self) return self.max_t_num end, max_target_num = function(self) return self.max_t_num end,
min_card_num = function(self) return self.min_c_num end, min_card_num = function(self) return self.min_c_num end,

View File

@ -152,8 +152,8 @@ Fk:loadTranslationTable({
["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.",
["fastchat_m"] = "快捷短语", ["fastchat_m"] = "quick chats",
["fastchat_f"] = "快捷短语", ["fastchat_f"] = "quick chats",
["$fastchat_m1"] = "能不能快一点啊,兵贵神速啊。", ["$fastchat_m1"] = "能不能快一点啊,兵贵神速啊。",
["$fastchat_m2"] = "主公,别开枪,自己人!", ["$fastchat_m2"] = "主公,别开枪,自己人!",
@ -171,6 +171,13 @@ Fk:loadTranslationTable({
["$fastchat_m14"] = "哥们,给力点行吗?", ["$fastchat_m14"] = "哥们,给力点行吗?",
["$fastchat_m15"] = "哥哥,交个朋友吧。", ["$fastchat_m15"] = "哥哥,交个朋友吧。",
["$fastchat_m16"] = "妹子,交个朋友吧。", ["$fastchat_m16"] = "妹子,交个朋友吧。",
["$fastchat_m17"] = "我从未见过如此厚颜无耻之人!",
["$fastchat_m18"] = "你随便杀,闪不了算我输。",
["$fastchat_m19"] = "这波,不亏。",
["$fastchat_m20"] = "请收下我的膝盖。",
["$fastchat_m21"] = "你咋不上天呢?",
["$fastchat_m22"] = "放开我的队友,冲我来。",
["$fastchat_m23"] = "见证奇迹的时刻到了。",
["$fastchat_f1"] = "能不能快一点啊,兵贵神速啊。", ["$fastchat_f1"] = "能不能快一点啊,兵贵神速啊。",
["$fastchat_f2"] = "主公,别开枪,自己人!", ["$fastchat_f2"] = "主公,别开枪,自己人!",
["$fastchat_f3"] = "小内再不跳,后面还怎么玩啊?", ["$fastchat_f3"] = "小内再不跳,后面还怎么玩啊?",
@ -187,6 +194,13 @@ Fk:loadTranslationTable({
["$fastchat_f14"] = "哥们,给力点行吗?", ["$fastchat_f14"] = "哥们,给力点行吗?",
["$fastchat_f15"] = "哥,交个朋友吧。", ["$fastchat_f15"] = "哥,交个朋友吧。",
["$fastchat_f16"] = "妹子,交个朋友吧。", ["$fastchat_f16"] = "妹子,交个朋友吧。",
["$fastchat_f17"] = "我从未见过如此厚颜无耻之人!",
["$fastchat_f18"] = "你随便杀,闪不了算我输。",
["$fastchat_f19"] = "这波,不亏。",
["$fastchat_f20"] = "请收下我的膝盖。",
["$fastchat_f21"] = "你咋不上天呢?",
["$fastchat_f22"] = "放开我的队友,冲我来。",
["$fastchat_f23"] = "见证奇迹的时刻到了。",
["aaa_role_mode"] = "Role mode", ["aaa_role_mode"] = "Role mode",
[":aaa_role_mode"] = [[ [":aaa_role_mode"] = [[

View File

@ -1,4 +1,6 @@
-- SPDX-License-Identifier: GPL-3.0-or-later -- SPDX-License-Identifier: GPL-3.0-or-later
dofile "packages/standard/i18n/zh_CN.lua" local pkgprefix = "packages/"
dofile "packages/standard/i18n/en_US.lua" if UsingNewCore then pkgprefix = "packages/freekill-core/" end
dofile(pkgprefix .. "standard/i18n/zh_CN.lua")
dofile(pkgprefix .. "standard/i18n/en_US.lua")

View File

@ -359,9 +359,7 @@ Fk:loadTranslationTable{
___ ___
https://sgs.52pk.com/zl/201205/5299813.shtml
___ ___
@ -518,6 +516,8 @@ ___
2 2
https://sgs.52pk.com/zl/201205/5299813.shtml
]========================================], ]========================================],
} }

View File

@ -2,9 +2,12 @@
local extension = Package:new("standard") local extension = Package:new("standard")
extension.metadata = require "packages.standard.metadata" extension.metadata = require "packages.standard.metadata"
dofile "packages/standard/game_rule.lua"
dofile "packages/standard/aux_skills.lua" local pkgprefix = "packages/"
dofile "packages/standard/aux_poxi.lua" if UsingNewCore then pkgprefix = "packages/freekill-core/" end
dofile(pkgprefix .. "standard/game_rule.lua")
dofile(pkgprefix .. "standard/aux_skills.lua")
dofile(pkgprefix .. "standard/aux_poxi.lua")
Fk:appendKingdomMap("god", {"wei", "shu", "wu", "qun"}) Fk:appendKingdomMap("god", {"wei", "shu", "wu", "qun"})
@ -162,15 +165,15 @@ local tuxi = fk.CreateTriggerSkill{
local result = room:askForChoosePlayers(player, targets, 1, 2, "#tuxi-ask", self.name) local result = room:askForChoosePlayers(player, targets, 1, 2, "#tuxi-ask", self.name)
if #result > 0 then if #result > 0 then
self.cost_data = result room:sortPlayersByAction(result)
self.cost_data = {tos = result}
return true return true
end 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
room:sortPlayersByAction(self.cost_data) for _, id in ipairs(self.cost_data.tos) do
for _, id in ipairs(self.cost_data) do if player.dead then break end
if player.dead then return end
local p = room:getPlayerById(id) local p = room:getPlayerById(id)
if not p.dead and not p:isKongcheng() then if not p.dead and not p:isKongcheng() then
local c = room:askForCardChosen(player, p, "h", self.name) local c = room:askForCardChosen(player, p, "h", self.name)
@ -1368,6 +1371,6 @@ Fk:loadTranslationTable{
} }
-- load translations of this package -- load translations of this package
dofile "packages/standard/i18n/init.lua" dofile(pkgprefix .. "standard/i18n/init.lua")
return extension return extension

View File

@ -1,4 +1,6 @@
-- SPDX-License-Identifier: GPL-3.0-or-later -- SPDX-License-Identifier: GPL-3.0-or-later
dofile "packages/standard_cards/i18n/zh_CN.lua" local pkgprefix = "packages/"
dofile "packages/standard_cards/i18n/en_US.lua" if UsingNewCore then pkgprefix = "packages/freekill-core/" end
dofile(pkgprefix .. "standard_cards/i18n/zh_CN.lua")
dofile(pkgprefix .. "standard_cards/i18n/en_US.lua")

View File

@ -305,7 +305,7 @@ local duelSkill = fk.CreateActiveSkill{
local cardResponded local cardResponded
for i = 1, loopTimes do for i = 1, loopTimes do
cardResponded = room:askForResponse(currentResponser, 'slash', nil, nil, false, nil, effect) cardResponded = room:askForResponse(currentResponser, 'slash', nil, nil, true, nil, effect)
if cardResponded then if cardResponded then
room:responseCard({ room:responseCard({
from = currentResponser.id, from = currentResponser.id,
@ -495,7 +495,7 @@ local savageAssaultSkill = fk.CreateActiveSkill{
return user ~= to_select return user ~= to_select
end, end,
on_effect = function(self, room, effect) on_effect = function(self, room, effect)
local cardResponded = room:askForResponse(room:getPlayerById(effect.to), 'slash', nil, nil, false, nil, effect) local cardResponded = room:askForResponse(room:getPlayerById(effect.to), 'slash', nil, nil, true, nil, effect)
if cardResponded then if cardResponded then
room:responseCard({ room:responseCard({
@ -539,7 +539,7 @@ local archeryAttackSkill = fk.CreateActiveSkill{
return user ~= to_select return user ~= to_select
end, end,
on_effect = function(self, room, effect) on_effect = function(self, room, effect)
local cardResponded = room:askForResponse(room:getPlayerById(effect.to), 'jink', nil, nil, false, nil, effect) local cardResponded = room:askForResponse(room:getPlayerById(effect.to), 'jink', nil, nil, true, nil, effect)
if cardResponded then if cardResponded then
room:responseCard({ room:responseCard({
@ -631,6 +631,7 @@ local amazingGraceSkill = fk.CreateActiveSkill{
use.extra_data = use.extra_data or {} use.extra_data = use.extra_data or {}
use.extra_data.AGFilled = toDisplay use.extra_data.AGFilled = toDisplay
use.extra_data.AGResult = {}
else else
if use.extra_data and use.extra_data.AGFilled then if use.extra_data and use.extra_data.AGFilled then
table.forEach(room.players, function(p) table.forEach(room.players, function(p)
@ -661,7 +662,8 @@ local amazingGraceSkill = fk.CreateActiveSkill{
local chosen = room:askForAG(to, effect.extra_data.AGFilled, false, self.name) local chosen = room:askForAG(to, effect.extra_data.AGFilled, false, self.name)
room:takeAG(to, chosen, room.players) room:takeAG(to, chosen, room.players)
room:obtainCard(effect.to, chosen, true, fk.ReasonPrey) table.insert(effect.extra_data.AGResult, {effect.to, chosen})
room:moveCardTo(chosen, Card.PlayerHand, effect.to, fk.ReasonPrey, self.name, nil, true, effect.to)
table.removeOne(effect.extra_data.AGFilled, chosen) table.removeOne(effect.extra_data.AGFilled, chosen)
end end
} }
@ -705,7 +707,8 @@ local lightningSkill = fk.CreateActiveSkill{
to = to, to = to,
damage = 3, damage = 3,
card = effect.card, card = effect.card,
damageType = fk.ThunderDamage, -- damageType = fk.ThunderDamage,
damageType = Fk:getDamageNature(fk.ThunderDamage) and fk.ThunderDamage or fk.NormalDamage,
skillName = self.name, skillName = self.name,
} }
@ -1048,17 +1051,20 @@ local axeSkill = fk.CreateTriggerSkill{
attached_equip = "axe", attached_equip = "axe",
events = {fk.CardEffectCancelledOut}, events = {fk.CardEffectCancelledOut},
can_trigger = function(self, event, target, player, data) can_trigger = function(self, event, target, player, data)
return player:hasSkill(self) and data.from == player.id and data.card.trueName == "slash" and not player.room:getPlayerById(data.to).dead return player:hasSkill(self) and data.from == player.id and data.card.trueName == "slash" and
not player.room:getPlayerById(data.to).dead
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 pattern local cards = {}
if player:getEquipment(Card.SubtypeWeapon) then for _, id in ipairs(player:getCardIds("he")) do
pattern = ".|.|.|.|.|.|^"..tostring(player:getEquipment(Card.SubtypeWeapon)) if not player:prohibitDiscard(id) and
else not (table.contains(player:getEquipments(Card.SubtypeWeapon), id) and Fk:getCardById(id).name == "axe") then
pattern = "." table.insert(cards, id)
end end
local cards = room:askForDiscard(player, 2, 2, true, self.name, true, pattern, "#axe-invoke::"..data.to, true) end
cards = room:askForDiscard(player, 2, 2, true, self.name, true, ".|.|.|.|.|.|"..table.concat(cards, ","),
"#axe-invoke::"..data.to, true)
if #cards > 0 then if #cards > 0 then
self.cost_data = cards self.cost_data = cards
return true return true
@ -1248,84 +1254,134 @@ extension:addCards({
niohShield, niohShield,
}) })
local horseSkill = fk.CreateDistanceSkill{ local diluSkill = fk.CreateDistanceSkill{
name = "horse_skill", name = "#dilu_skill",
global = true, attached_equip = "dilu",
correct_func = function(self, from, to) correct_func = function(self, from, to)
local ret = 0 if to:hasSkill(self) then
if from:getEquipment(Card.SubtypeOffensiveRide) then return 1
ret = ret - 1
end end
if to:getEquipment(Card.SubtypeDefensiveRide) then
ret = ret + 1
end
return ret
end, end,
} }
if not Fk.skills["horse_skill"] then Fk:addSkill(diluSkill)
Fk:addSkill(horseSkill)
end
local diLu = fk.CreateDefensiveRide{ local diLu = fk.CreateDefensiveRide{
name = "dilu", name = "dilu",
suit = Card.Club, suit = Card.Club,
number = 5, number = 5,
equip_skill = diluSkill,
} }
extension:addCards({ extension:addCards({
diLu, diLu,
}) })
local jueyingSkill = fk.CreateDistanceSkill{
name = "#jueying_skill",
attached_equip = "jueying",
correct_func = function(self, from, to)
if to:hasSkill(self) then
return 1
end
end,
}
Fk:addSkill(jueyingSkill)
local jueYing = fk.CreateDefensiveRide{ local jueYing = fk.CreateDefensiveRide{
name = "jueying", name = "jueying",
suit = Card.Spade, suit = Card.Spade,
number = 5, number = 5,
equip_skill = jueyingSkill,
} }
extension:addCards({ extension:addCards({
jueYing, jueYing,
}) })
local zhuahuangfeidianSkill = fk.CreateDistanceSkill{
name = "#zhuahuangfeidian_skill",
attached_equip = "zhuahuangfeidian",
correct_func = function(self, from, to)
if to:hasSkill(self) then
return 1
end
end,
}
Fk:addSkill(zhuahuangfeidianSkill)
local zhuaHuangFeiDian = fk.CreateDefensiveRide{ local zhuaHuangFeiDian = fk.CreateDefensiveRide{
name = "zhuahuangfeidian", name = "zhuahuangfeidian",
suit = Card.Heart, suit = Card.Heart,
number = 13, number = 13,
equip_skill = zhuahuangfeidianSkill,
} }
extension:addCards({ extension:addCards({
zhuaHuangFeiDian, zhuaHuangFeiDian,
}) })
local chituSkill = fk.CreateDistanceSkill{
name = "#chitu_skill",
attached_equip = "chitu",
correct_func = function(self, from, to)
if from:hasSkill(self) then
return -1
end
end,
}
Fk:addSkill(chituSkill)
local chiTu = fk.CreateOffensiveRide{ local chiTu = fk.CreateOffensiveRide{
name = "chitu", name = "chitu",
suit = Card.Heart, suit = Card.Heart,
number = 5, number = 5,
equip_skill = chituSkill,
} }
extension:addCards({ extension:addCards({
chiTu, chiTu,
}) })
local dayuanSkill = fk.CreateDistanceSkill{
name = "#dayuan_skill",
attached_equip = "dayuan",
correct_func = function(self, from, to)
if from:hasSkill(self) then
return -1
end
end,
}
Fk:addSkill(dayuanSkill)
local daYuan = fk.CreateOffensiveRide{ local daYuan = fk.CreateOffensiveRide{
name = "dayuan", name = "dayuan",
suit = Card.Spade, suit = Card.Spade,
number = 13, number = 13,
equip_skill = dayuanSkill,
} }
extension:addCards({ extension:addCards({
daYuan, daYuan,
}) })
local zixingSkill = fk.CreateDistanceSkill{
name = "#zixing_skill",
attached_equip = "zixing",
correct_func = function(self, from, to)
if from:hasSkill(self) then
return -1
end
end,
}
Fk:addSkill(zixingSkill)
local ziXing = fk.CreateOffensiveRide{ local ziXing = fk.CreateOffensiveRide{
name = "zixing", name = "zixing",
suit = Card.Diamond, suit = Card.Diamond,
number = 13, number = 13,
equip_skill = zixingSkill,
} }
extension:addCards({ extension:addCards({
ziXing, ziXing,
}) })
dofile "packages/standard_cards/i18n/init.lua" local pkgprefix = "packages/"
if UsingNewCore then pkgprefix = "packages/freekill-core/" end
dofile(pkgprefix .. "standard_cards/i18n/init.lua")
return extension return extension

View File

@ -1,5 +1,21 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
file(GLOB SWIG_FILES "${PROJECT_SOURCE_DIR}/src/swig/*.i")
if (DEFINED FK_SERVER_ONLY)
set(SWIG_SOURCE ${PROJECT_SOURCE_DIR}/src/swig/freekill-nogui.i)
else ()
set(SWIG_SOURCE ${PROJECT_SOURCE_DIR}/src/swig/freekill.i)
endif ()
add_custom_command(
OUTPUT ${PROJECT_SOURCE_DIR}/src/swig/freekill-wrap.cxx
DEPENDS ${SWIG_FILES}
COMMENT "Generating freekill-wrap.cxx"
COMMAND swig -c++ -lua -Wall -o
${PROJECT_SOURCE_DIR}/src/swig/freekill-wrap.cxx
${SWIG_SOURCE}
)
set(freekill_SRCS set(freekill_SRCS
# "main.cpp" # "main.cpp"
"freekill.cpp" "freekill.cpp"
@ -21,6 +37,8 @@ set(freekill_SRCS
"ui/qmlbackend.cpp" "ui/qmlbackend.cpp"
"swig/freekill-wrap.cxx" "swig/freekill-wrap.cxx"
) )
set_source_files_properties(
"swig/freekill-wrap.cxx" PROPERTIES GENERATED TRUE)
if (NOT DEFINED FK_SERVER_ONLY) if (NOT DEFINED FK_SERVER_ONLY)
list(APPEND freekill_SRCS list(APPEND freekill_SRCS

View File

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// 为了写测试而特意给程序本身单独分出一个main.cpp 顺便包含项目文档(这样真的好吗) // 为了写测试而特意给程序本身单独分出一个main.cpp 顺便包含项目文档(这样真的好吗)
#include "freekill.h" int freekill_main(int argc, char **argv);
int main(int argc, char **argv) { int main(int argc, char **argv) {
return freekill_main(argc, argv); return freekill_main(argc, argv);
} }