mirror of
https://github.com/Qsgs-Fans/FreeKill.git
synced 2024-11-15 19:22:25 +08:00
Changelog: v0.4.20
This commit is contained in:
parent
220fb93b0e
commit
30df075db2
1
.github/workflows/build-windows.yml
vendored
1
.github/workflows/build-windows.yml
vendored
|
@ -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
|
||||
|
|
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -1,5 +1,18 @@
|
|||
# ChangeLog
|
||||
|
||||
## v0.4.20
|
||||
|
||||
- 重构了UI逻辑
|
||||
- 重构了客户端隐藏信息的处理
|
||||
- 旁观不会看到手牌和身份了
|
||||
- 牌局内可查看一览
|
||||
- 新增了明置牌概念 以及相关状态技
|
||||
- 新增了筛选房间功能
|
||||
- 新增拖动手牌排序
|
||||
- 新增技能times
|
||||
|
||||
___
|
||||
|
||||
## v0.4.19
|
||||
|
||||
- 修改加入服务器界面,新增服务器列表(暂不会自动更新)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
32
deploycore.sh
Executable 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
1
include
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 4fd2070d099d1f967d1070d72beb0fae2cb6e4be
|
|
@ -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])
|
||||
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
|
||||
else
|
||||
if table.contains({ Player.Hand, Player.Equip, Player.Judge, Player.Special }, move.fromArea) then
|
||||
from:removeCards(move.fromArea, move.ids, move.fromSpecialName)
|
||||
end
|
||||
end
|
||||
elseif move.fromArea == Card.DiscardPile then
|
||||
table.removeOne(self.discard_pile, move.ids[1])
|
||||
end
|
||||
|
||||
if move.to and move.toArea then
|
||||
local ids = move.ids
|
||||
if (move.toArea == Card.PlayerHand and not Self:isBuddy(self:getPlayerById(move.to))) or
|
||||
(move.toArea == Card.PlayerSpecial and not move.moveVisible) then
|
||||
ids = {-1}
|
||||
end
|
||||
|
||||
self:getPlayerById(move.to):addCards(move.toArea, ids, move.specialName)
|
||||
elseif move.toArea == Card.DiscardPile then
|
||||
table.insert(self.discard_pile, move.ids[1])
|
||||
end
|
||||
|
||||
-- FIXME: 需要系统化的重构
|
||||
if move.fromArea == Card.Processing then
|
||||
for _, v in ipairs(move.ids) do
|
||||
self._processing[v] = nil
|
||||
end
|
||||
end
|
||||
if move.toArea == Card.Processing then
|
||||
for _, v in ipairs(move.ids) do
|
||||
self._processing[v] = true
|
||||
end
|
||||
end
|
||||
|
||||
if (move.ids[1] ~= -1) then
|
||||
Fk:filterCard(move.ids[1], ClientInstance:getPlayerById(move.to))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@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
|
||||
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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)",
|
||||
|
|
|
@ -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 伤害",
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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是跑在服务端还是客户端,并返回相应的实例。
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
80
lua/core/request_handler.lua
Normal file
80
lua/core/request_handler.lua
Normal 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
|
367
lua/core/request_type/active_skill.lua
Normal file
367
lua/core/request_type/active_skill.lua
Normal 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组件:手牌区内的牌(TODO:expand牌)、在场角色、确定取消按钮
|
||||
(TODO:interaction小组件)
|
||||
可能发生的事件:
|
||||
* 点击手牌:刷新所有未选中牌的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
|
36
lua/core/request_type/invoke.lua
Normal file
36
lua/core/request_type/invoke.lua
Normal 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
|
132
lua/core/request_type/play_card.lua
Normal file
132
lua/core/request_type/play_card.lua
Normal 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
|
164
lua/core/request_type/response_card.lua
Normal file
164
lua/core/request_type/response_card.lua
Normal 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
|
107
lua/core/request_type/use_card.lua
Normal file
107
lua/core/request_type/use_card.lua
Normal 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
|
92
lua/core/room/abstract_room.lua
Normal file
92
lua/core/room/abstract_room.lua
Normal 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
|
355
lua/core/room/card_manager.lua
Normal file
355
lua/core/room/card_manager.lua
Normal 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
|
0
lua/core/room/skill_manager.lua
Normal file
0
lua/core/room/skill_manager.lua
Normal file
0
lua/core/room/user_manager.lua
Normal file
0
lua/core/room/user_manager.lua
Normal 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
|
||||
|
|
|
@ -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,12 +99,14 @@ function ActiveSkill:getMaxTargetNum(player, card)
|
|||
ret = ret[#ret]
|
||||
end
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
22
lua/core/skill_type/visibility.lua
Normal file
22
lua/core/skill_type/visibility.lua
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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"
|
||||
|
||||
-- 读取配置文件。
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -35,6 +35,8 @@ function Object:isInstanceOf(class) end
|
|||
---@return boolean
|
||||
function Object:isSubclassOf(class) end
|
||||
|
||||
function Object:include(e) end
|
||||
|
||||
---@class json
|
||||
json = {}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
if skill:feasible(selected_targets, selected_cards, self.player, card) then
|
||||
end
|
||||
|
||||
local all_cards = player:getCardIds{ Player.Hand, Player.Equip }
|
||||
if skill.expand_pile then
|
||||
if type(skill.expand_pile) == "string" then
|
||||
table.insertTableIfNeed(all_cards, player.special_cards[skill.expand_pile] or {})
|
||||
elseif type(skill.expand_pile) == "table" then
|
||||
table.insertTableIfNeed(all_cards, skill.expand_pile)
|
||||
end
|
||||
end
|
||||
|
||||
--local max_target_num = skill:getMaxTargetNum(player, card)
|
||||
local card_filter_func = card and Util.FalseFunc or skill.cardFilter
|
||||
local firstTry
|
||||
for _ = 0, max_try_times do
|
||||
if not firstTry and skill:feasible(selected_targets, selected_cards, self.player, card) then
|
||||
firstTry = {table.simpleClone(selected_targets), table.simpleClone(selected_cards)}
|
||||
end
|
||||
if firstTry and math.random() < 0.1 then break end
|
||||
local avail_targets = table.filter(room.alive_players, function(p)
|
||||
return not table.contains(selected_targets, p.id) and (#limited_targets == 0 or table.contains(limited_targets, p.id))
|
||||
and skill:targetFilter(p.id, selected_targets, selected_cards, card)
|
||||
and (not card or not player:isProhibited(p, card))
|
||||
end)
|
||||
local avail_cards = table.filter(all_cards, function(id)
|
||||
return not table.contains(selected_cards, id) and card_filter_func(skill, id, selected_cards, selected_targets)
|
||||
end)
|
||||
local random_list = table.connect(avail_targets, avail_cards)
|
||||
if #random_list == 0 then break end
|
||||
local randomIndex = math.random(#random_list)
|
||||
if randomIndex <= #avail_targets then
|
||||
table.insertIfNeed(selected_targets, random_list[randomIndex].id)
|
||||
else
|
||||
table.insertIfNeed(selected_cards, random_list[randomIndex])
|
||||
end
|
||||
end
|
||||
local feasibleCheck = function () return skill:feasible(selected_targets, selected_cards, self.player, card) end
|
||||
if firstTry and not feasibleCheck() then
|
||||
selected_targets = firstTry[1]
|
||||
selected_cards = firstTry[2]
|
||||
end
|
||||
if feasibleCheck() then
|
||||
local ret = json.encode{
|
||||
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 {
|
||||
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)
|
||||
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)
|
||||
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')
|
||||
local vss = table.filter(player:getAllSkills(), function(s)
|
||||
return s:isInstanceOf(ViewAsSkill)
|
||||
end)
|
||||
avail_targets = table.map(avail_targets, function(p) return p.id end)
|
||||
table.insertTable(cards, vss)
|
||||
|
||||
if #avail_targets == 0 and #avail_cards == 0 then break end
|
||||
table.insertIfNeed(selected_targets, table.random(avail_targets))
|
||||
end
|
||||
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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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)
|
||||
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
|
||||
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)
|
||||
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
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
316
lua/server/network.lua
Normal 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
|
|
@ -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
|
||||
|
|
1638
lua/server/room.lua
1638
lua/server/room.lua
File diff suppressed because it is too large
Load Diff
|
@ -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 = {
|
||||
o.setup_data = {
|
||||
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,
|
||||
}
|
||||
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
|
||||
|
|
|
@ -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
152
lua/ui_emu/base.lua
Normal 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 })
|
||||
|
||||
这样做是为了在后续操作中能成功的将此处作出的修改传达给QML。因为没有类似QML的
|
||||
属性绑定机制,因此要另外调用update方法来记录相关的属性变动。
|
||||
|
||||
2. 使用Scene提供的方法来访问元素
|
||||
---------------------------------
|
||||
|
||||
例如RoomScene中已经创建了表达卡牌和技能的Item,因此在Handler的逻辑编写中,
|
||||
应当避免再去使用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,
|
||||
}
|
34
lua/ui_emu/choosecardbox.lua
Normal file
34
lua/ui_emu/choosecardbox.lua
Normal 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
29
lua/ui_emu/common.lua
Normal 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
11
lua/ui_emu/control.lua
Normal 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,
|
||||
}
|
23
lua/ui_emu/interaction.lua
Normal file
23
lua/ui_emu/interaction.lua
Normal 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
18
lua/ui_emu/okscene.lua
Normal 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
42
lua/ui_emu/popupbox.lua
Normal 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
0
lua/ui_emu/poxi_box.lua
Normal file
86
lua/ui_emu/roomscene.lua
Normal file
86
lua/ui_emu/roomscene.lua
Normal 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
|
21
lua/ui_emu/specialskills.lua
Normal file
21
lua/ui_emu/specialskills.lua
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"] = [[
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 )
|
||||
|
||||
]========================================],
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
local cards = room:askForDiscard(player, 2, 2, true, self.name, true, pattern, "#axe-invoke::"..data.to, true)
|
||||
end
|
||||
cards = room:askForDiscard(player, 2, 2, true, self.name, true, ".|.|.|.|.|.|"..table.concat(cards, ","),
|
||||
"#axe-invoke::"..data.to, true)
|
||||
if #cards > 0 then
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user