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 "*.h" -exec sed -i '1i #include "pch.h"' "{}" \;
sed -i '1d' pch.h
sed -i '1d' main.cpp
sed -i '/pch.h/d' CMakeLists.txt
- name: Configure CMake Project

View File

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

View File

@ -6,7 +6,7 @@
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}\")
find_package(Qt6 REQUIRED COMPONENTS
@ -40,22 +40,6 @@ include_directories(include)
include_directories(include/libgit2)
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)
if (NOT DEFINED FK_SERVER_ONLY)

View File

@ -3,8 +3,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.notify.FreeKill"
android:installLocation="preferExternal"
android:versionCode="419"
android:versionName="0.4.19">
android:versionCode="420"
android:versionName="0.4.20">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<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 observers ClientPlayer[] @ 观察者的数组
---@field public current ClientPlayer @ 当前回合玩家
---@field public discard_pile integer[] @ 弃牌堆
---@field public observing boolean
---@field public record any
---@field public last_update_ui integer @ 上次刷新状态技UI的时间
@ -35,7 +34,7 @@ local no_decode_commands = {
function Client:initialize()
AbstractRoom.initialize(self)
self.client = fk.ClientInstance
self.notifyUI = function(self, command, data)
self.notifyUI = function(_, command, data)
fk.Backend:notifyUI(command, data)
end
self.client.callback = function(_self, command, jsonData, isRequest)
@ -83,12 +82,11 @@ function Client:initialize()
for _, cid in ipairs(Self:getCardIds("h")) do
self:notifyUI("UpdateCard", cid)
end
-- 刷技能状态
self:notifyUI("UpdateSkill", nil)
end
end
self.discard_pile = {}
self._processing = {}
self.disabled_packs = {}
self.disabled_generals = {}
-- self.last_update_ui = os.getms()
@ -106,79 +104,20 @@ function Client:getPlayerById(id)
return nil
end
---@param cardId integer | Card
---@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
---@param moves CardsMoveStruct[]
function Client:moveCards(moves)
for _, move in ipairs(moves) do
if move.from and move.fromArea then
local from = self:getPlayerById(move.from)
if move.fromArea == Card.PlayerHand and not Self:isBuddy(self:getPlayerById(move.from)) then
for _ = 1, #move.ids do
table.remove(from.player_cards[Player.Hand])
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
for _, data in ipairs(moves) do
if #data.moveInfo > 0 then
for _, info in ipairs(data.moveInfo) do
self:applyMoveInfo(data, info)
Fk:filterCard(info.cardId, self:getPlayerById(data.to))
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
---@param msg LogMessage
local function parseMsg(msg, nocolor)
local function parseMsg(msg, nocolor, visible_data)
local self = ClientInstance
local data = msg
local function getPlayerStr(pid, color)
@ -218,7 +157,9 @@ local function parseMsg(msg, nocolor)
local allUnknown = true
local unknownCount = 0
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
else
unknownCount = unknownCount + 1
@ -230,11 +171,15 @@ local function parseMsg(msg, nocolor)
else
local card_str = {}
for _, id in ipairs(card) do
table.insert(card_str, Fk:getCardById(id, true):toLogString())
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())
end
end
if unknownCount > 0 then
table.insert(card_str, Fk:translate("unknown_card")
.. unknownCount == 1 and "x" .. unknownCount or "")
local suffix = unknownCount > 1 and ("x" .. unknownCount) or ""
table.insert(card_str, Fk:translate("unknown_card") .. suffix)
end
card = table.concat(card_str, ", ")
end
@ -261,8 +206,8 @@ local function parseMsg(msg, nocolor)
end
---@param msg LogMessage
function Client:appendLog(msg)
local text = parseMsg(msg)
function Client:appendLog(msg, visible_data)
local text = parseMsg(msg, nil, visible_data)
self:notifyUI("GameLog", text)
if msg.toast then
self:notifyUI("ShowToast", text)
@ -298,7 +243,10 @@ end
fk.client_callback["EnterRoom"] = function(_data)
Self = ClientPlayer:new(fk.Self)
-- 垃圾bug 怎么把这玩意忘了
local ob = ClientInstance.observing
ClientInstance = Client:new() -- clear old client data
ClientInstance.observing = ob
ClientInstance.players = {Self}
ClientInstance.alive_players = {Self}
ClientInstance.discard_pile = {}
@ -405,6 +353,14 @@ fk.client_callback["PropertyUpdate"] = function(data)
ClientInstance:notifyUI("PropertyUpdate", data)
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)
-- jsonData: [ int target_id, string flag, int reason ]
local id, flag, reason, prompt = data[1], data[2], data[3], data[4]
@ -557,7 +513,8 @@ local function mergeMoves(moves)
proposer = move.proposer,
}
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
for _, v in pairs(temp) do
table.insert(ret, v)
@ -565,109 +522,111 @@ local function mergeMoves(moves)
return ret
end
local function sendMoveCardLog(move)
local function sendMoveCardLog(move, visible_data)
local client = ClientInstance ---@class Client
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
if move.toArea == Card.PlayerHand then
if move.fromArea == Card.PlayerSpecial then
client:appendLog{
client:appendLog({
type = "$GetCardsFromPile",
from = move.to,
arg = move.fromSpecialName,
arg2 = #move.ids,
card = move.ids,
}
}, visible_data)
elseif move.fromArea == Card.DrawPile then
client:appendLog{
client:appendLog({
type = "$DrawCards",
from = move.to,
card = move.ids,
arg = #move.ids,
}
}, visible_data)
elseif move.fromArea == Card.Processing then
client:appendLog{
client:appendLog({
type = "$GotCardBack",
from = move.to,
card = move.ids,
arg = #move.ids,
}
}, visible_data)
elseif move.fromArea == Card.DiscardPile then
client:appendLog{
client:appendLog({
type = "$RecycleCard",
from = move.to,
card = move.ids,
arg = #move.ids,
}
}, visible_data)
elseif move.from then
client:appendLog{
client:appendLog({
type = "$MoveCards",
from = move.from,
to = { move.to },
arg = #move.ids,
card = move.ids,
}
}, visible_data)
else
client:appendLog{
client:appendLog({
type = "$PreyCardsFromPile",
from = move.to,
card = move.ids,
arg = #move.ids,
}
}, visible_data)
end
elseif move.toArea == Card.PlayerEquip then
client:appendLog{
client:appendLog({
type = "$InstallEquip",
from = move.to,
card = move.ids,
}
}, visible_data)
elseif move.toArea == Card.PlayerJudge then
if move.from ~= move.to and move.fromArea == Card.PlayerJudge then
client:appendLog{
client:appendLog({
type = "$LightningMove",
from = move.from,
to = { move.to },
card = move.ids,
}
}, visible_data)
elseif move.from then
client:appendLog{
client:appendLog({
type = "$PasteCard",
from = move.from,
to = { move.to },
card = move.ids,
}
}, visible_data)
end
elseif move.toArea == Card.PlayerSpecial then
client:appendLog{
client:appendLog({
type = "$AddToPile",
arg = move.specialName,
arg2 = #move.ids,
from = move.to,
card = move.ids,
}
}, visible_data)
elseif move.fromArea == Card.PlayerEquip then
client:appendLog{
client:appendLog({
type = "$UninstallEquip",
from = move.from,
card = move.ids,
}
}, visible_data)
elseif move.toArea == Card.Processing then
if move.fromArea == Card.DrawPile and (move.moveReason == fk.ReasonPut or move.moveReason == fk.ReasonJustMove) then
if hidden then
client:appendLog{
client:appendLog({
type = "$ViewCardFromDrawPile",
from = move.proposer,
arg = #move.ids,
}
}, visible_data)
else
client:appendLog{
client:appendLog({
type = "$TurnOverCardFromDrawPile",
from = move.proposer,
card = move.ids,
arg = #move.ids,
}
}, visible_data)
client:setCardNote(move.ids, {
type = "$$TurnOverCard",
from = move.proposer,
@ -676,12 +635,12 @@ local function sendMoveCardLog(move)
end
elseif move.from and move.toArea == Card.DrawPile then
msgtype = hidden and "$PutCard" or "$PutKnownCard"
client:appendLog{
client:appendLog({
type = msgtype,
from = move.from,
card = move.ids,
arg = #move.ids,
}
}, visible_data)
client:setCardNote(move.ids, {
type = "$$PutCard",
from = move.from,
@ -689,27 +648,27 @@ local function sendMoveCardLog(move)
elseif move.toArea == Card.DiscardPile then
if move.moveReason == fk.ReasonDiscard then
if move.proposer and move.proposer ~= move.from then
client:appendLog{
client:appendLog({
type = "$DiscardOther",
from = move.from,
to = {move.proposer},
card = move.ids,
arg = #move.ids,
}
}, visible_data)
else
client:appendLog{
client:appendLog({
type = "$DiscardCards",
from = move.from,
card = move.ids,
arg = #move.ids,
}
}, visible_data)
end
elseif move.moveReason == fk.ReasonPutIntoDiscardPile then
client:appendLog{
client:appendLog({
type = "$PutToDiscard",
card = move.ids,
arg = #move.ids,
}
}, visible_data)
end
-- elseif move.toArea == Card.Void then
-- nop
@ -724,14 +683,23 @@ local function sendMoveCardLog(move)
end
end
---@param raw_moves CardsMoveStruct[]
fk.client_callback["MoveCards"] = function(raw_moves)
-- 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)
ClientInstance:moveCards(separated)
local merged = mergeMoves(separated)
ClientInstance:notifyUI("MoveCards", merged)
visible_data.merged = merged
ClientInstance:notifyUI("MoveCards", visible_data)
for _, move in ipairs(merged) do
sendMoveCardLog(move)
sendMoveCardLog(move, visible_data)
end
end
@ -861,6 +829,17 @@ fk.client_callback["AddSkill"] = function(data)
updateLimitSkill(id, skill, target:usedSkillTimes(skill_name, Player.HistoryGame))
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)
-- jsonData: [ string skill_name, string prompt, bool cancelable. json extra_data ]
local skill = Fk.skills[data[1]]
@ -868,16 +847,44 @@ fk.client_callback["AskForUseActiveSkill"] = function(data)
skill._extra_data = extra_data
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)
end
fk.client_callback["AskForUseCard"] = function(data)
-- jsonData: card, pattern, prompt, cancelable, {}
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)
end
fk.client_callback["AskForResponseCard"] = function(data)
-- jsonData: card, pattern, prompt, cancelable, {}
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)
end
@ -894,7 +901,7 @@ fk.client_callback["SetPlayerMark"] = function(data)
local spec = Fk.qml_marks[mtype]
if spec then
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
ClientInstance:notifyUI("SetPlayerMark", data)
@ -1006,10 +1013,11 @@ fk.client_callback["Heartbeat"] = function()
end
fk.client_callback["ChangeSelf"] = function(data)
local p = ClientInstance:getPlayerById(data.id)
p.player_cards[Player.Hand] = data.handcards
p.special_cards = data.special_cards
ClientInstance:notifyUI("ChangeSelf", data.id)
local pid = tonumber(data)
local c = ClientInstance
c.client:changeSelf(pid) -- for qml
Self = c:getPlayerById(pid)
ClientInstance:notifyUI("ChangeSelf", pid)
end
fk.client_callback["UpdateQuestSkillUI"] = function(data)
@ -1111,161 +1119,60 @@ end
fk.client_callback["PrintCard"] = function(data)
local n, s, num = table.unpack(data)
local cd = Fk:cloneCard(n, s, num)
Fk:_addPrintedCard(cd)
ClientInstance:printCard(n, s, num)
end
fk.client_callback["AddBuddy"] = function(data)
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)
Self:addBuddy(to)
to.player_cards[Player.Hand] = hand
from:addBuddy(to)
end
fk.client_callback["RmBuddy"] = function(data)
local c = ClientInstance
local id = data
local fromid, id = table.unpack(data)
local from = c:getPlayerById(fromid)
local to = c:getPlayerById(id)
Self: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
from:removeBuddy(to)
end
local function loadRoomSummary(data)
local players = data.p
local players = data.players
fk.client_callback["StartGame"]("")
for _, pid in ipairs(data.circle) do
if pid ~= data.you then
fk.client_callback["AddPlayer"](players[tostring(pid)].d)
fk.client_callback["AddPlayer"](players[tostring(pid)].setup_data)
end
end
fk.client_callback["ArrangeSeats"](data.circle)
for _, d in ipairs(data.pc) do
local cd = Fk:cloneCard(table.unpack(d))
Fk:_addPrintedCard(cd)
end
ClientInstance:loadJsonObject(data) -- 此处已同步全部数据 剩下就是更新UI
for cid, marks in pairs(data.cm) do
for k, v in pairs(marks) do
Fk:getCardById(tonumber(cid)):setMark(k, v)
ClientInstance:notifyUI("UpdateCard", cid)
for k, v in pairs(ClientInstance.banners) do
if k[1] == "@" then
ClientInstance:notifyUI("SetBanner", { k, v })
end
end
for k, v in pairs(data.b) do
fk.client_callback["SetBanner"]{ k, v }
end
for _, p in ipairs(ClientInstance.players) do p:sendDataToUI() end
for _, pid in ipairs(data.circle) do
local pdata = data.p[tostring(pid)]
loadPlayerSummary(pdata)
end
ClientInstance:notifyUI("UpdateDrawPile", data.dp)
ClientInstance:notifyUI("UpdateRoundNum", data.rnd)
ClientInstance:notifyUI("UpdateDrawPile", #ClientInstance.draw_pile)
ClientInstance:notifyUI("UpdateRoundNum", data.round_count)
end
fk.client_callback["Reconnect"] = function(data)
local players = data.p
local setup_data = players[tostring(data.you)].d
local players = data.players
local setup_data = players[tostring(data.you)].setup_data
setup(setup_data[1], setup_data[2], setup_data[3])
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)
fk.client_callback["EnterLobby"]("")
fk.client_callback["EnterRoom"](enter_room_data)
@ -1274,19 +1181,28 @@ fk.client_callback["Reconnect"] = function(data)
end
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])
local enter_room_data = data.d
local enter_room_data = { data.timeout, data.settings }
table.insert(enter_room_data, 1, #data.circle)
fk.client_callback["EnterRoom"](enter_room_data)
fk.client_callback["StartGame"]("")
loadRoomSummary(data)
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)
ClientInstance = Client:new()
dofile "lua/client/client_util.lua"

View File

@ -37,31 +37,13 @@ function GetGeneralDetail(name)
deputyMaxHp = general.deputyMaxHpAdjustedValue,
gender = general.gender,
skill = {},
related_skill = {},
companions = general.companions
}
for _, s in ipairs(general.skills) do
for _, s in ipairs(general.all_skills) do
table.insert(ret.skill, {
name = s.name,
description = Fk:getDescription(s.name)
})
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)
name = s[1],
description = Fk:getDescription(s[1]),
is_related_skill = s[2],
})
end
for _, g in pairs(Fk.generals) do
@ -114,7 +96,8 @@ function GetCardData(id, virtualCardForm)
color = card:getColorString(),
mark = mark,
type = card.type,
subtype = cardSubtypeStrings[card.sub_type]
subtype = cardSubtypeStrings[card.sub_type],
known = Self:cardVisible(id)
}
if card.skillName ~= "" then
local orig = Fk:getCardById(id, true)
@ -352,6 +335,49 @@ function CanUseCardToTarget(card, to_select, selected, extra_data_str)
return ret
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 to_select integer @ id of a card not selected
---@param selected_targets integer[] @ ids of selected players
@ -430,6 +456,18 @@ function GetSkillData(skill_name)
}
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)
local extra_data = extra_data_str == "" and nil or json.decode(extra_data_str)
local skill = Fk.skills[skill_name]
@ -505,6 +543,46 @@ function ActiveTargetFilter(skill_name, to_select, selected, selected_cards, ext
return ret
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)
local skill = Fk.skills[skill_name]
local ret = false
@ -654,12 +732,6 @@ function SetInteractionDataOfSkill(skill_name, data)
end
end
function ChangeSelf(pid)
local c = ClientInstance
c.client:changeSelf(pid) -- for qml
Self = c:getPlayerById(pid)
end
function GetPlayerHandcards(pid)
local c = ClientInstance
local p = c:getPlayerById(pid)
@ -767,6 +839,10 @@ function GetCardProhibitReason(cid, method, pattern)
end
end
function CanSortHandcards(pid)
return ClientInstance:getPlayerById(pid):getMark(MarkEnum.SortProhibited) == 0
end
function PoxiPrompt(poxi_type, data, extra_data)
local poxi = Fk.poxi_methods[poxi_type]
if not poxi or not poxi.prompt then return "" end
@ -810,4 +886,91 @@ function ReloadPackage(path)
Fk:reloadPackage(path)
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"

View File

@ -2,16 +2,84 @@
---@class ClientPlayer: Player
---@field public player fk.Player
---@field public known_cards integer[]
---@field public global_known_cards integer[]
local ClientPlayer = Player:subclass("ClientPlayer")
function ClientPlayer:initialize(cp)
Player.initialize(self)
self.id = cp:getId()
self.player = cp
self.known_cards = {} -- you know he/she have this card, but not shown
self.global_known_cards = {} -- card that visible to all players
end
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
return ClientPlayer

View File

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

View File

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

View File

@ -29,10 +29,13 @@
---@field public printed_cards table<integer, Card> @ 被某些房间现场打印的卡牌id都是负数且从-2开始
---@field private kingdoms string[] @ 总势力
---@field private kingdom_map table<string, string[]> @ 势力映射表
---@field private damage_nature table<any, table> @ 伤害映射表
---@field private _custom_events any[] @ 自定义事件列表
---@field public poxi_methods table<string, PoxiSpec> @ “魄袭”框操作方法表
---@field public qml_marks table<string, QmlMarkSpec> @ 自定义Qml标记的表
---@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")
--- Engine的构造函数。
@ -69,13 +72,18 @@ function Engine:initialize()
self.game_mode_disabled = {}
self.kingdoms = {}
self.kingdom_map = {}
self.damage_nature = { [fk.NormalDamage] = { "normal_damage", false } }
self._custom_events = {}
self.poxi_methods = {}
self.qml_marks = {}
self.mini_games = {}
self.request_handlers = {}
self.target_tips = {}
self:loadPackages()
self:setLords()
self:loadDisabled()
self:loadRequestHandlers()
self:addSkills(AuxSkills)
end
@ -201,15 +209,14 @@ end
--- 标包和标准卡牌包比较特殊,它们永远会在第一个加载。
---@return nil
function Engine:loadPackages()
local new_core = false
if FileIO.pwd():endsWith("packages/freekill-core") then
new_core = true
UsingNewCore = true
FileIO.cd("../..")
end
local directories = FileIO.ls("packages")
-- 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_cards"))
self:loadPackage(require("packages.freekill-core.maneuvering"))
@ -248,7 +255,7 @@ function Engine:loadPackages()
end
end
if new_core then
if UsingNewCore then
FileIO.cd("packages/freekill-core")
end
end
@ -269,6 +276,15 @@ function Engine:loadDisabled()
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 lang? string @ 目标语言默认为zh_CN
@ -349,9 +365,9 @@ function Engine:addGeneral(general)
table.insert(self.same_generals[tName], general.name)
end
if table.find(general.skills, function(s) return s.lordSkill end) then
table.insert(self.lords, general.name)
end
-- if table.find(general.skills, function(s) return s.lordSkill end) then
-- table.insert(self.lords, general.name)
-- end
end
--- 加载一系列武将。
@ -363,6 +379,19 @@ function Engine:addGenerals(generals)
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
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 @ 武将名
function Engine:canUseGeneral(g)
@ -504,6 +577,15 @@ function Engine:addMiniGame(spec)
self.mini_games[spec.name] = spec
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 data any @ 随意目前只用到JudgeStruct为了影响判定牌
function Engine:filterCard(id, player, data)
if player == nil then
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
return Fk:currentRoom():filterCard(id, player, data)
end
--- 获知当前的Engine是跑在服务端还是客户端并返回相应的实例。

View File

@ -22,6 +22,7 @@
---@field public other_skills string[] @ 武将身上属于其他武将的技能,通过字符串调用
---@field public related_skills Skill[] @ 武将相关的不属于其他武将的技能,例如邓艾的急袭
---@field public related_other_skills string [] @ 武将相关的属于其他武将的技能,例如孙策的英姿
---@field public all_skills table @ 武将的所有技能,包括相关技能和属于其他武将的技能
---@field public companions string [] @ 有珠联璧合关系的武将
---@field public 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.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.all_skills = {}
self.companions = {}
@ -75,8 +77,10 @@ end
function General:addSkill(skill)
if (type(skill) == "string") then
table.insert(self.other_skills, skill)
table.insert(self.all_skills, {skill, false})
elseif (skill.class and skill.class:isSubclassOf(Skill)) then
table.insert(self.skills, skill)
table.insert(self.all_skills, {skill.name, false})
skill.package = self.package
end
end
@ -86,8 +90,10 @@ end
function General:addRelatedSkill(skill)
if (type(skill) == "string") then
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
table.insert(self.related_skills, skill)
table.insert(self.all_skills, {skill.name, true}) -- only for UI
Fk:addSkill(skill)
skill.package = self.package
end

View File

@ -13,6 +13,7 @@
---@field public shield integer @ 护甲数
---@field public kingdom string @ 势力
---@field public role string @ 身份
---@field public role_shown boolean
---@field public general string @ 武将
---@field public deputyGeneral string @ 副将
---@field public gender integer @ 性别
@ -71,6 +72,11 @@ Player.JudgeSlot = 'JudgeSlot'
--- 构造函数。总之这不是随便调用的函数
function Player:initialize()
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.maxHp = 0
self.kingdom = "qun"
@ -98,8 +104,8 @@ function Player:initialize()
[Player.Equip] = {},
[Player.Judge] = {},
}
self.virtual_equips = {}
self.special_cards = {}
self.virtual_equips = {}
self.equipSlots = {
Player.WeaponSlot,
@ -221,6 +227,15 @@ function Player:getMark(mark)
return mark
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。
function Player:getMarkNames()
local ret = {}
@ -623,7 +638,7 @@ function Player:getNextAlive(ignoreRemoved, num, ignoreRest)
num = num or 1
for _ = 1, num do
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
end
end
@ -1179,6 +1194,71 @@ function Player:isBuddy(other)
return self.id == id or table.contains(self.buddy_list, id)
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 diff? bool @ 比较二者不同
@ -1204,4 +1284,50 @@ function Player:isFemale()
return self.gender == General.Female or self.gender == General.Bigender
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

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 relate_to_place string @ 主将技/副将技
---@field public switchSkillName string @ 转换技名字
---@field public times integer @ 技能剩余次数,负数不显示,正数显示
local Skill = class("Skill")
---@alias Frequency integer
@ -112,7 +113,7 @@ end
---@param player Player @ 玩家
---@return boolean
function Skill:isEffectable(player)
if self.cardSkill then
if self.cardSkill or self.permanent_skill then
return true
end
@ -144,4 +145,15 @@ function Skill:isPlayerSkill(player)
return not (self:isEquipmentSkill(player) or self.name:endsWith("&"))
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

View File

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

View File

@ -40,4 +40,13 @@ function TargetModSkill:getExtraTargetNum(player, card_skill, card)
return 0
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

View File

@ -72,9 +72,17 @@ function TriggerSkill:doCost(event, target, player, data)
self.cost_data = cost_data_bak
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 self:use(event, target, player, data)
end)
end, skill_data)
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({}, {
__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 = {
"__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
-- 阶段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
--- 全局卡牌(包括自己)的canUse
@ -249,14 +273,16 @@ function table:contains(element)
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 math.random() < 0.5 then
if rnd:random() < 0.5 then
self[1], self[2] = self[2], self[1]
end
else
for i = #self, 2, -1 do
local j = math.random(i)
local j = rnd:random(i)
self[i], self[j] = self[j], self[i]
end
end
@ -414,6 +440,17 @@ function table:assign(targetTbl)
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)
return next(t) == nil
end

View File

@ -18,6 +18,7 @@ MaxCardsSkill = require "core.skill_type.max_cards"
TargetModSkill = require "core.skill_type.target_mod"
FilterSkill = require "core.skill_type.filter"
InvaliditySkill = require "lua.core.skill_type.invalidity"
VisibilitySkill = require "lua.core.skill_type.visibility"
BasicCard = require "core.card_type.basic"
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.expand_pile = spec.expand_pile
skill.times = spec.times or skill.times
end
local function readStatusSpecToSkill(skill, spec)
@ -85,6 +87,7 @@ end
---@field public max_turn_use_time? integer
---@field public max_round_use_time? integer
---@field public max_game_use_time? integer
---@field public times? integer | fun(self: UsableSkill): integer
---@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_nullified then skill.onNullified = spec.on_nullified 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
skill.interaction = setmetatable({}, {
@ -237,7 +241,7 @@ end
---@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 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
---@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 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 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
---@return TargetModSkill
@ -415,6 +420,9 @@ function fk.CreateTargetModSkill(spec)
if spec.extra_target_func then
skill.getExtraTargetNum = spec.extra_target_func
end
if spec.target_tip_func then
skill.getTargetTip = spec.target_tip_func
end
return skill
end
@ -453,6 +461,22 @@ function fk.CreateInvaliditySkill(spec)
return skill
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
---@field public skill? Skill
---@field public equip_skill? Skill
@ -663,3 +687,11 @@ end
---@field qml_path string | fun(player: Player, data: any): string
---@field update_func? fun(player: ServerPlayer, data: 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"
Player = require "core.player"
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"
-- 读取配置文件。

View File

@ -244,6 +244,9 @@ local function where(info, context_lines)
source = {}
local filename = info.source:match("@(.*)")
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)
elseif info.source then
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
function Object:isSubclassOf(class) end
function Object:include(e) end
---@class json
json = {}

View File

@ -34,11 +34,12 @@ end
function AI:makeReply()
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 to_delay = 500 - (os.getms() - start) / 1000
if ret == "" then ret = "__cancel" end
-- local to_delay = 500 - (os.getms() - start) / 1000
-- print(to_delay)
self.room:delay(to_delay)
-- self.room:delay(to_delay)
return ret
end

View File

@ -6,75 +6,118 @@ local RandomAI = AI:subclass("RandomAI")
---@param self RandomAI
---@param skill ActiveSkill
---@param card? Card
function RandomAI:useActiveSkill(skill, card)
---@param extra_data? table
function RandomAI:useActiveSkill(skill, card, extra_data)
local room = self.room
local player = self.player
extra_data = extra_data or Util.DummyTable
if skill:isInstanceOf(ViewAsSkill) then return "" end
local filter_func = skill.cardFilter
if card then
filter_func = Util.FalseFunc
if self.command == "PlayCard" and (not skill:canUse(player, card) or (card and player:prohibitUse(card))) then
return ""
end
if self.command == "PlayCard" and (not skill:canUse(player, card) or player:prohibitUse(card)) then
return ""
local interaction_data
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
local max_try_times = 100
local selected_targets = {}
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
table.insertIfNeed(selected_targets, table.random(avail_targets))
table.insertIfNeed(selected_cards, table.random(avail_cards))
-- TODO: ng that 'must_targets' & 'exclusive_targets' should be rebuilt later
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
if skill:feasible(selected_targets, selected_cards, self.player, card) then
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{
card = card and card.id or json.encode{
skill = skill.name,
subcards = selected_cards,
},
targets = selected_targets,
interaction_data = interaction_data,
}
-- print(ret)
return ret
end
return ""
end
---@param self RandomAI
---@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 room = self.room
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)
if not precondition then return nil end
if not precondition then return "" end
local exp = Exppattern:Parse(skill.pattern)
local cnames = {}
for _, m in ipairs(exp.matchers) do
@ -82,35 +125,107 @@ function RandomAI:useVSSkill(skill, pattern, cancelable, extra_data)
end
for _, n in ipairs(cnames) do
local c = Fk:cloneCard(n)
precondition = c.skill:canUse(Self, c)
precondition = c.skill:canUse(player, c, extra_data)
if precondition then break end
end
else
precondition = skill:enabledAtResponse(player)
if not precondition then return nil end
local exp = Exppattern:Parse(pattern)
precondition = exp:matchExp(skill.pattern)
precondition = skill:enabledAtResponse(player, cardResponsing) and Exppattern:Parse(pattern):matchExp(skill.pattern)
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_targets = {}
local card
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
local avail_cards = table.filter(player:getCardIds{ Player.Hand, Player.Equip }, function(id)
return skill:cardFilter(id, selected_cards)
card = skill:viewAs(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)
if #avail_cards == 0 then break end
table.insert(selected_cards, table.random(avail_cards))
if skill:viewAs(selected_cards) then
return {
skill = skill.name,
subcards = selected_cards,
end
if not card then return "" end
if cardResponsing then
if not player:prohibitResponse(card) then
return json.encode{
card = json.encode{
skill = skill.name,
subcards = selected_cards,
},
targets = {},
interaction_data = interaction_data,
}
end
return ""
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
---@type table<string, fun(self: RandomAI, jsonData: string): string>
@ -119,6 +234,7 @@ local random_cb = {}
random_cb["AskForUseActiveSkill"] = function(self, jsonData)
local data = json.decode(jsonData)
local skill = Fk.skills[data[1]]
if not skill then return "" end
local cancelable = data[3]
if cancelable and math.random() < 0.25 then return "" end
local extra_data = data[4]
@ -126,13 +242,47 @@ random_cb["AskForUseActiveSkill"] = function(self, jsonData)
skill[k] = v
end
if skill:isInstanceOf(ViewAsSkill) then
return RandomAI.useVSSkill(skill)
return self:useVSSkill(skill, nil, cancelable, extra_data)
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
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
random_cb["AskForUseCard"] = function(self, jsonData)
@ -140,61 +290,74 @@ random_cb["AskForUseCard"] = function(self, jsonData)
local data = json.decode(jsonData)
local card_name = data[1]
local pattern = data[2] or card_name
local cancelable = data[4] or true
local exp = Exppattern:Parse(pattern)
local prompt = data[3]
local cancelable = data[4]
local extra_data = data[5] or Util.DummyTable
local avail_cards = table.map(player:getCardIds("he"), Util.Id2CardMapper)
avail_cards = table.filter(avail_cards, function(c)
return exp:match(c) and not player:prohibitUse(c)
end)
if #avail_cards > 0 then
if math.random() < 0.25 then return "" end
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)
avail_targets = table.map(avail_targets, function(p) return p.id end)
if #avail_targets == 0 and #avail_cards == 0 then break end
table.insertIfNeed(selected_targets, table.random(avail_targets))
end
if card_name == "peach" then
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)
end)
local vss = table.filter(player:getAllSkills(), function(s)
return s:isInstanceOf(ViewAsSkill)
end)
table.insertTable(cards, vss)
while #cards > 0 do
local sth = table.remove(cards, math.random(#cards))
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
return ""
end
random_cb["AskForResponseCard"] = function(self, jsonData)
local data = json.decode(jsonData)
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 avail_cards = table.filter(self.player:getCardIds{ Player.Hand, Player.Equip }, function(id)
return exp:match(Fk:getCardById(id))
cards = table.filter(cards, function(c)
return exp:match(c) and not player:prohibitResponse(c)
end)
if #avail_cards > 0 then return json.encode{
card = table.random(avail_cards),
targets = {},
} end
-- TODO: vs skill
local vss = table.filter(player:getAllSkills(), function(s)
return s:isInstanceOf(ViewAsSkill)
end)
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 ""
end
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)
return s:isInstanceOf(ActiveSkill)
end)
@ -205,16 +368,13 @@ random_cb["PlayCard"] = function(self, jsonData)
table.insertTable(cards, vss)
while #cards > 0 do
local sth = table.random(cards)
local sth = table.remove(cards, math.random(#cards))
if sth:isInstanceOf(Card) then
local card = sth
local skill = card.skill ---@type ActiveSkill
if math.random() > 0.15 then
local ret = RandomAI.useActiveSkill(self, skill, card)
if ret ~= "" then return ret end
table.removeOne(cards, card)
else
table.removeOne(cards, card)
end
elseif sth:isInstanceOf(ActiveSkill) then
local active = sth
@ -222,14 +382,12 @@ random_cb["PlayCard"] = function(self, jsonData)
local ret = RandomAI.useActiveSkill(self, active, nil)
if ret ~= "" then return ret end
end
table.removeOne(cards, active)
else
local vs = sth
if math.random() > 0.20 then
local ret = self:useVSSkill(vs)
-- TODO: handle vs result
if ret ~= "" then return ret end
end
table.removeOne(cards, vs)
end
end

View File

@ -12,6 +12,7 @@ fk.BeforeTurnStart = 83
fk.TurnStart = 3
fk.TurnEnd = 73
fk.AfterTurnEnd = 84
fk.EventTurnChanging = 96
fk.EventPhaseStart = 4
fk.EventPhaseProceeding = 5
fk.EventPhaseEnd = 6
@ -95,6 +96,10 @@ fk.GameFinished = 66
fk.AskForCardUse = 67
fk.AskForCardResponse = 68
fk.HandleAskForPlayCard = 97
fk.AfterAskForCardUse = 98
fk.AfterAskForCardResponse = 99
fk.AfterAskForNullification = 100
fk.StartPindian = 69
fk.PindianCardsDisplayed = 70
@ -138,4 +143,10 @@ fk.AfterPropertyChange = 94
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
---@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
local Dying = GameEvent:subclass("GameEvent.Dying")
function Dying:main()
@ -43,6 +53,12 @@ function Dying:exit()
logic:trigger(fk.AfterDying, dyingPlayer, dyingStruct, self.interrupted)
end
--- 根据濒死数据让人进入濒死。
---@param dyingStruct DyingStruct
function DeathEventWrappers:enterDying(dyingStruct)
return exec(Dying, dyingStruct)
end
---@class GameEvent.Death : GameEvent
local Death = GameEvent:subclass("GameEvent.Death")
function Death:prepare()
@ -103,6 +119,12 @@ function Death:main()
logic:trigger(fk.Deathed, victim, deathStruct)
end
--- 根据死亡数据杀死角色。
---@param deathStruct DeathStruct
function DeathEventWrappers:killPlayer(deathStruct)
return exec(Death, deathStruct)
end
---@class GameEvent.Revive : GameEvent
local Revive = GameEvent:subclass("GameEvent.Revive")
function Revive:main()
@ -125,4 +147,10 @@ function Revive:main()
room.logic:trigger(fk.AfterPlayerRevived, player, { reason = reason })
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
end
room:setTag("LuckCardData", luck_data)
room:notifyMoveFocus(room.alive_players, "AskForLuckCard")
room:doBroadcastNotify("AskForLuckCard", room.settings.luckTime or 4)
room.room:setRequestTimer(room.timeout * 1000 + 1000)
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
local request = Request:new("AskForSkillInvoke", room.alive_players)
for _, p in ipairs(room.alive_players) do
request:setData(p, { "AskForLuckCard", "#AskForLuckCard:::" .. room.settings.luckTime })
end
while true do
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()
request.luck_data = luck_data
request.accept_cancel = true
request:ask()
for _, player in ipairs(room.alive_players) do
local draw_data = luck_data[player.id]
draw_data.luckTime = nil
room.logic:trigger(fk.AfterDrawInitialCards, player, draw_data)
end
room:removeTag("LuckCardData")
end
---@class GameEvent.Round : GameEvent
@ -143,12 +102,27 @@ local Round = GameEvent:subclass("GameEvent.Round")
function Round:action()
local room = self.room
local p
local nextTurnOwner
local skipRoundPlus = false
repeat
nextTurnOwner = nil
skipRoundPlus = false
p = room.current
GameEvent.Turn:create(p):exec()
if room.game_finished then break end
room.current = room.current:getNextAlive(true, nil, true)
until p.seat >= p:getNextAlive(true, nil, true).seat
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
function Round:main()
@ -269,6 +243,8 @@ function Turn:clear()
logic:trigger(fk.AfterTurnEnd, current, nil, self.interrupted)
current.phase = Player.NotActive
room:setTag("endTurn", false)
for _, p in ipairs(room.players) do
p:setCardUseHistory("", 0, Player.HistoryTurn)
p:setSkillUseHistory("", 0, Player.HistoryTurn)
@ -317,6 +293,7 @@ function Phase:main()
[Player.Judge] = function()
local cards = player:getCardIds(Player.Judge)
while #cards > 0 do
if player._phase_end then break end
local cid = table.remove(cards)
if not cid then return end
local card = player:removeVirtualEquip(cid)
@ -344,12 +321,17 @@ function Phase:main()
n = 2
}
room.logic:trigger(fk.DrawNCards, player, data)
room:drawCards(player, data.n, "game_rule")
if not player._phase_end then
room:drawCards(player, data.n, "game_rule")
end
room.logic:trigger(fk.AfterDrawNCards, player, data)
end,
[Player.Play] = function()
player._play_phase_end = false
room:doBroadcastNotify("UpdateSkill", "", {player})
while not player.dead do
if player._phase_end then break end
logic:trigger(fk.StartPlayCard, player, nil, true)
room:notifyMoveFocus(player, "PlayCard")
local result = room:doRequest(player, "PlayCard", player.id)
@ -359,14 +341,10 @@ function Phase:main()
if type(useResult) == "table" then
room:useCard(useResult)
end
if player._play_phase_end then
player._play_phase_end = false
break
end
end
end,
[Player.Discard] = function()
if player._phase_end then return end
local discardNum = #table.filter(
player:getCardIds(Player.Hand), function(id)
local card = Fk:getCardById(id)
@ -408,6 +386,7 @@ function Phase:clear()
room:setPlayerMark(p, name, 0)
end
end
p._phase_end = false
end
for cid, cmark in pairs(room.card_marks) do

View File

@ -1,32 +1,43 @@
-- SPDX-License-Identifier: GPL-3.0-or-later
local damage_nature_table = {
[fk.NormalDamage] = "normal_damage",
[fk.FireDamage] = "fire_damage",
[fk.ThunderDamage] = "thunder_damage",
[fk.IceDamage] = "ice_damage",
}
---@class HpEventWrappers: Object
local HpEventWrappers = {} -- mixin
---@return boolean
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 damageName = Fk:getDamageNatureName(damageStruct.damageType)
if damageStruct.from then
room:sendLog{
type = "#Damage",
to = {damageStruct.from.id},
from = damageStruct.to.id,
arg = damageStruct.damage,
arg2 = damage_nature_table[damageStruct.damageType],
arg2 = damageName,
}
else
room:sendLog{
type = "#DamageWithNoFrom",
from = damageStruct.to.id,
arg = damageStruct.damage,
arg2 = damage_nature_table[damageStruct.damageType],
arg2 = damageName,
}
end
room:sendLogEvent("Damage", {
to = damageStruct.to.id,
damageType = damage_nature_table[damageStruct.damageType],
damageType = damageName,
damageNum = damageStruct.damage,
})
end
@ -51,6 +62,17 @@ function ChangeHp:main()
}
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.num = num + data.shield_lost
end
@ -114,6 +136,17 @@ function ChangeHp:main()
return true
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
local Damage = GameEvent:subclass("GameEvent.Damage")
function Damage:main()
@ -178,11 +211,6 @@ function Damage:main()
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(
damageStruct.to,
-damageStruct.damage,
@ -213,11 +241,11 @@ function Damage:exit()
logic:trigger(fk.DamageFinished, damageStruct.to, damageStruct)
if damageStruct.beginnerOfTheDamage and not damageStruct.chain then
local targets = table.filter(room:getOtherPlayers(damageStruct.to), function(p)
return p.chained
if damageStruct.chain_table and #damageStruct.chain_table > 0 then
damageStruct.chain_table = table.filter(damageStruct.chain_table, function(p)
return p:isAlive() and p.chained
end)
for _, p in ipairs(targets) do
for _, p in ipairs(damageStruct.chain_table) do
room:sendLog{
type = "#ChainDamage",
from = p.id
@ -238,6 +266,13 @@ function Damage:exit()
end
end
--- 根据伤害数据造成伤害。
---@param damageStruct DamageStruct
---@return boolean
function HpEventWrappers:damage(damageStruct)
return exec(Damage, damageStruct)
end
---@class GameEvent.LoseHp : GameEvent
local LoseHp = GameEvent:subclass("GameEvent.LoseHp")
function LoseHp:main()
@ -268,6 +303,15 @@ function LoseHp:main()
return true
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
local Recover = GameEvent:subclass("GameEvent.Recover")
function Recover:prepare()
@ -316,6 +360,13 @@ function Recover:main()
return true
end
--- 根据回复数据回复体力。
---@param recoverStruct RecoverStruct
---@return boolean
function HpEventWrappers:recover(recoverStruct)
return exec(Recover, recoverStruct)
end
---@class GameEvent.ChangeMaxHp : GameEvent
local ChangeMaxHp = GameEvent:subclass("GameEvent.ChangeMaxHp")
function ChangeMaxHp:main()
@ -374,4 +425,12 @@ function ChangeMaxHp:main()
return true
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
-- Definitions of game events
@ -5,11 +6,16 @@
-- 某类事件对应的结束事件其id刚好就是那个事件的相反数
-- GameEvent.EventFinish = -1
--- 给Room.lua瘦身一系列GameEvent的封装
---@class GameEventWrappers: MiscEventWrappers, HpEventWrappers, DeathEventWrappers, MoveEventWrappers, UseCardEventWrappers, SkillEventWrappers, JudgeEventWrappers, PindianEventWrappers
local GameEventWrappers = {} -- mixin
local tmp
tmp = require "server.events.misc"
GameEvent.Game = tmp[1]
GameEvent.ChangeProperty = tmp[2]
GameEvent.ClearEvent = tmp[3]
table.assign(GameEventWrappers, tmp[4])
tmp = require "server.events.hp"
GameEvent.ChangeHp = tmp[1]
@ -17,25 +23,31 @@ GameEvent.Damage = tmp[2]
GameEvent.LoseHp = tmp[3]
GameEvent.Recover = tmp[4]
GameEvent.ChangeMaxHp = tmp[5]
table.assign(GameEventWrappers, tmp[6])
tmp = require "server.events.death"
GameEvent.Dying = tmp[1]
GameEvent.Death = tmp[2]
GameEvent.Revive = tmp[3]
table.assign(GameEventWrappers, tmp[4])
tmp = require "server.events.movecard"
GameEvent.MoveCards = tmp
GameEvent.MoveCards = tmp[1]
table.assign(GameEventWrappers, tmp[2])
tmp = require "server.events.usecard"
GameEvent.UseCard = tmp[1]
GameEvent.RespondCard = tmp[2]
GameEvent.CardEffect = tmp[3]
table.assign(GameEventWrappers, tmp[4])
tmp = require "server.events.skill"
GameEvent.SkillEffect = tmp
GameEvent.SkillEffect = tmp[1]
table.assign(GameEventWrappers, tmp[2])
tmp = require "server.events.judge"
GameEvent.Judge = tmp
GameEvent.Judge = tmp[1]
table.assign(GameEventWrappers, tmp[2])
tmp = require "server.events.gameflow"
GameEvent.DrawInitial = tmp[1]
@ -44,7 +56,8 @@ GameEvent.Turn = tmp[3]
GameEvent.Phase = tmp[4]
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
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.exit_funcs[name] = e
end
return GameEventWrappers

View File

@ -1,5 +1,15 @@
-- 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
local Judge = GameEvent:subclass("GameEvent.Judge")
function Judge:main()
@ -74,4 +84,66 @@ function Judge:clear()
})
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
---@class MiscEventWrappers: Object
local MiscEventWrappers = {} -- mixin
---@class GameEvent.Game : GameEvent
local Game = GameEvent:subclass("GameEvent.Game")
function Game:main()
@ -129,6 +132,63 @@ function ChangeProperty:main()
logic:trigger(fk.AfterPropertyChange, player, data)
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
local ClearEvent = GameEvent:subclass("GameEvent.ClearEvent")
function ClearEvent:main()
@ -154,4 +214,4 @@ function ClearEvent:main()
logic.cleaner_stack:pop()
end
return { Game, ChangeProperty, ClearEvent }
return { Game, ChangeProperty, ClearEvent, MiscEventWrappers }

View File

@ -1,5 +1,15 @@
-- 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
local MoveCards = GameEvent:subclass("GameEvent.MoveCards")
function MoveCards:main()
@ -102,58 +112,7 @@ function MoveCards:main()
---@param info MoveInfo
for _, info in ipairs(data.moveInfo) do
local realFromArea = room:getCardArea(info.cardId)
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)
room:applyMoveInfo(data, info)
if data.toArea == Card.DrawPile or realFromArea == Card.DrawPile then
room:doBroadcastNotify("UpdateDrawPile", #room.draw_pile)
end
@ -212,4 +171,259 @@ function MoveCards:main()
return true
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
---@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
local Pindian = GameEvent:subclass("GameEvent.Pindian")
function Pindian:main()
@ -190,4 +200,11 @@ function Pindian:clear()
if not self.interrupted then return 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
---@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
local SkillEffect = GameEvent:subclass("GameEvent.SkillEffect")
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 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
player:addSkillUseHistory(skill.name)
if player and not skill.cardSkill then
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
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
local ret = effect_cb()
logic:trigger(fk.AfterSkillEffect, player, skill)
local ret = effect_cb and effect_cb() or false
logic:trigger(fk.AfterSkillEffect, player, main_skill)
return ret
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
---@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)
if card.type ~= Card.TypeEquip then
local anim_path = "./packages/" .. card.package.extensionName .. "/image/anim/" .. card.name
@ -397,4 +407,512 @@ function CardEffect:main()
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]
p.role = roles[i]
if p.role == "lord" then
p.role_shown = true
room:broadcastProperty(p, "role")
else
room:notifyProperty(p, p, "role")
room:setPlayerProperty(p, "role_shown", true)
end
room:broadcastProperty(p, "role")
end
end
@ -212,22 +210,9 @@ end
function GameLogic:prepareDrawPile()
local room = self.room
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(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
local seed = math.random(2 << 32 - 1)
room:prepareDrawPile(seed)
room:doBroadcastNotify("PrepareDrawPile", seed)
end
function GameLogic:attachSkillToPlayers()
@ -774,7 +759,7 @@ function GameLogic:damageByCardEffect(is_exact)
local c_event = d_event:findParent(GameEvent.CardEffect, false, 2)
if c_event == nil then return false end
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
function GameLogic:dumpEventStack(detailed)

View File

@ -29,10 +29,14 @@ MarkEnum.BypassTimesLimitTo = "BypassTimesLimitTo"
MarkEnum.BypassDistancesLimitTo = "BypassDistancesLimitTo"
---非锁定技失效
MarkEnum.UncompulsoryInvalidity = "UncompulsoryInvalidity"
---失效技能表
MarkEnum.InvalidSkills = "InvalidSkills"
---不可明置值为表m - 主将, d - 副将)
MarkEnum.RevealProhibited = "RevealProhibited"
---不计入距离、座次后缀
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
-- 本文件是用来处理各种异步请求的
-- 与游戏中常见的请求-答复没有什么联系
local function tellRoomToObserver(self, player)
local observee = self.players[1]
local start_time = os.getms()
local summary = self:getSummary(observee, true)
local summary = self:toJsonObject(observee)
player:doNotify("Observe", json.encode(summary))
fk.qInfo(string.format("[Observe] %d, %s, in %.3fms",
@ -61,54 +64,6 @@ request_handlers["prelight"] = function(room, id, reqlist)
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)
local player = room:getPlayerById(id)
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 phase_state table[]
---@field public phase_index integer
---@field public role_shown boolean
---@field private _fake_skills Skill[]
---@field private _manually_fake_skills Skill[]
---@field public prelighted_skills Skill[]
@ -32,11 +31,12 @@ function ServerPlayer:initialize(_self)
self.room = nil
-- Below are for doBroadcastRequest
-- 但是几乎全部被船新request杀了
self.request_data = ""
self.client_reply = ""
--self.client_reply = ""
self.default_reply = ""
self.reply_ready = false
self.reply_cancel = false
--self.reply_ready = false
--self.reply_cancel = false
self.phases = {}
self.skipped_phases = {}
self.phase_state = {}
@ -74,92 +74,14 @@ function ServerPlayer:doNotify(command, jsonData)
end
end
--- Send a request to client, and allow client to reply within *timeout* seconds.
---
--- *timeout* must not be negative. If nil, room.timeout is used.
---@param command string
---@param jsonData string
---@param timeout? integer
function ServerPlayer:doRequest(command, jsonData, timeout)
self.client_reply = ""
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)
-- FIXME: 基本都改成新写法后删了这个兼容玩意
function ServerPlayer:__index(k)
local request = self.room.last_request
if not request then return nil end
if k == "client_reply" then
return request.result[self.id]
elseif k == "reply_ready" then
return request.result[self.id] and request.result[self.id] ~= ""
end
end
@ -173,128 +95,27 @@ function ServerPlayer:chat(msg)
})
end
--- Wait for at most *timeout* seconds for reply from client.
---
--- 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
function ServerPlayer:toJsonObject()
local o = Player.toJsonObject(self)
local sp = self._splayer
return {
-- data for Setup/AddPlayer
d = {
self.id,
sp:getScreenName(),
sp:getAvatar(),
false,
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,
o.setup_data = {
self.id,
sp:getScreenName(),
sp:getAvatar(),
false,
sp:getTotalGameTime(),
}
return o
end
-- 似乎没有必要
-- function ServerPlayer:loadJsonObject() end
function ServerPlayer:reconnect()
local room = self.room
self.serverplayer:setState(fk.Player_Online)
local summary = room:getSummary(self, false)
local summary = room:toJsonObject(self)
self:doNotify("Reconnect", json.encode(summary))
room:notifyProperty(self, self, "role")
self:doNotify("RoomOwner", json.encode{ room.room:getOwner():getId() })
@ -331,6 +152,7 @@ function ServerPlayer:turnOver()
self.room.logic:trigger(fk.TurnedOver, self)
end
---@param cards integer|integer[]|Card|Card[]
function ServerPlayer:showCards(cards)
cards = Card:getIdList(cards)
for _, id in ipairs(cards) do
@ -355,12 +177,7 @@ function ServerPlayer:showCards(cards)
room.logic:trigger(fk.CardShown, self, { cardIds = cards })
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 to_phase Phase
@ -427,7 +244,7 @@ function ServerPlayer:gainAnExtraPhase(phase, delay)
room:sendLog{
type = "#GainAnExtraPhase",
from = self.id,
arg = phase_name_table[phase],
arg = Util.PhaseStrMapper(phase),
}
GameEvent.Phase:create(self, self.phase):exec()
@ -441,7 +258,7 @@ function ServerPlayer:gainAnExtraPhase(phase, delay)
room:sendLog{
type = "#PhaseSkipped",
from = self.id,
arg = phase_name_table[phase],
arg = Util.PhaseStrMapper(phase),
}
end
@ -479,7 +296,7 @@ function ServerPlayer:play(phase_table)
end
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)
break
end
@ -514,7 +331,7 @@ function ServerPlayer:play(phase_table)
room:sendLog{
type = "#PhaseSkipped",
from = self.id,
arg = phase_name_table[self.phase],
arg = Util.PhaseStrMapper(self.phase),
}
end
end
@ -540,10 +357,17 @@ end
--- 当进行到出牌阶段空闲点时,结束出牌阶段。
function ServerPlayer:endPlayPhase()
self._play_phase_end = true
if self.phase == Player.Play then
self._phase_end = true
end
-- TODO: send log
end
--- 结束当前阶段。
function ServerPlayer:endCurrentPhase()
self._phase_end = true
end
--- 获得一个额外回合
---@param delay? boolean
---@param skillName? string
@ -1014,7 +838,7 @@ function ServerPlayer:addBuddy(other)
other = self.room:getPlayerById(other)
end
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
function ServerPlayer:removeBuddy(other)
@ -1022,7 +846,7 @@ function ServerPlayer:removeBuddy(other)
other = self.room:getPlayerById(other)
end
Player.removeBuddy(self, other)
self:doNotify("RmBuddy", tostring(other.id))
self.room:doBroadcastNotify("RmBuddy", json.encode{ self.id, other.id })
end
return ServerPlayer

View File

@ -82,6 +82,7 @@ fk.IceDamage = 4
---@field public skillName? string @ 造成本次伤害的技能名
---@field public beginnerOfTheDamage? boolean @ 是否是本次铁索传导的起点
---@field public by_user? boolean @ 是否由卡牌直接生效造成的伤害
---@field public chain_table? ServerPlayer[] @ 铁索连环表
--- 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")
Fk:addDamageNature(fk.FireDamage, "fire_damage")
Fk:addDamageNature(fk.ThunderDamage, "thunder_damage")
local thunderSlashSkill = fk.CreateActiveSkill{
name = "thunder__slash_skill",
prompt = function(self, selected_cards)
@ -462,10 +465,21 @@ local silverLion = fk.CreateArmor{
}
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{
name = "hualiu",
suit = Card.Diamond,
number = 13,
equip_skill = hualiuSkill,
}
extension:addCards({
@ -549,6 +563,8 @@ Fk:loadTranslationTable{
[":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

View File

@ -87,6 +87,13 @@ local choosePlayersSkill = fk.CreateActiveSkill{
return table.contains(self.targets, to_select)
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,
min_target_num = function(self) return self.min_num end,
max_target_num = function(self) return self.num end,
@ -104,9 +111,6 @@ local exChooseSkill = fk.CreateActiveSkill{
local checkpoint = true
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
checkpoint = checkpoint and (Exppattern:Parse(self.pattern):match(card))
@ -119,6 +123,13 @@ local exChooseSkill = fk.CreateActiveSkill{
return table.contains(self.targets, to_select)
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,
max_target_num = function(self) return self.max_t_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"] = "In your Finish Phase, you can draw 1 card.",
["fastchat_m"] = "快捷短语",
["fastchat_f"] = "快捷短语",
["fastchat_m"] = "quick chats",
["fastchat_f"] = "quick chats",
["$fastchat_m1"] = "能不能快一点啊,兵贵神速啊。",
["$fastchat_m2"] = "主公,别开枪,自己人!",
@ -171,6 +171,13 @@ Fk:loadTranslationTable({
["$fastchat_m14"] = "哥们,给力点行吗?",
["$fastchat_m15"] = "哥哥,交个朋友吧。",
["$fastchat_m16"] = "妹子,交个朋友吧。",
["$fastchat_m17"] = "我从未见过如此厚颜无耻之人!",
["$fastchat_m18"] = "你随便杀,闪不了算我输。",
["$fastchat_m19"] = "这波,不亏。",
["$fastchat_m20"] = "请收下我的膝盖。",
["$fastchat_m21"] = "你咋不上天呢?",
["$fastchat_m22"] = "放开我的队友,冲我来。",
["$fastchat_m23"] = "见证奇迹的时刻到了。",
["$fastchat_f1"] = "能不能快一点啊,兵贵神速啊。",
["$fastchat_f2"] = "主公,别开枪,自己人!",
["$fastchat_f3"] = "小内再不跳,后面还怎么玩啊?",
@ -187,6 +194,13 @@ Fk:loadTranslationTable({
["$fastchat_f14"] = "哥们,给力点行吗?",
["$fastchat_f15"] = "哥,交个朋友吧。",
["$fastchat_f16"] = "妹子,交个朋友吧。",
["$fastchat_f17"] = "我从未见过如此厚颜无耻之人!",
["$fastchat_f18"] = "你随便杀,闪不了算我输。",
["$fastchat_f19"] = "这波,不亏。",
["$fastchat_f20"] = "请收下我的膝盖。",
["$fastchat_f21"] = "你咋不上天呢?",
["$fastchat_f22"] = "放开我的队友,冲我来。",
["$fastchat_f23"] = "见证奇迹的时刻到了。",
["aaa_role_mode"] = "Role mode",
[":aaa_role_mode"] = [[

View File

@ -1,4 +1,6 @@
-- SPDX-License-Identifier: GPL-3.0-or-later
dofile "packages/standard/i18n/zh_CN.lua"
dofile "packages/standard/i18n/en_US.lua"
local pkgprefix = "packages/"
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
https://sgs.52pk.com/zl/201205/5299813.shtml
]========================================],
}

View File

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

View File

@ -1,4 +1,6 @@
-- SPDX-License-Identifier: GPL-3.0-or-later
dofile "packages/standard_cards/i18n/zh_CN.lua"
dofile "packages/standard_cards/i18n/en_US.lua"
local pkgprefix = "packages/"
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
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
room:responseCard({
from = currentResponser.id,
@ -495,7 +495,7 @@ local savageAssaultSkill = fk.CreateActiveSkill{
return user ~= to_select
end,
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
room:responseCard({
@ -539,7 +539,7 @@ local archeryAttackSkill = fk.CreateActiveSkill{
return user ~= to_select
end,
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
room:responseCard({
@ -631,6 +631,7 @@ local amazingGraceSkill = fk.CreateActiveSkill{
use.extra_data = use.extra_data or {}
use.extra_data.AGFilled = toDisplay
use.extra_data.AGResult = {}
else
if use.extra_data and use.extra_data.AGFilled then
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)
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)
end
}
@ -705,7 +707,8 @@ local lightningSkill = fk.CreateActiveSkill{
to = to,
damage = 3,
card = effect.card,
damageType = fk.ThunderDamage,
-- damageType = fk.ThunderDamage,
damageType = Fk:getDamageNature(fk.ThunderDamage) and fk.ThunderDamage or fk.NormalDamage,
skillName = self.name,
}
@ -1048,17 +1051,20 @@ local axeSkill = fk.CreateTriggerSkill{
attached_equip = "axe",
events = {fk.CardEffectCancelledOut},
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,
on_cost = function(self, event, target, player, data)
local room = player.room
local pattern
if player:getEquipment(Card.SubtypeWeapon) then
pattern = ".|.|.|.|.|.|^"..tostring(player:getEquipment(Card.SubtypeWeapon))
else
pattern = "."
local cards = {}
for _, id in ipairs(player:getCardIds("he")) do
if not player:prohibitDiscard(id) and
not (table.contains(player:getEquipments(Card.SubtypeWeapon), id) and Fk:getCardById(id).name == "axe") then
table.insert(cards, id)
end
end
local cards = room:askForDiscard(player, 2, 2, true, self.name, true, pattern, "#axe-invoke::"..data.to, true)
cards = room:askForDiscard(player, 2, 2, true, self.name, true, ".|.|.|.|.|.|"..table.concat(cards, ","),
"#axe-invoke::"..data.to, true)
if #cards > 0 then
self.cost_data = cards
return true
@ -1248,84 +1254,134 @@ extension:addCards({
niohShield,
})
local horseSkill = fk.CreateDistanceSkill{
name = "horse_skill",
global = true,
local diluSkill = fk.CreateDistanceSkill{
name = "#dilu_skill",
attached_equip = "dilu",
correct_func = function(self, from, to)
local ret = 0
if from:getEquipment(Card.SubtypeOffensiveRide) then
ret = ret - 1
if to:hasSkill(self) then
return 1
end
if to:getEquipment(Card.SubtypeDefensiveRide) then
ret = ret + 1
end
return ret
end,
}
if not Fk.skills["horse_skill"] then
Fk:addSkill(horseSkill)
end
Fk:addSkill(diluSkill)
local diLu = fk.CreateDefensiveRide{
name = "dilu",
suit = Card.Club,
number = 5,
equip_skill = diluSkill,
}
extension:addCards({
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{
name = "jueying",
suit = Card.Spade,
number = 5,
equip_skill = jueyingSkill,
}
extension:addCards({
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{
name = "zhuahuangfeidian",
suit = Card.Heart,
number = 13,
equip_skill = zhuahuangfeidianSkill,
}
extension:addCards({
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{
name = "chitu",
suit = Card.Heart,
number = 5,
equip_skill = chituSkill,
}
extension:addCards({
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{
name = "dayuan",
suit = Card.Spade,
number = 13,
equip_skill = dayuanSkill,
}
extension:addCards({
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{
name = "zixing",
suit = Card.Diamond,
number = 13,
equip_skill = zixingSkill,
}
extension:addCards({
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

View File

@ -1,5 +1,21 @@
# 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
# "main.cpp"
"freekill.cpp"
@ -21,6 +37,8 @@ set(freekill_SRCS
"ui/qmlbackend.cpp"
"swig/freekill-wrap.cxx"
)
set_source_files_properties(
"swig/freekill-wrap.cxx" PROPERTIES GENERATED TRUE)
if (NOT DEFINED FK_SERVER_ONLY)
list(APPEND freekill_SRCS

View File

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