mirror of
https://github.com/Qsgs-Fans/FreeKill.git
synced 2024-11-15 19:22:25 +08:00
Changelog: v0.4.23
This commit is contained in:
parent
f8b67531c0
commit
a09a973064
|
@ -1,6 +1,6 @@
|
|||
# ChangeLog
|
||||
|
||||
## v0.4.21 & v0.4.22
|
||||
## v0.4.21 & v0.4.22 & v0.4.23
|
||||
|
||||
- 修复了确认键亮起时取消键不可用的bug
|
||||
- lua端的ob属性根本没同步,同步一下
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
project(FreeKill VERSION 0.4.22)
|
||||
project(FreeKill VERSION 0.4.23)
|
||||
add_definitions(-DFK_VERSION=\"${CMAKE_PROJECT_VERSION}\")
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS
|
||||
|
|
|
@ -153,6 +153,10 @@ Flickable {
|
|||
selectByMouse: false
|
||||
wrapMode: TextEdit.WordWrap
|
||||
textFormat: TextEdit.RichText
|
||||
property var savedtext: []
|
||||
function clearSavedText() {
|
||||
savedtext = [];
|
||||
}
|
||||
onLinkActivated: (link) => {
|
||||
if (link === "back") {
|
||||
text = savedtext.pop();
|
||||
|
@ -180,6 +184,7 @@ Flickable {
|
|||
screenName.text = "";
|
||||
playerGameData.text = "";
|
||||
skillDesc.text = "";
|
||||
skillDesc.clearSavedText();
|
||||
|
||||
const id = extra_data.photo.playerid;
|
||||
if (id === 0) return;
|
||||
|
|
|
@ -5,7 +5,7 @@ import QtQuick.Layouts
|
|||
import QtQuick.Controls
|
||||
import Fk
|
||||
import Fk.RoomElement
|
||||
import "RoomLogic.js" as RoomLogic
|
||||
// import "RoomLogic.js" as RoomLogic
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
@ -390,12 +390,28 @@ Item {
|
|||
}
|
||||
|
||||
onClicked: {
|
||||
callbacks["LogEvent"]({
|
||||
type: "PlaySkillSound",
|
||||
name: name,
|
||||
general: specific ? detailGeneralCard.name : null, // 分化特别和一般
|
||||
i: idx,
|
||||
});
|
||||
const skill = name;
|
||||
const general = specific ? detailGeneralCard.name : null;
|
||||
let extension;
|
||||
let path;
|
||||
let dat;
|
||||
|
||||
// try main general
|
||||
if (general) {
|
||||
dat = lcall("GetGeneralData", general);
|
||||
extension = dat.extension;
|
||||
path = "./packages/" + extension + "/audio/skill/" + skill + "_" + general;
|
||||
if (Backend.exists(path + ".mp3") || Backend.exists(path + "1.mp3")) {
|
||||
Backend.playSound(path, idx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// finally normal skill
|
||||
dat = lcall("GetSkillData", skill);
|
||||
extension = dat.extension;
|
||||
path = "./packages/" + extension + "/audio/skill/" + skill;
|
||||
Backend.playSound(path, idx);
|
||||
}
|
||||
|
||||
onPressAndHold: {
|
||||
|
|
|
@ -341,6 +341,7 @@ Item {
|
|||
}
|
||||
|
||||
lcall("FinishRequestUI");
|
||||
applyChange({});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1338,7 +1339,12 @@ Item {
|
|||
photo.state = pdata.state;
|
||||
photo.selectable = pdata.enabled;
|
||||
photo.selected = pdata.selected;
|
||||
})
|
||||
});
|
||||
for (let i = 0; i < photoModel.count; i++) {
|
||||
const item = photos.itemAt(i);
|
||||
item.targetTip = lcall("GetTargetTip", item.playerid);
|
||||
}
|
||||
|
||||
const buttons = uiUpdate["Button"];
|
||||
if (buttons) {
|
||||
okCancel.visible = true;
|
||||
|
|
|
@ -768,26 +768,17 @@ callbacks["MoveFocus"] = (data) => {
|
|||
cancelAllFocus();
|
||||
const focuses = data[0];
|
||||
const command = data[1];
|
||||
const timeout = data[2] ?? (config.roomTimeout * 1000);
|
||||
|
||||
let item, model;
|
||||
for (let i = 0; i < playerNum; i++) {
|
||||
model = photoModel.get(i);
|
||||
if (focuses.indexOf(model.id) != -1) {
|
||||
item = photos.itemAt(i);
|
||||
item.progressBar.duration = timeout;
|
||||
item.progressBar.visible = true;
|
||||
item.progressTip = luatr(command)
|
||||
+ luatr(" thinking...");
|
||||
|
||||
/*
|
||||
if (command === "PlayCard") {
|
||||
item.playing = true;
|
||||
}
|
||||
} else {
|
||||
item = photos.itemAt(i);
|
||||
if (command === "PlayCard") {
|
||||
item.playing = false;
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1241,10 +1232,6 @@ callbacks["AskForResponseCard"] = (data) => {
|
|||
roomScene.okCancel.visible = true;
|
||||
}
|
||||
|
||||
callbacks["WaitForNullification"] = () => {
|
||||
roomScene.state = "notactive";
|
||||
}
|
||||
|
||||
callbacks["SetPlayerMark"] = (data) => {
|
||||
const player = getPhoto(data[0]);
|
||||
const mark = data[1];
|
||||
|
@ -1469,6 +1456,10 @@ callbacks["UpdateMiniGame"] = (data) => {
|
|||
}
|
||||
}
|
||||
|
||||
callbacks["EmptyRequest"] = (data) => {
|
||||
roomScene.activate();
|
||||
}
|
||||
|
||||
callbacks["UpdateLimitSkill"] = (data) => {
|
||||
const id = data[0];
|
||||
const skill = data[1];
|
||||
|
|
|
@ -162,6 +162,13 @@ Item {
|
|||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
for (let i = 0; i < cards.length; i++) {
|
||||
const card = cards[i];
|
||||
if (!card.selectable) {
|
||||
const reason = lcall("GetCardProhibitReason", card.cid);
|
||||
card.prohibitReason = reason;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -627,13 +627,14 @@ Item {
|
|||
anchors.bottomMargin: -4
|
||||
from: 0.0
|
||||
to: 100.0
|
||||
property int duration: config.roomTimeout * 1000
|
||||
|
||||
visible: false
|
||||
NumberAnimation on value {
|
||||
running: progressBar.visible
|
||||
from: 100.0
|
||||
to: 0.0
|
||||
duration: config.roomTimeout * 1000
|
||||
duration: progressBar.duration
|
||||
|
||||
onFinished: {
|
||||
progressBar.visible = false;
|
||||
|
@ -665,6 +666,59 @@ Item {
|
|||
visible: root.state === "candidate" && selectable
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
|
||||
Repeater {
|
||||
model: root.targetTip
|
||||
|
||||
Item {
|
||||
// Layout.alignment: Qt.AlignHCenter
|
||||
width: modelData.type === "normal" ? 40 : 24
|
||||
|
||||
GlowText {
|
||||
anchors.centerIn: parent
|
||||
visible: modelData.type === "normal"
|
||||
text: Util.processPrompt(modelData.content)
|
||||
font.family: fontLi2.name
|
||||
color: "#FEFE84"
|
||||
font.pixelSize: {
|
||||
if (text.length <= 3) return 36;
|
||||
else return 28;
|
||||
}
|
||||
//font.bold: true
|
||||
glow.color: "black"
|
||||
glow.spread: 0.3
|
||||
glow.radius: 5
|
||||
lineHeight: 0.85
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WrapAnywhere
|
||||
width: font.pixelSize + 4
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: modelData.type === "warning"
|
||||
font.family: fontLibian.name
|
||||
font.pixelSize: 24
|
||||
opacity: 0.9
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
lineHeight: 24
|
||||
lineHeightMode: Text.FixedHeight
|
||||
//color: "#EAC28A"
|
||||
color: "snow"
|
||||
width: 24
|
||||
wrapMode: Text.WrapAnywhere
|
||||
style: Text.Outline
|
||||
//styleColor: "#83231F"
|
||||
styleColor: "red"
|
||||
text: Util.processPrompt(modelData.content)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InvisibleCardArea {
|
||||
id: handcardAreaItem
|
||||
anchors.centerIn: parent
|
||||
|
@ -739,53 +793,6 @@ Item {
|
|||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
|
||||
Repeater {
|
||||
model: root.targetTip
|
||||
|
||||
Item {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
width: 30
|
||||
|
||||
GlowText {
|
||||
anchors.centerIn: parent
|
||||
visible: modelData.type === "normal"
|
||||
text: Util.processPrompt(modelData.content)
|
||||
font.family: fontLibian.name
|
||||
color: "#F7F589"
|
||||
font.pixelSize: 30
|
||||
font.bold: true
|
||||
glow.color: "black"
|
||||
glow.spread: 0.3
|
||||
glow.radius: 5
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WrapAnywhere
|
||||
width: 30
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: modelData.type === "warning"
|
||||
font.family: fontLibian.name
|
||||
font.pixelSize: 24
|
||||
opacity: 0.9
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
lineHeight: 24
|
||||
lineHeightMode: Text.FixedHeight
|
||||
color: "#EAC28A"
|
||||
width: 24
|
||||
wrapMode: Text.WrapAnywhere
|
||||
style: Text.Outline
|
||||
styleColor: "#83231F"
|
||||
text: Util.processPrompt(modelData.content)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: "#CC2E2C27"
|
||||
radius: 6
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.notify.FreeKill"
|
||||
android:installLocation="preferExternal"
|
||||
android:versionCode="422"
|
||||
android:versionName="0.4.22">
|
||||
android:versionCode="423"
|
||||
android:versionName="0.4.23">
|
||||
<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" />
|
||||
|
|
|
@ -213,18 +213,21 @@ fk.client_callback["SetCardFootnote"] = function(data)
|
|||
ClientInstance:setCardNote(data[1], data[2]);
|
||||
end
|
||||
|
||||
local function setup(id, name, avatar)
|
||||
local function setup(id, name, avatar, msec)
|
||||
local self = fk.Self
|
||||
self:setId(id)
|
||||
self:setScreenName(name)
|
||||
self:setAvatar(avatar)
|
||||
Self = ClientPlayer:new(fk.Self)
|
||||
if msec then
|
||||
fk.ClientInstance:setupServerLag(msec)
|
||||
end
|
||||
end
|
||||
|
||||
fk.client_callback["Setup"] = function(data)
|
||||
-- jsonData: [ int id, string screenName, string avatar ]
|
||||
local id, name, avatar = data[1], data[2], data[3]
|
||||
setup(id, name, avatar)
|
||||
local id, name, avatar, msec = data[1], data[2], data[3], data[4]
|
||||
setup(id, name, avatar, msec)
|
||||
end
|
||||
|
||||
fk.client_callback["EnterRoom"] = function(_data)
|
||||
|
@ -243,7 +246,7 @@ fk.client_callback["EnterRoom"] = function(_data)
|
|||
|
||||
local data = _data[3]
|
||||
ClientInstance.enter_room_data = json.encode(_data);
|
||||
ClientInstance.room_settings = data
|
||||
ClientInstance.settings = data
|
||||
table.insertTableIfNeed(
|
||||
data.disabledPack,
|
||||
Fk.game_mode_disabled[data.gameMode]
|
||||
|
@ -857,13 +860,9 @@ fk.client_callback["AskForUseActiveSkill"] = function(data)
|
|||
local skill = Fk.skills[data[1]]
|
||||
local extra_data = data[4]
|
||||
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]
|
||||
|
||||
local h = Fk.request_handlers["AskForUseActiveSkill"]:new(Self, data)
|
||||
h.change = {}
|
||||
h:setup()
|
||||
h.scene:notifyUI()
|
||||
|
@ -873,12 +872,7 @@ 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]
|
||||
local h = Fk.request_handlers["AskForUseCard"]:new(Self, data)
|
||||
h.change = {}
|
||||
h:setup()
|
||||
h.scene:notifyUI()
|
||||
|
@ -888,12 +882,7 @@ 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]
|
||||
local h = Fk.request_handlers["AskForResponseCard"]:new(Self, data)
|
||||
h.change = {}
|
||||
h:setup()
|
||||
h.scene:notifyUI()
|
||||
|
@ -1100,7 +1089,7 @@ fk.client_callback["GameOver"] = function(jsonData)
|
|||
c.record[2] = table.concat({
|
||||
c.record[2],
|
||||
Self.player:getScreenName(),
|
||||
c.room_settings.gameMode,
|
||||
c.settings.gameMode,
|
||||
Self.general,
|
||||
Self.role,
|
||||
jsonData,
|
||||
|
@ -1118,7 +1107,7 @@ fk.client_callback["EnterLobby"] = function(jsonData)
|
|||
c.record[2] = table.concat({
|
||||
c.record[2],
|
||||
Self.player:getScreenName(),
|
||||
c.room_settings.gameMode,
|
||||
c.settings.gameMode,
|
||||
Self.general,
|
||||
Self.role,
|
||||
"",
|
||||
|
|
|
@ -335,49 +335,6 @@ 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
|
||||
|
@ -457,10 +414,25 @@ function GetSkillData(skill_name)
|
|||
end
|
||||
|
||||
function GetSkillStatus(skill_name)
|
||||
local player = Self
|
||||
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
|
||||
local locked = not skill:isEffectable(player)
|
||||
if not locked then
|
||||
for mark, value in pairs(player.mark) do
|
||||
if mark == MarkEnum.InvalidSkills then
|
||||
if table.contains(value, skill_name) then
|
||||
locked = true
|
||||
break
|
||||
end
|
||||
elseif mark:startsWith(MarkEnum.InvalidSkills .. "-") and table.contains(value, skill_name) then
|
||||
for _, suffix in ipairs(MarkEnum.TempMarkSuffix) do
|
||||
if mark:find(suffix, 1, true) then
|
||||
locked = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return {
|
||||
locked = locked, ---@type boolean
|
||||
|
@ -543,46 +515,6 @@ 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
|
||||
|
@ -746,7 +678,7 @@ end
|
|||
|
||||
function ResetClientLua()
|
||||
local _data = ClientInstance.enter_room_data;
|
||||
local data = ClientInstance.room_settings
|
||||
local data = ClientInstance.settings
|
||||
Self = ClientPlayer:new(fk.Self)
|
||||
ClientInstance = Client:new() -- clear old client data
|
||||
ClientInstance.players = {Self}
|
||||
|
@ -754,7 +686,7 @@ function ResetClientLua()
|
|||
ClientInstance.discard_pile = {}
|
||||
|
||||
ClientInstance.enter_room_data = _data;
|
||||
ClientInstance.room_settings = data
|
||||
ClientInstance.settings = data
|
||||
|
||||
ClientInstance.disabled_packs = data.disabledPack
|
||||
ClientInstance.disabled_generals = data.disabledGenerals
|
||||
|
@ -766,7 +698,7 @@ function ResetAddPlayer(j)
|
|||
end
|
||||
|
||||
function GetRoomConfig()
|
||||
return ClientInstance.room_settings
|
||||
return ClientInstance.settings
|
||||
end
|
||||
|
||||
function GetPlayerGameData(pid)
|
||||
|
@ -813,7 +745,7 @@ function SetReplayingShowCards(o)
|
|||
end
|
||||
|
||||
function CheckSurrenderAvailable(playedTime)
|
||||
local curMode = ClientInstance.room_settings.gameMode
|
||||
local curMode = ClientInstance.settings.gameMode
|
||||
return Fk.game_modes[curMode]:surrenderFunc(playedTime)
|
||||
end
|
||||
|
||||
|
@ -822,17 +754,38 @@ function SaveRecord()
|
|||
c.client:saveRecord(json.encode(c.record), c.record[2])
|
||||
end
|
||||
|
||||
function GetCardProhibitReason(cid, method, pattern)
|
||||
function GetCardProhibitReason(cid)
|
||||
local card = Fk:getCardById(cid)
|
||||
if not card then return "" end
|
||||
local handler = ClientInstance.current_request_handler
|
||||
if (not handler) or (not handler:isInstanceOf(Fk.request_handlers["AskForUseActiveSkill"])) then return "" end
|
||||
local method, pattern = "", handler.pattern or "."
|
||||
|
||||
if handler.class.name == "ReqPlayCard" then method = "play"
|
||||
elseif handler.class.name == "ReqResponseCard" then method = "response"
|
||||
elseif handler.class.name == "ReqUseCard" then method = "use"
|
||||
elseif handler.skill_name == "discard_skill" then method = "discard"
|
||||
end
|
||||
|
||||
if method == "play" and not card.skill:canUse(Self, card) then return "" end
|
||||
if method ~= "play" and not card:matchPattern(pattern) then return "" end
|
||||
if method == "play" then method = "use" end
|
||||
|
||||
local fn_table = {
|
||||
use = "prohibitUse",
|
||||
response = "prohibitResponse",
|
||||
discard = "prohibitDiscard",
|
||||
}
|
||||
local str_table = {
|
||||
use = "method_use",
|
||||
response = "method_response_play",
|
||||
discard = "method_discard",
|
||||
}
|
||||
|
||||
local status_skills = Fk:currentRoom().status_skills[ProhibitSkill] or Util.DummyTable
|
||||
local s
|
||||
for _, skill in ipairs(status_skills) do
|
||||
local fn = method == "use" and skill.prohibitUse or skill.prohibitResponse
|
||||
local fn = skill[fn_table[method]]
|
||||
if fn(skill, Self, card) then
|
||||
s = skill
|
||||
break
|
||||
|
@ -844,14 +797,70 @@ function GetCardProhibitReason(cid, method, pattern)
|
|||
local skillName = s.name
|
||||
local ret = Fk:translate(skillName)
|
||||
if ret ~= skillName then
|
||||
return ret .. Fk:translate("prohibit") .. Fk:translate(method == "use" and "method_use" or "method_response_play")
|
||||
return ret .. Fk:translate("prohibit") .. Fk:translate(str_table[method])
|
||||
elseif skillName:endsWith("_prohibit") and skillName:startsWith("#") then
|
||||
return Fk:translate(skillName:sub(2, -10)) .. Fk:translate("prohibit") .. Fk:translate(method == "use" and "method_use" or "method_response_play")
|
||||
return Fk:translate(skillName:sub(2, -10)) .. Fk:translate("prohibit") .. Fk:translate(str_table[method])
|
||||
else
|
||||
return ret
|
||||
end
|
||||
end
|
||||
|
||||
function GetTargetTip(pid)
|
||||
local handler = ClientInstance.current_request_handler --[[@as ReqPlayCard ]]
|
||||
if (not handler) or (not handler:isInstanceOf(Fk.request_handlers["AskForUseActiveSkill"])) then return "" end
|
||||
|
||||
local to_select = pid
|
||||
local selected = handler.selected_targets
|
||||
local selected_cards = handler.pendings
|
||||
local card = handler.selected_card --[[@as Card?]]
|
||||
local skill = Fk.skills[handler.skill_name]
|
||||
local photo = handler.scene.items["Photo"][pid] --[[@as Photo]]
|
||||
local selectable = photo.enabled
|
||||
local extra_data = handler.extra_data
|
||||
|
||||
local ret = {}
|
||||
|
||||
if skill then
|
||||
if skill:isInstanceOf(ActiveSkill) then
|
||||
local tip = skill:targetTip(to_select, selected, selected_cards, nil, selectable)
|
||||
if type(tip) == "string" then
|
||||
table.insert(ret, { content = tip, type = "normal" })
|
||||
elseif type(tip) == "table" then
|
||||
table.insertTable(ret, tip)
|
||||
end
|
||||
elseif skill:isInstanceOf(ViewAsSkill) then
|
||||
card = skill:viewAs(selected_cards)
|
||||
end
|
||||
end
|
||||
|
||||
if card then
|
||||
local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or Util.DummyTable
|
||||
for _, sk in ipairs(status_skills) do
|
||||
ret = ret or {}
|
||||
if #ret > 4 then
|
||||
return ret
|
||||
end
|
||||
|
||||
local tip = sk:getTargetTip(Self, to_select, selected, selected_cards, card, selectable, extra_data)
|
||||
if type(tip) == "string" then
|
||||
table.insert(ret, { content = tip, 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 = tip, type = "normal" })
|
||||
elseif type(tip) == "table" then
|
||||
table.insertTable(ret, tip)
|
||||
end
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
function CanSortHandcards(pid)
|
||||
return ClientInstance:getPlayerById(pid):getMark(MarkEnum.SortProhibited) == 0
|
||||
end
|
||||
|
@ -940,7 +949,10 @@ function RevertSelection()
|
|||
h.scene:notifyUI()
|
||||
end
|
||||
|
||||
local requestUIUpdating = false
|
||||
function UpdateRequestUI(elemType, id, action, data)
|
||||
if requestUIUpdating then return end
|
||||
requestUIUpdating = true
|
||||
local h = ClientInstance.current_request_handler
|
||||
h.change = {}
|
||||
local finish = h:update(elemType, id, action, data)
|
||||
|
@ -949,12 +961,14 @@ function UpdateRequestUI(elemType, id, action, data)
|
|||
else
|
||||
h:_finish()
|
||||
end
|
||||
requestUIUpdating = false
|
||||
end
|
||||
|
||||
function FinishRequestUI()
|
||||
local h = ClientInstance.current_request_handler
|
||||
if h then
|
||||
h:_finish()
|
||||
ClientInstance.current_request_handler = nil
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -236,7 +236,7 @@ Fk:loadTranslationTable({
|
|||
["#replaceEquip"] = "Please Choose a Equip Card to be replaced",
|
||||
["#askForPindian"] = "%arg: please choose a hand card for point fight",
|
||||
["#StartPindianReason"] = "%from started point fight (%arg)",
|
||||
["#ShowPindianCard"] = "The point fight card of %from is %card",
|
||||
["#ShowPindianCard"] = "The point fight card of %from is %arg",
|
||||
["#ShowPindianResult"] = "%from %arg the point fight between %from and %to",
|
||||
["pindianwin"] = "won",
|
||||
["pindiannotwin"] = "lost",
|
||||
|
|
|
@ -205,6 +205,7 @@ FreeKill使用的是libgit2的C API,与此同时使用Git完成拓展包的下
|
|||
["Cancel"] = "取消",
|
||||
["End"] = "结束",
|
||||
-- ["Quit"] = "退出",
|
||||
["All"] = "全部",
|
||||
["BanGeneral"] = "禁将",
|
||||
["ResumeGeneral"] = "解禁",
|
||||
["Enable"] = "启用",
|
||||
|
@ -293,7 +294,7 @@ FreeKill使用的是libgit2的C API,与此同时使用Git完成拓展包的下
|
|||
["#replaceEquip"] = "选择一张装备牌替换之",
|
||||
["#askForPindian"] = "%arg:请选择一张手牌作为拼点牌",
|
||||
["#StartPindianReason"] = "%from 由于 %arg 而发起拼点",
|
||||
["#ShowPindianCard"] = "%from 的拼点牌是 %card",
|
||||
["#ShowPindianCard"] = "%from 的拼点牌是 %arg",
|
||||
["#ShowPindianResult"] = "%from 在 %from 和 %to 之间的拼点中 %arg",
|
||||
["pindianwin"] = "赢",
|
||||
["pindiannotwin"] = "没赢",
|
||||
|
|
|
@ -23,7 +23,7 @@ function Traceback()
|
|||
end
|
||||
|
||||
local msgh = function(err)
|
||||
fk.qCritical(err .. "\n" .. debug.traceback(nil, 2))
|
||||
fk.qCritical(tostring(err) .. "\n" .. debug.traceback(nil, 2))
|
||||
end
|
||||
|
||||
function Pcall(f, ...)
|
||||
|
@ -36,3 +36,10 @@ end
|
|||
|
||||
function p(v) print(inspect(v)) end
|
||||
function pt(t) for k, v in pairs(t) do print(k, v) end end
|
||||
|
||||
local _verbose = false
|
||||
function verbose(fmt, ...)
|
||||
if not _verbose then return end
|
||||
local str = fmt:format(...)
|
||||
fk.qInfo(str)
|
||||
end
|
||||
|
|
|
@ -19,10 +19,12 @@
|
|||
---@field public same_generals table<string, string[]> @ 所有同名武将组合
|
||||
---@field public lords string[] @ 所有主公武将,用于常备主公
|
||||
---@field public all_card_types table<string, Card> @ 所有的卡牌类型以及一张样板牌
|
||||
---@field public all_card_names string[] @ 有序的所有的卡牌牌名,顺序:基本牌(杀置顶),普通锦囊,延时锦囊,按副类别排序的装备
|
||||
---@field public cards Card[] @ 所有卡牌
|
||||
---@field public translations table<string, table<string, string>> @ 翻译表
|
||||
---@field public game_modes table<string, GameMode> @ 所有游戏模式
|
||||
---@field public game_mode_disabled table<string, string[]> @ 游戏模式禁用的包
|
||||
---@field public main_mode_list table<string, string[]> @ 主模式检索表
|
||||
---@field public currentResponsePattern string @ 要求用牌的种类(如要求用特定花色的桃···)
|
||||
---@field public currentResponseReason string @ 要求用牌的原因(如濒死,被特定牌指定,使用特定技能···)
|
||||
---@field public filtered_cards table<integer, Card> @ 被锁视技影响的卡牌
|
||||
|
@ -66,10 +68,12 @@ function Engine:initialize()
|
|||
self.same_generals = {}
|
||||
self.lords = {} -- lordName[]
|
||||
self.all_card_types = {}
|
||||
self.all_card_names = {}
|
||||
self.cards = {} -- Card[]
|
||||
self.translations = {} -- srcText --> translated
|
||||
self.game_modes = {}
|
||||
self.game_mode_disabled = {}
|
||||
self.main_mode_list = {}
|
||||
self.kingdoms = {}
|
||||
self.kingdom_map = {}
|
||||
self.damage_nature = { [fk.NormalDamage] = { "normal_damage", false } }
|
||||
|
@ -82,6 +86,7 @@ function Engine:initialize()
|
|||
|
||||
self:loadPackages()
|
||||
self:setLords()
|
||||
self:loadCardNames()
|
||||
self:loadDisabled()
|
||||
self:loadRequestHandlers()
|
||||
self:addSkills(AuxSkills)
|
||||
|
@ -265,9 +270,28 @@ function Engine:loadDisabled()
|
|||
for mode_name, game_mode in pairs(self.game_modes) do
|
||||
local disabled_packages = {}
|
||||
for name, pkg in pairs(self.packages) do
|
||||
if table.contains(game_mode.blacklist or Util.DummyTable, name) or
|
||||
(game_mode.whitelist and not table.contains(game_mode.whitelist, name)) or
|
||||
table.contains(pkg.game_modes_blacklist or Util.DummyTable, mode_name) or
|
||||
--- GameMode对Package筛选
|
||||
if type(game_mode.whitelist) == "function" then
|
||||
if not game_mode:whitelist(pkg) then
|
||||
table.insert(disabled_packages, name)
|
||||
end
|
||||
elseif type(game_mode.whitelist) == "table" then
|
||||
if not table.contains(game_mode.whitelist, name) then
|
||||
table.insert(disabled_packages, name)
|
||||
end
|
||||
end
|
||||
if type(game_mode.blacklist) == "function" then
|
||||
if game_mode:blacklist(pkg) then
|
||||
table.insert(disabled_packages, name)
|
||||
end
|
||||
elseif type(game_mode.blacklist) == "table" then
|
||||
if table.contains(game_mode.blacklist, name) then
|
||||
table.insert(disabled_packages, name)
|
||||
end
|
||||
end
|
||||
|
||||
--- Package对GameMode筛选
|
||||
if table.contains(pkg.game_modes_blacklist or Util.DummyTable, mode_name) or
|
||||
(pkg.game_modes_whitelist and not table.contains(pkg.game_modes_whitelist, mode_name)) then
|
||||
table.insert(disabled_packages, name)
|
||||
end
|
||||
|
@ -387,6 +411,7 @@ function Engine:setLords()
|
|||
for _, skill in ipairs(skills) do
|
||||
if skill.lordSkill then
|
||||
table.insert(self.lords, general.name)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -498,6 +523,7 @@ function Engine:addCard(card)
|
|||
if self.all_card_types[card.name] == nil then
|
||||
self.skills[card.skill.name] = card.skill
|
||||
self.all_card_types[card.name] = card
|
||||
table.insert(self.all_card_names, card.name)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -524,6 +550,23 @@ function Engine:cloneCard(name, suit, number)
|
|||
return ret
|
||||
end
|
||||
|
||||
--- 为所有加载的卡牌牌名排序
|
||||
function Engine:loadCardNames()
|
||||
local slash, basic, commonTrick, other = {}, {}, {}, {}
|
||||
for _, name in ipairs(self.all_card_names) do
|
||||
local card = self.all_card_types[name]
|
||||
if card.type == Card.TypeBasic then
|
||||
table.insert(card.trueName == "slash" and slash or basic, name)
|
||||
elseif card:isCommonTrick() then
|
||||
table.insert(commonTrick, name)
|
||||
else
|
||||
table.insert(other, name)
|
||||
end
|
||||
end
|
||||
table.sort(other, function(a, b) return self.all_card_types[a].sub_type < self.all_card_types[b].sub_type end)
|
||||
self.all_card_names = table.connect(slash, basic, commonTrick, other)
|
||||
end
|
||||
|
||||
--- 向Engine中添加一系列游戏模式。
|
||||
---@param game_modes GameMode[] @ 要添加的游戏模式列表
|
||||
function Engine:addGameModes(game_modes)
|
||||
|
|
|
@ -1,6 +1,22 @@
|
|||
-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
-- 呃 起码要做出以下几种吧:
|
||||
--- GameMode用来描述一个游戏模式。
|
||||
---
|
||||
--- 可以参考欢乐斗地主。
|
||||
---
|
||||
---@class GameMode: Object
|
||||
---@field public name string @ 游戏模式名
|
||||
---@field public minPlayer integer @ 最小玩家数
|
||||
---@field public maxPlayer integer @ 最大玩家数
|
||||
---@field public rule? TriggerSkill @ 规则(通过技能完成,通常用来为特定角色及特定时机提供触发事件)
|
||||
---@field public logic? fun(): GameLogic @ 逻辑(通过function完成,通常用来初始化、分配身份及座次)
|
||||
---@field public whitelist? string[] | fun(self: GameMode, pkg: Package): bool @ 白名单
|
||||
---@field public blacklist? string[] | fun(self: GameMode, pkg: Package): bool @ 黑名单
|
||||
---@field public config_template? GameModeConfigEntry[] 游戏模式的配置页面,如此一个数组
|
||||
---@field public main_mode? string @ 主模式名(用于判断此模式是否为某模式的衍生)
|
||||
local GameMode = class("GameMode")
|
||||
|
||||
-- 呃 起码要做出以下几种吧:(class必须放顶层否则文档那个东西不识别 辣鸡
|
||||
-- Switch:那个开关组件 [boolean model=nil]
|
||||
-- RadioButton:那个多选一,圆圈里面打点组件 [string model={ label, value }[]]
|
||||
-- ComboBox: 那个多选一,下拉一个菜单并选择一个的组件 [string model同上]
|
||||
|
@ -16,21 +32,6 @@
|
|||
---@field public model any @ 这种delegate需要的model:参见注释
|
||||
---@field public default? any @ 默认值 cfg的value
|
||||
|
||||
--- GameMode用来描述一个游戏模式。
|
||||
---
|
||||
--- 可以参考欢乐斗地主。
|
||||
---
|
||||
---@class GameMode: Object
|
||||
---@field public name string @ 游戏模式名
|
||||
---@field public minPlayer integer @ 最小玩家数
|
||||
---@field public maxPlayer integer @ 最大玩家数
|
||||
---@field public rule? TriggerSkill @ 规则(通过技能完成,通常用来为特定角色及特定时机提供触发事件)
|
||||
---@field public logic? fun(): GameLogic @ 逻辑(通过function完成,通常用来初始化、分配身份及座次)
|
||||
---@field public whitelist? string[] @ 白名单
|
||||
---@field public blacklist? string[] @ 黑名单
|
||||
---@field public config_template? GameModeConfigEntry[] 游戏模式的配置页面,如此一个数组
|
||||
local GameMode = class("GameMode")
|
||||
|
||||
--- 构造函数,不可随意调用。
|
||||
---@param name string @ 游戏模式名
|
||||
---@param min integer @ 最小玩家数
|
||||
|
@ -100,14 +101,17 @@ function GameMode:getAdjustedProperty (player)
|
|||
return list
|
||||
end
|
||||
|
||||
--- 向游戏模式中添加拓展包过滤。
|
||||
---@param whitelist string[] @ 白名单
|
||||
---@param blacklist string[] @ 黑名单
|
||||
function GameMode:addPackageFilter(whitelist, blacklist)
|
||||
assert(type(whitelist) == "table")
|
||||
assert(type(blacklist) == "table")
|
||||
table.insertTable(self.whitelist, whitelist)
|
||||
table.insertTable(self.blacklist, blacklist)
|
||||
|
||||
-- 执行死亡奖惩
|
||||
---@param victim ServerPlayer @ 死亡角色
|
||||
---@param killer? ServerPlayer @ 击杀者
|
||||
function GameMode:deathRewardAndPunish (victim, killer)
|
||||
if not killer or killer.dead then return end
|
||||
if victim.role == "rebel" then
|
||||
killer:drawCards(3, "kill")
|
||||
elseif victim.role == "loyalist" and killer.role == "lord" then
|
||||
killer:throwAllCards("he")
|
||||
end
|
||||
end
|
||||
|
||||
return GameMode
|
||||
|
|
|
@ -85,21 +85,4 @@ function Package:addGameMode(game_mode)
|
|||
table.insert(self.game_modes, game_mode)
|
||||
end
|
||||
|
||||
--- 向拓展包中设置游戏模式过滤。
|
||||
---@param whitelist string[] @ 白名单
|
||||
---@param blacklist string[] @ 黑名单
|
||||
function Package:setGameModeFilter(whitelist, blacklist)
|
||||
self.game_modes_whitelist = whitelist
|
||||
self.game_modes_blacklist = blacklist
|
||||
end
|
||||
|
||||
--- 向拓展包中添加游戏模式过滤。
|
||||
---@param whitelist string[] @ 白名单
|
||||
---@param blacklist string[] @ 黑名单
|
||||
function Package:addGameModeFilter(whitelist, blacklist)
|
||||
assert(type(whitelist) == "table")
|
||||
assert(type(blacklist) == "table")
|
||||
table.insertTable(self.game_modes_whitelist, whitelist)
|
||||
table.insertTable(self.game_modes_blacklist, blacklist)
|
||||
end
|
||||
return Package
|
||||
|
|
|
@ -1194,6 +1194,25 @@ function Player:isBuddy(other)
|
|||
return self.id == id or table.contains(self.buddy_list, id)
|
||||
end
|
||||
|
||||
local function defaultCardVisible(self, cardId, area, owner, falsy)
|
||||
local public_areas = {Card.DiscardPile, Card.Processing, Card.Void, Card.PlayerEquip, Card.PlayerJudge}
|
||||
local player_areas = {Card.PlayerHand, Card.PlayerSpecial}
|
||||
|
||||
if area == Card.DrawPile then return false
|
||||
elseif table.contains(public_areas, area) then return not falsy
|
||||
elseif table.contains(player_areas, area) then
|
||||
if area == Card.PlayerSpecial then
|
||||
local specialName = owner:getPileNameOfId(cardId)
|
||||
if not specialName:startsWith("$") then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return owner == self or self:isBuddy(owner)
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
--- Player是否可看到某card
|
||||
--- @param cardId integer
|
||||
---@param move? CardsMoveStruct
|
||||
|
@ -1203,11 +1222,15 @@ function Player:cardVisible(cardId, move)
|
|||
if room.replaying and room.replaying_show then return true end
|
||||
|
||||
local falsy = false -- 当难以决定时是否要选择暗置?
|
||||
local oldarea, oldowner
|
||||
if move then
|
||||
if table.find(move.moveInfo, function(info) return info.cardId == cardId end) then
|
||||
---@type MoveInfo
|
||||
local info = table.find(move.moveInfo, function(info) return info.cardId == cardId end)
|
||||
if info then
|
||||
oldarea = info.fromArea
|
||||
oldowner = move.from and room:getPlayerById(move.from)
|
||||
if move.moveVisible then return true end
|
||||
if move.moveVisible == false then falsy = true end
|
||||
-- specialVisible还要控制这个pile对他人是否应该可见,但是不在这里生效
|
||||
if move.specialVisible then return true end
|
||||
|
||||
if (type(move.visiblePlayers) == "number" and move.visiblePlayers == self.id) or
|
||||
|
@ -1218,11 +1241,9 @@ function Player:cardVisible(cardId, move)
|
|||
end
|
||||
|
||||
local area = room:getCardArea(cardId)
|
||||
local owner = room:getCardOwner(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 and not room.replaying then return table.contains(public_areas, area) end
|
||||
|
||||
local status_skills = Fk:currentRoom().status_skills[VisibilitySkill] or Util.DummyTable
|
||||
|
@ -1233,16 +1254,14 @@ function Player:cardVisible(cardId, move)
|
|||
end
|
||||
end
|
||||
|
||||
if area == Card.DrawPile then return false
|
||||
elseif table.contains(public_areas, area) then return not falsy
|
||||
elseif move and area == Card.PlayerSpecial and not move.specialName:startsWith("$") then
|
||||
return not falsy
|
||||
elseif table.contains(player_areas, area) then
|
||||
local to = room:getCardOwner(cardId)
|
||||
return to == self or self:isBuddy(to)
|
||||
else
|
||||
return false
|
||||
if defaultCardVisible(self, cardId, area, owner, falsy) then
|
||||
return true
|
||||
elseif oldarea then
|
||||
-- 尽可能让牌可见
|
||||
return defaultCardVisible(self, cardId, oldarea, oldowner, falsy)
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--- Player是否可看到某target的身份
|
||||
|
@ -1328,7 +1347,7 @@ function Player:loadJsonObject(o)
|
|||
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 _, ids in pairs(o.special_cards) do
|
||||
for _, id in ipairs(ids) do
|
||||
room:setCardArea(id, Card.PlayerSpecial, pid)
|
||||
end
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
---@field public change { [string]: Item[] } 将会传递给UI的更新数据
|
||||
local RequestHandler = class("RequestHandler")
|
||||
|
||||
function RequestHandler:initialize(player)
|
||||
function RequestHandler:initialize(player, data)
|
||||
self.room = Fk:currentRoom()
|
||||
self.player = player
|
||||
-- finish只在Client执行 用于保证UI执行了某些必须执行的善后
|
||||
|
|
|
@ -25,11 +25,18 @@ local CardItem = (require 'ui_emu.common').CardItem
|
|||
---@field public expanded_piles { [string]: integer[] } 用于展开/收起
|
||||
local ReqActiveSkill = RequestHandler:subclass("ReqActiveSkill")
|
||||
|
||||
function ReqActiveSkill:initialize(player)
|
||||
function ReqActiveSkill:initialize(player, data)
|
||||
RequestHandler.initialize(self, player)
|
||||
self.scene = RoomScene:new(self)
|
||||
|
||||
self.expanded_piles = {}
|
||||
|
||||
if data then
|
||||
self.skill_name = data[1]
|
||||
self.prompt = data[2]
|
||||
self.cancelable = data[3]
|
||||
self.extra_data = data[4]
|
||||
end
|
||||
end
|
||||
|
||||
function ReqActiveSkill:setup(ignoreInteraction)
|
||||
|
@ -55,6 +62,7 @@ function ReqActiveSkill:setup(ignoreInteraction)
|
|||
self:updateUnselectedTargets()
|
||||
|
||||
self:updateButtons()
|
||||
self:updatePrompt()
|
||||
end
|
||||
|
||||
function ReqActiveSkill:finish()
|
||||
|
@ -73,6 +81,15 @@ function ReqActiveSkill:setSkillPrompt(skill, cid)
|
|||
end
|
||||
end
|
||||
|
||||
function ReqActiveSkill:updatePrompt()
|
||||
local skill = Fk.skills[self.skill_name]
|
||||
if skill then
|
||||
self:setSkillPrompt(skill)
|
||||
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
|
||||
|
@ -80,7 +97,7 @@ function ReqActiveSkill:setupInteraction()
|
|||
if not interaction then
|
||||
return
|
||||
end
|
||||
skill.interaction.data = interaction.default_choice or nil -- FIXME
|
||||
skill.interaction.data = interaction.default or interaction.default_choice or nil -- FIXME
|
||||
-- 假设只有1个interaction (其实目前就是这样)
|
||||
local i = Interaction:new(self.scene, "1", interaction)
|
||||
i.skill_name = self.skill_name
|
||||
|
@ -291,11 +308,19 @@ function ReqActiveSkill:doOKButton()
|
|||
if self.selected_card then
|
||||
reply.special_skill = self.skill_name
|
||||
end
|
||||
ClientInstance:notifyUI("ReplyToServer", json.encode(reply))
|
||||
if ClientInstance then
|
||||
ClientInstance:notifyUI("ReplyToServer", json.encode(reply))
|
||||
else
|
||||
return reply
|
||||
end
|
||||
end
|
||||
|
||||
function ReqActiveSkill:doCancelButton()
|
||||
ClientInstance:notifyUI("ReplyToServer", "__cancel")
|
||||
if ClientInstance then
|
||||
ClientInstance:notifyUI("ReplyToServer", "__cancel")
|
||||
else
|
||||
return "__cancel"
|
||||
end
|
||||
end
|
||||
|
||||
-- 对点击卡牌的处理。data中包含selected属性,可能是选中或者取消选中,分开考虑。
|
||||
|
@ -365,6 +390,7 @@ function ReqActiveSkill:update(elemType, id, action, data)
|
|||
elseif elemType == "Interaction" then
|
||||
self:updateInteraction(data)
|
||||
end
|
||||
self:updatePrompt()
|
||||
end
|
||||
|
||||
return ReqActiveSkill
|
||||
|
|
|
@ -19,7 +19,6 @@ end
|
|||
function ReqPlayCard:setup()
|
||||
ReqUseCard.setup(self)
|
||||
|
||||
self:setPrompt(self.original_prompt)
|
||||
self.scene:update("Button", "End", { enabled = true })
|
||||
end
|
||||
|
||||
|
@ -85,14 +84,22 @@ 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
|
||||
local card = self.selected_card
|
||||
if self.skill_name then
|
||||
local skill = Fk.skills[self.skill_name]
|
||||
if skill:isInstanceOf(ActiveSkill) then
|
||||
return ReqActiveSkill.feasible(self)
|
||||
else -- viewasskill
|
||||
card = skill:viewAs(self.pendings)
|
||||
end
|
||||
end
|
||||
if card then
|
||||
local skill = card.skill ---@type ActiveSkill
|
||||
ret = skill:feasible(self.selected_targets, { card.id }, player, card)
|
||||
if ret then
|
||||
ret = skill:canUse(player, card, self.extra_data) and not player:prohibitUse(card)
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
@ -107,11 +114,11 @@ function ReqPlayCard:selectSpecialUse(data)
|
|||
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())
|
||||
-- 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)
|
||||
-- self:setSkillPrompt(Fk.skills[data], self.pendings)
|
||||
end
|
||||
self:initiateTargets()
|
||||
end
|
||||
|
@ -131,7 +138,11 @@ end
|
|||
function ReqPlayCard:doEndButton()
|
||||
self.scene:update("SpecialSkills", "1", { skills = {} })
|
||||
self.scene:notifyUI()
|
||||
ClientInstance:notifyUI("ReplyToServer", "")
|
||||
if ClientInstance then
|
||||
ClientInstance:notifyUI("ReplyToServer", "")
|
||||
else
|
||||
return ""
|
||||
end
|
||||
end
|
||||
|
||||
function ReqPlayCard:selectCard(cid, data)
|
||||
|
@ -146,7 +157,7 @@ function ReqPlayCard:selectCard(cid, data)
|
|||
self.skill_name = nil
|
||||
self.selected_card = Fk:getCardById(cid)
|
||||
scene:unselectOtherCards(cid)
|
||||
self:setSkillPrompt(self.selected_card.skill, self.selected_card:getEffectiveId())
|
||||
-- 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)
|
||||
|
|
|
@ -17,6 +17,19 @@ local ReqActiveSkill = require 'core.request_type.active_skill'
|
|||
---@field public original_prompt string 最开始的提示信息;这种涉及技能按钮的需要这样一下
|
||||
local ReqResponseCard = ReqActiveSkill:subclass("ReqResponseCard")
|
||||
|
||||
function ReqResponseCard:initialize(player, data)
|
||||
ReqActiveSkill.initialize(self, player)
|
||||
|
||||
if data then
|
||||
-- self.skill_name = data[1] (skill_name是给选中的视为技用的)
|
||||
self.pattern = data[2]
|
||||
self.prompt = data[3]
|
||||
self.cancelable = data[4]
|
||||
self.extra_data = data[5]
|
||||
self.disabledSkillNames = data[6]
|
||||
end
|
||||
end
|
||||
|
||||
function ReqResponseCard:setup()
|
||||
if not self.original_prompt then
|
||||
self.original_prompt = self.prompt or ""
|
||||
|
@ -25,6 +38,7 @@ function ReqResponseCard:setup()
|
|||
ReqActiveSkill.setup(self)
|
||||
self.selected_card = nil
|
||||
self:updateSkillButtons()
|
||||
self:updatePrompt()
|
||||
end
|
||||
|
||||
-- FIXME: 关于&牌堆的可使用打出瞎jb写了点 来个懂哥优化一下
|
||||
|
@ -41,8 +55,12 @@ end
|
|||
function ReqResponseCard:skillButtonValidity(name)
|
||||
local player = self.player
|
||||
local skill = Fk.skills[name]
|
||||
return skill:isInstanceOf(ViewAsSkill) and skill:enabledAtResponse(player, true)
|
||||
and skill.pattern and Exppattern:Parse(self.pattern):matchExp(skill.pattern)
|
||||
return
|
||||
skill:isInstanceOf(ViewAsSkill) and
|
||||
skill:enabledAtResponse(player, true) and
|
||||
skill.pattern and
|
||||
Exppattern:Parse(self.pattern):matchExp(skill.pattern) and
|
||||
not table.contains(self.disabledSkillNames or {}, name)
|
||||
end
|
||||
|
||||
function ReqResponseCard:cardValidity(cid)
|
||||
|
@ -86,7 +104,11 @@ function ReqResponseCard:doOKButton()
|
|||
card = self.selected_card:getEffectiveId(),
|
||||
targets = self.selected_targets,
|
||||
}
|
||||
ClientInstance:notifyUI("ReplyToServer", json.encode(reply))
|
||||
if ClientInstance then
|
||||
ClientInstance:notifyUI("ReplyToServer", json.encode(reply))
|
||||
else
|
||||
return reply
|
||||
end
|
||||
end
|
||||
|
||||
function ReqResponseCard:doCancelButton()
|
||||
|
@ -109,9 +131,10 @@ function ReqResponseCard:selectSkill(skill, data)
|
|||
end
|
||||
self.skill_name = skill
|
||||
self.selected_card = nil
|
||||
self:setSkillPrompt(skill)
|
||||
|
||||
ReqActiveSkill.setup(self)
|
||||
|
||||
-- self:setSkillPrompt(Fk.skills[skill])
|
||||
else
|
||||
self.skill_name = nil
|
||||
self.prompt = self.original_prompt
|
||||
|
|
|
@ -4,11 +4,27 @@ local ReqResponseCard = require 'core.request_type.response_card'
|
|||
---@class ReqUseCard: ReqResponseCard
|
||||
local ReqUseCard = ReqResponseCard:subclass("ReqUseCard")
|
||||
|
||||
function ReqUseCard:updatePrompt()
|
||||
if self.skill_name then
|
||||
return ReqActiveSkill.updatePrompt(self)
|
||||
end
|
||||
local card = self.selected_card
|
||||
if card and card.skill then
|
||||
self:setSkillPrompt(card.skill, self.selected_card.id)
|
||||
else
|
||||
self:setPrompt(self.original_prompt or "")
|
||||
end
|
||||
end
|
||||
|
||||
function ReqUseCard:skillButtonValidity(name)
|
||||
local player = self.player
|
||||
local skill = Fk.skills[name]
|
||||
return skill:isInstanceOf(ViewAsSkill) and skill:enabledAtResponse(player, false)
|
||||
and skill.pattern and Exppattern:Parse(self.pattern):matchExp(skill.pattern)
|
||||
return
|
||||
skill:isInstanceOf(ViewAsSkill) and
|
||||
skill:enabledAtResponse(player, false) and
|
||||
skill.pattern and
|
||||
Exppattern:Parse(self.pattern):matchExp(skill.pattern) and
|
||||
not table.contains(self.disabledSkillNames or {}, name)
|
||||
end
|
||||
|
||||
function ReqUseCard:cardValidity(cid)
|
||||
|
@ -45,7 +61,7 @@ function ReqUseCard:targetValidity(pid)
|
|||
-- 若include中全都没选,且target不在include中则不可选择
|
||||
if table.every(data.include_targets, function(id)
|
||||
return not table.contains(self.selected_targets, id)
|
||||
end) and not table.contains(data.must_targets, pid) then
|
||||
end) and not table.contains(data.include_targets, pid) then
|
||||
ret = false
|
||||
end
|
||||
end
|
||||
|
@ -108,7 +124,7 @@ function ReqUseCard:selectTarget(playerid, data)
|
|||
self.selected_targets = {}
|
||||
for _, pid in ipairs(previous_targets) do
|
||||
local ret
|
||||
ret = not player:isProhibited(pid, card) and skill and
|
||||
ret = not player:isProhibited(self.room:getPlayerById(pid), card) and skill and
|
||||
skill:targetFilter(pid, self.selected_targets,
|
||||
{ card.id }, card, data.extra_data)
|
||||
-- 从头开始写目标
|
||||
|
|
|
@ -89,4 +89,11 @@ function AbstractRoom:loadJsonObject(o)
|
|||
end
|
||||
end
|
||||
|
||||
-- 判断当前模式是否为某类模式
|
||||
---@param mode string @ 需要判定的模式类型
|
||||
---@return boolean
|
||||
function AbstractRoom:isGameMode(mode)
|
||||
return table.contains(Fk.main_mode_list[mode] or {}, self.settings.gameMode)
|
||||
end
|
||||
|
||||
return AbstractRoom
|
||||
|
|
|
@ -156,4 +156,16 @@ function Skill:getTimes()
|
|||
return ret
|
||||
end
|
||||
|
||||
-- 获得此技能时,触发此函数
|
||||
---@param player ServerPlayer
|
||||
---@param is_start bool
|
||||
function Skill:onAcquire(player, is_start)
|
||||
end
|
||||
|
||||
-- 失去此技能时,触发此函数
|
||||
---@param player ServerPlayer
|
||||
---@param is_death bool
|
||||
function Skill:onLose(player, is_death)
|
||||
end
|
||||
|
||||
return Skill
|
||||
|
|
|
@ -57,15 +57,18 @@ end
|
|||
-- DO NOT modify this function
|
||||
function TriggerSkill:doCost(event, target, player, data)
|
||||
local start_time = os.getms()
|
||||
local room = player.room
|
||||
room.current_cost_skill = self
|
||||
local ret = self:cost(event, target, player, data)
|
||||
local end_time = os.getms()
|
||||
|
||||
local room = player.room
|
||||
-- 对于那种cost直接返回true的锁定技,如果是预亮技,那么还是询问一下好
|
||||
if ret and player:isFakeSkill(self) and end_time - start_time < 10000 and
|
||||
if ret and player:isFakeSkill(self) and end_time - start_time < 1000 and
|
||||
(self.main_skill and self.main_skill or self).visible then
|
||||
ret = room:askForSkillInvoke(player, self.name)
|
||||
end
|
||||
room.current_cost_skill = nil
|
||||
|
||||
local cost_data_bak = self.cost_data
|
||||
room.logic:trigger(fk.BeforeTriggerSkillUse, player, { skill = self, willUse = ret })
|
||||
|
|
|
@ -92,4 +92,25 @@ function UsableSkill:withinTimesLimit(player, scope, card, card_name, to)
|
|||
-- end)))
|
||||
end
|
||||
|
||||
-- 失去此技能时,触发此函数
|
||||
---@param player ServerPlayer
|
||||
---@param is_death bool
|
||||
function UsableSkill:onLose(player, is_death)
|
||||
local lost_piles = {}
|
||||
if self.derived_piles then
|
||||
for _, pile_name in ipairs(self.derived_piles) do
|
||||
table.insertTableIfNeed(lost_piles, player:getPile(pile_name))
|
||||
end
|
||||
end
|
||||
|
||||
if #lost_piles > 0 then
|
||||
player.room:moveCards({
|
||||
ids = lost_piles,
|
||||
from = player.id,
|
||||
toArea = Card.DiscardPile,
|
||||
moveReason = fk.ReasonPutIntoDiscardPile,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
return UsableSkill
|
||||
|
|
|
@ -98,15 +98,15 @@ end
|
|||
---@param val_func? fun(e: T): integer @ 计算权值的函数,对int[]可不写
|
||||
---@param reverse? boolean @ 是否反排?反排的话优先返回权值小的元素
|
||||
function fk.sorted_pairs(t, val_func, reverse)
|
||||
val_func = val_func or function(e) return e end
|
||||
local t2 = table.simpleClone(t) -- 克隆一次表,用作迭代器上值
|
||||
local t_vals = table.map(t2, val_func or function(e) return e end)
|
||||
local iter = function()
|
||||
local max_idx, max, max_val = -1, nil, nil
|
||||
for i, v in ipairs(t2) do
|
||||
if not max then
|
||||
max_idx, max, max_val = i, v, val_func(v)
|
||||
max_idx, max, max_val = i, v, t_vals[i]
|
||||
else
|
||||
local val = val_func(v)
|
||||
local val = t_vals[i]
|
||||
local checked = val > max_val
|
||||
if reverse then checked = not checked end
|
||||
if checked then
|
||||
|
@ -116,6 +116,7 @@ function fk.sorted_pairs(t, val_func, reverse)
|
|||
end
|
||||
if max_idx == -1 then return nil, nil end
|
||||
table.remove(t2, max_idx)
|
||||
table.remove(t_vals, max_idx)
|
||||
return -1, max, max_val
|
||||
end
|
||||
return iter, nil, 1
|
||||
|
@ -729,4 +730,15 @@ function AimGroup:getCancelledTargets(aimGroup)
|
|||
return aimGroup[AimGroup.Cancelled]
|
||||
end
|
||||
|
||||
---@param target ServerPlayer
|
||||
---@param data AimStruct
|
||||
---@return boolean
|
||||
function AimGroup:isOnlyTarget(target, data)
|
||||
if data.tos == nil then return false end
|
||||
local tos = AimGroup:getAllTargets(data.tos)
|
||||
return table.contains(tos, target.id) and not table.find(target.room.alive_players, function (p)
|
||||
return p ~= target and table.contains(tos, p.id)
|
||||
end)
|
||||
end
|
||||
|
||||
return { TargetGroup, AimGroup, Util }
|
||||
|
|
|
@ -45,6 +45,16 @@ local function readCommonSpecToSkill(skill, spec)
|
|||
assert(type(spec.relate_to_place) == "string")
|
||||
skill.relate_to_place = spec.relate_to_place
|
||||
end
|
||||
|
||||
if spec.on_acquire then
|
||||
assert(type(spec.on_acquire) == "function")
|
||||
skill.onAcquire = spec.on_acquire
|
||||
end
|
||||
|
||||
if spec.on_lose then
|
||||
assert(type(spec.on_lose) == "function")
|
||||
skill.onLose = spec.on_lose
|
||||
end
|
||||
end
|
||||
|
||||
local function readUsableSpecToSkill(skill, spec)
|
||||
|
@ -88,6 +98,8 @@ end
|
|||
---@field public max_round_use_time? integer
|
||||
---@field public max_game_use_time? integer
|
||||
---@field public times? integer | fun(self: UsableSkill): integer
|
||||
---@field public on_acquire? fun(self: UsableSkill, player: ServerPlayer, is_start: boolean)
|
||||
---@field public on_lose? fun(self: UsableSkill, player: ServerPlayer, is_death: boolean)
|
||||
|
||||
---@class StatusSkillSpec: StatusSkill
|
||||
|
||||
|
@ -191,6 +203,7 @@ end
|
|||
---@field public mod_target_filter? fun(self: ActiveSkill, to_select: integer, selected: integer[], user: integer, card: Card, distance_limited: boolean): boolean?
|
||||
---@field public prompt? string|fun(self: ActiveSkill, selected_cards: integer[], selected_targets: integer[]): string
|
||||
---@field public interaction any
|
||||
---@field public target_tip? fun(self: ActiveSkill, to_select: integer, selected: integer[], selected_cards: integer[], card: Card, selectable: boolean, extra_data: any): string|TargetTipDataSpec?
|
||||
|
||||
---@param spec ActiveSkillSpec
|
||||
---@return ActiveSkill
|
||||
|
@ -635,7 +648,14 @@ function fk.CreateTreasure(spec)
|
|||
return card
|
||||
end
|
||||
|
||||
---@param spec GameMode
|
||||
---@class GameModeSpec: GameMode
|
||||
---@field public winner_getter? fun(self: GameMode, victim: ServerPlayer): string
|
||||
---@field public surrender_func? fun(self: GameMode, playedTime: number): string
|
||||
---@field public is_counted? fun(self: GameMode, room: Room): boolean
|
||||
---@field public get_adjusted? fun(self: GameMode, player: ServerPlayer): table
|
||||
---@field public reward_punish? fun(self: GameMode, victim: ServerPlayer, killer: ServerPlayer)
|
||||
|
||||
---@param spec GameModeSpec
|
||||
---@return GameMode
|
||||
function fk.CreateGameMode(spec)
|
||||
assert(type(spec.name) == "string")
|
||||
|
@ -646,6 +666,9 @@ function fk.CreateGameMode(spec)
|
|||
ret.blacklist = spec.blacklist
|
||||
ret.rule = spec.rule
|
||||
ret.logic = spec.logic
|
||||
ret.main_mode = spec.main_mode or spec.name
|
||||
Fk.main_mode_list[ret.main_mode] = Fk.main_mode_list[ret.main_mode] or {}
|
||||
table.insert(Fk.main_mode_list[ret.main_mode], ret.name)
|
||||
|
||||
if spec.winner_getter then
|
||||
assert(type(spec.winner_getter) == "function")
|
||||
|
@ -663,6 +686,10 @@ function fk.CreateGameMode(spec)
|
|||
assert(type(spec.get_adjusted) == "function")
|
||||
ret.getAdjustedProperty = spec.get_adjusted
|
||||
end
|
||||
if spec.reward_punish then
|
||||
assert(type(spec.reward_punish) == "function")
|
||||
ret.deathRewardAndPunish = spec.reward_punish
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
|
|
|
@ -79,17 +79,18 @@ end
|
|||
Config = loadConf()
|
||||
|
||||
-- 禁用各种危险的函数,尽可能让Lua执行安全的代码。
|
||||
local _os = {
|
||||
os = {
|
||||
time = os.time,
|
||||
date = os.date,
|
||||
clock = os.clock,
|
||||
difftime = os.difftime,
|
||||
getms = os.getms,
|
||||
}
|
||||
os = _os
|
||||
io = nil
|
||||
io = {
|
||||
lines = io.lines
|
||||
}
|
||||
package = nil
|
||||
load = nil
|
||||
-- load = nil
|
||||
loadfile = nil
|
||||
local _dofile = dofile
|
||||
dofile = function(f)
|
||||
|
|
|
@ -245,7 +245,9 @@ local function where(info, context_lines)
|
|||
local filename = info.source:match("@(.*)")
|
||||
if filename then
|
||||
if UsingNewCore and (filename:startsWith("./lua/") or filename:startsWith("lua/")) then
|
||||
filename = "./packages/freekill-core/" .. filename
|
||||
if not FileIO.pwd():endsWith("packages/freekill-core") then
|
||||
filename = "./packages/freekill-core/" .. filename
|
||||
end
|
||||
end
|
||||
pcall(function() for line in io.lines(filename) do table.insert(source, line) end end)
|
||||
elseif info.source then
|
||||
|
|
|
@ -24,7 +24,10 @@ function Object:initialize(...) end
|
|||
---@return T
|
||||
function Object:new(...)end
|
||||
|
||||
---@generic T
|
||||
---@param self T
|
||||
---@param name string
|
||||
---@return T
|
||||
function Object:subclass(name)end
|
||||
|
||||
---@param class class|Object
|
||||
|
|
|
@ -7,39 +7,222 @@
|
|||
---@field public room Room
|
||||
---@field public player ServerPlayer
|
||||
---@field public command string
|
||||
---@field public jsonData string
|
||||
---@field public cb_table table<string, fun(self: AI, jsonData: string)>
|
||||
---@field public data any
|
||||
---@field public handler ReqActiveSkill 可能空,但是没打问号(免得一堆警告)
|
||||
local AI = class("AI")
|
||||
|
||||
function AI:initialize(player)
|
||||
---@diagnostic disable-next-line
|
||||
self.room = RoomInstance
|
||||
self.player = player
|
||||
local cb_t = {}
|
||||
-- default strategy: print command and data, then return ""
|
||||
setmetatable(cb_t, {
|
||||
__index = function()
|
||||
return function()
|
||||
print(self.command, self.jsonData)
|
||||
return ""
|
||||
end
|
||||
end,
|
||||
})
|
||||
self.cb_table = cb_t
|
||||
end
|
||||
|
||||
function AI:readRequestData()
|
||||
self.command = self.player.ai_data.command
|
||||
self.jsonData = self.player.ai_data.jsonData
|
||||
function AI:__tostring()
|
||||
return string.format("%s: %s", self.class.name, tostring(self.player))
|
||||
end
|
||||
|
||||
-- activeSkill, responseCard, useCard, playCard 四巨头专属
|
||||
function AI:isInDashboard()
|
||||
if not (self.handler and self.handler:isInstanceOf(Fk.request_handlers["AskForUseActiveSkill"])) then
|
||||
fk.qWarning("请检查是否在AI中调用了专属于dashboard操作的一系列函数")
|
||||
fk.qWarning(debug.traceback())
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function AI:getPrompt()
|
||||
local handler = self.handler
|
||||
if not handler then return "" end
|
||||
return handler.prompt
|
||||
end
|
||||
|
||||
--- 返回当前手牌区域内(包含展开的pile)中所有可选且未选中的卡牌 返回ids
|
||||
---@param pattern string? 可以带一个过滤条件
|
||||
---@return integer[]
|
||||
function AI:getEnabledCards(pattern)
|
||||
if not self:isInDashboard() then return Util.DummyTable end
|
||||
|
||||
local ret = {}
|
||||
for cid, item in pairs(self.handler.scene:getAllItems("CardItem")) do
|
||||
if item.enabled and not item.selected then
|
||||
if (not pattern) or Exppattern:Parse(pattern):match(Fk:getCardById(cid)) then
|
||||
table.insert(ret, cid)
|
||||
end
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
--- 返回当前所有可选并且还未选中的角色,包括自己
|
||||
---@return ServerPlayer[]
|
||||
function AI:getEnabledTargets()
|
||||
if not self:isInDashboard() then return Util.DummyTable end
|
||||
local room = self.room
|
||||
|
||||
local ret = {}
|
||||
for pid, item in pairs(self.handler.scene:getAllItems("Photo")) do
|
||||
if item.enabled and not item.selected then
|
||||
table.insert(ret, room:getPlayerById(pid))
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
function AI:hasEnabledTarget()
|
||||
if not self:isInDashboard() then return false end
|
||||
local room = self.room
|
||||
|
||||
for _, item in pairs(self.handler.scene:getAllItems("Photo")) do
|
||||
if item.enabled and not item.selected then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--- 获取技能面板中所有可以按下的技能按钮
|
||||
---@return string[]
|
||||
|
||||
--- 获取技能面板中所有可以按下的技能按钮
|
||||
---@return string[]
|
||||
function AI:getEnabledSkills()
|
||||
if not self:isInDashboard() then return Util.DummyTable end
|
||||
|
||||
local ret = {}
|
||||
for name, item in pairs(self.handler.scene:getAllItems("SkillButton")) do
|
||||
if item.enabled and not item.selected then
|
||||
table.insert(ret, name)
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
---@return integer[]
|
||||
function AI:getSelectedCards()
|
||||
if not self:isInDashboard() then return Util.DummyTable end
|
||||
return self.handler.pendings
|
||||
end
|
||||
|
||||
---@return Card?
|
||||
function AI:getSelectedCard()
|
||||
if not self:isInDashboard() then return Util.DummyTable end
|
||||
local handler = self.handler
|
||||
if handler.selected_card then return handler.selected_card end
|
||||
if not handler.skill_name then return end
|
||||
local skill = Fk.skills[handler.skill_name]
|
||||
if not skill:isInstanceOf(ViewAsSkill) then return end
|
||||
return skill:viewAs(handler.pendings)
|
||||
end
|
||||
|
||||
---@return ServerPlayer[]
|
||||
function AI:getSelectedTargets()
|
||||
if not self:isInDashboard() then return Util.DummyTable end
|
||||
return table.map(self.handler.selected_targets, function(pid)
|
||||
return self.room:getPlayerById(pid)
|
||||
end)
|
||||
end
|
||||
|
||||
function AI:getSelectedSkill()
|
||||
if not self:isInDashboard() then return nil end
|
||||
return self.handler.skill_name
|
||||
end
|
||||
|
||||
function AI:selectCard(cid, selected)
|
||||
if not self:isInDashboard() then return end
|
||||
verbose("%s选择卡牌%d(%s)", selected and "" or "取消", cid, tostring(Fk:getCardById(cid)))
|
||||
self.handler:update("CardItem", cid, "click", { selected = selected })
|
||||
end
|
||||
|
||||
---@param player ServerPlayer
|
||||
function AI:selectTarget(player, selected)
|
||||
if not self:isInDashboard() then return end
|
||||
verbose("%s选择角色%s", selected and "" or "取消", tostring(player))
|
||||
self.handler:update("Photo", player.id, "click", { selected = selected })
|
||||
end
|
||||
|
||||
function AI:selectSkill(skill_name, selected)
|
||||
if not self:isInDashboard() then return end
|
||||
verbose("%s选择技能%s", selected and "" or "取消", skill_name)
|
||||
self.handler:update("SkillButton", skill_name, "click", { selected = selected })
|
||||
end
|
||||
|
||||
function AI:unSelectAllCards()
|
||||
for _, id in ipairs(self:getSelectedCards()) do
|
||||
self:selectCard(id, false)
|
||||
end
|
||||
end
|
||||
|
||||
function AI:unSelectAllTargets()
|
||||
for _, p in ipairs(self:getSelectedTargets()) do
|
||||
self:selectTarget(p, false)
|
||||
end
|
||||
end
|
||||
|
||||
function AI:unSelectSkill()
|
||||
local skill = self:getSelectedSkill()
|
||||
if not skill then return end
|
||||
self:selectSkill(skill, false)
|
||||
end
|
||||
|
||||
function AI:unSelectAll()
|
||||
self:unSelectSkill()
|
||||
self:unSelectAllCards()
|
||||
self:unSelectAllTargets()
|
||||
end
|
||||
|
||||
function AI:okButtonEnabled()
|
||||
if not self:isInDashboard() then return false end
|
||||
return self.handler:feasible()
|
||||
end
|
||||
|
||||
function AI:isDeadend()
|
||||
if not self:isInDashboard() then return true end
|
||||
return (not self:okButtonEnabled()) and #self:getEnabledCards() == 0
|
||||
and #self:getEnabledTargets() == 0
|
||||
end
|
||||
|
||||
function AI:doOKButton()
|
||||
if not self:isInDashboard() then return end
|
||||
if not self:okButtonEnabled() then return "" end
|
||||
return self.handler:doOKButton()
|
||||
end
|
||||
|
||||
---@return Skill?
|
||||
function AI:currentSkill()
|
||||
local room = self.room
|
||||
if room.current_cost_skill then return room.current_cost_skill end
|
||||
local sname = room.logic:getCurrentSkillName()
|
||||
if sname then
|
||||
return Fk.skills[sname]
|
||||
end
|
||||
end
|
||||
|
||||
function AI:makeReply()
|
||||
Self = self.player
|
||||
-- local start = os.getms()
|
||||
local ret = self.cb_table[self.command] and self.cb_table[self.command](self, self.jsonData) or "__cancel"
|
||||
if ret == "" then ret = "__cancel" end
|
||||
-- local to_delay = 500 - (os.getms() - start) / 1000
|
||||
-- print(to_delay)
|
||||
-- self.room:delay(to_delay)
|
||||
local now = os.getms()
|
||||
local fn = self["handle" .. self.command]
|
||||
local is_active = self.command == "AskForUseActiveSkill"
|
||||
if is_active then
|
||||
local skill = Fk.skills[self.data[1]]
|
||||
skill._extra_data = self.data[4]
|
||||
end
|
||||
local ret = "__cancel"
|
||||
if fn then
|
||||
local handler_class = Fk.request_handlers[self.command]
|
||||
if handler_class then
|
||||
self.handler = handler_class:new(self.player, self.data)
|
||||
self.handler:setup()
|
||||
end
|
||||
ret = fn(self, self.data)
|
||||
end
|
||||
if ret == nil or ret == "" then ret = "__cancel" end
|
||||
self.handler = nil
|
||||
if is_active then
|
||||
local skill = Fk.skills[self.data[1]]
|
||||
skill._extra_data = Util.DummyTable
|
||||
end
|
||||
verbose("%s 在%.2fms后得出结果:%s", self.command, (os.getms() - now) / 1000, json.encode(ret))
|
||||
return ret
|
||||
end
|
||||
|
||||
|
|
|
@ -2,20 +2,29 @@
|
|||
|
||||
AI = require "server.ai.ai"
|
||||
TrustAI = require "server.ai.trust_ai"
|
||||
RandomAI = require "server.ai.random_ai"
|
||||
-- RandomAI = require "server.ai.random_ai"
|
||||
|
||||
--[[ 在release版暂时不启动。
|
||||
---[[ 在release版暂时不启动。
|
||||
SmartAI = require "server.ai.smart_ai"
|
||||
---[[ 调试中,暂且不加载额外的AI。
|
||||
-- load ai module from packages
|
||||
local directories = FileIO.ls("packages")
|
||||
require "packages.standard.ai"
|
||||
require "packages.standard_cards.ai"
|
||||
require "packages.maneuvering.ai"
|
||||
local directories
|
||||
if UsingNewCore then
|
||||
directories = FileIO.ls("..")
|
||||
require "standard_cards.ai"
|
||||
require "standard.ai"
|
||||
-- require "maneuvering.ai"
|
||||
else
|
||||
directories = FileIO.ls("packages")
|
||||
require "packages.standard.ai"
|
||||
require "packages.standard_cards.ai"
|
||||
require "packages.maneuvering.ai"
|
||||
end
|
||||
table.removeOne(directories, "standard")
|
||||
table.removeOne(directories, "standard_cards")
|
||||
table.removeOne(directories, "maneuvering")
|
||||
|
||||
--[[
|
||||
local _disable_packs = json.decode(fk.GetDisabledPacks())
|
||||
|
||||
for _, dir in ipairs(directories) do
|
||||
|
|
421
lua/server/ai/logic.lua
Normal file
421
lua/server/ai/logic.lua
Normal file
|
@ -0,0 +1,421 @@
|
|||
--- 用于对标room和room.logic的、专用于计算某一轮操作的收益的类。
|
||||
---
|
||||
--- 里面提供的方法和room尽可能完全一致,以便自动分析与手工编写简易预测流程。
|
||||
---@class AIGameLogic: Object
|
||||
---@field public ai SmartAI
|
||||
---@field public player ServerPlayer
|
||||
---@field public benefit integer
|
||||
local AIGameLogic = class("AIGameLogic")
|
||||
|
||||
---@param ai SmartAI
|
||||
function AIGameLogic:initialize(ai, base_benefit)
|
||||
self.benefit = base_benefit or 0
|
||||
self.ai = ai
|
||||
self.player = ai.player
|
||||
self.logic = self -- 用于处理room.logic 这样真的好么。。
|
||||
|
||||
self.owner_map = ai.room.owner_map
|
||||
self.card_place = ai.room.card_place
|
||||
end
|
||||
|
||||
function AIGameLogic:__index(_)
|
||||
return function() return Util.DummyTable end
|
||||
end
|
||||
|
||||
function AIGameLogic:getPlayerById(id)
|
||||
return self.ai.room:getPlayerById(id)
|
||||
end
|
||||
|
||||
function AIGameLogic:trigger(event, target, data)
|
||||
local ai = self.ai
|
||||
local logic = ai.room.logic
|
||||
local skills = logic.skill_table[event] or Util.DummyTable
|
||||
local _target = ai.room.current -- for iteration
|
||||
local player = _target
|
||||
local exit
|
||||
|
||||
repeat
|
||||
for _, skill in ipairs(skills) do
|
||||
local skill_ai = fk.ai_trigger_skills[skill.name]
|
||||
if skill_ai then
|
||||
exit = skill_ai:getCorrect(self, event, target, player, data)
|
||||
if exit then break end
|
||||
end
|
||||
end
|
||||
if exit then break end
|
||||
player = player.next
|
||||
until player == _target
|
||||
|
||||
return exit
|
||||
end
|
||||
|
||||
--- 血条、翻面、铁索等等的收益论(瞎jb乱填版)
|
||||
function AIGameLogic:setPlayerProperty(player, key, value)
|
||||
local orig = player[key]
|
||||
local benefit = 0
|
||||
if key == "hp" then
|
||||
benefit = (value - orig) * 200
|
||||
elseif key == "shield" then
|
||||
benefit = (value - orig) * 150
|
||||
elseif key == "chained" then
|
||||
benefit = value and -80 or 80
|
||||
elseif key == "faceup" then
|
||||
if value and not orig then
|
||||
benefit = 330
|
||||
elseif orig and not value then
|
||||
benefit = -330
|
||||
end
|
||||
end
|
||||
if self.ai:isEnemy(player) then benefit = -benefit end
|
||||
self.benefit = self.benefit + benefit
|
||||
end
|
||||
|
||||
--- 牌差收益论(瞎jb乱填版):
|
||||
--- 根据moveInfo判断玩家是拿牌还是掉牌进而暴力算收益
|
||||
---@param data CardsMoveStruct
|
||||
---@param info MoveInfo
|
||||
function AIGameLogic:applyMoveInfo(data, info)
|
||||
local benefit = 0
|
||||
|
||||
if data.from then
|
||||
if info.fromArea == Player.Hand then
|
||||
benefit = -90
|
||||
elseif info.fromArea == Player.Equip then
|
||||
benefit = -110
|
||||
elseif info.fromArea == Player.Judge then
|
||||
benefit = 180
|
||||
elseif info.fromArea == Player.Special then
|
||||
benefit = -60
|
||||
end
|
||||
|
||||
local from = data.from and self:getPlayerById(data.from)
|
||||
if from and self.ai:isEnemy(from) then benefit = -benefit end
|
||||
self.benefit = self.benefit + benefit
|
||||
benefit = 0
|
||||
end
|
||||
|
||||
if data.to then
|
||||
if data.toArea == Player.Hand then
|
||||
benefit = 90
|
||||
elseif data.toArea == Player.Equip then
|
||||
benefit = 110
|
||||
elseif data.toArea == Player.Judge then
|
||||
benefit = -180
|
||||
elseif data.toArea == Player.Special then
|
||||
benefit = 60
|
||||
end
|
||||
|
||||
local to = data.to and self:getPlayerById(data.to)
|
||||
if to and self.ai:isEnemy(to) then benefit = -benefit end
|
||||
self.benefit = self.benefit + benefit
|
||||
end
|
||||
end
|
||||
|
||||
--- 阉割版GameEvent: 专用于AI进行简单的收益推理。
|
||||
---
|
||||
--- 事件首先需要定义自己对于某某玩家的基础收益值,例如伤害事件对目标造成-200的
|
||||
--- 收益。事件还要定义自己包含的触发时机列表,根据时机列表考虑相关技能对本次
|
||||
--- 事件的收益修正,最终获得真正的收益值。
|
||||
---
|
||||
--- 事件用于即将选卡/选目标时,或者触发技AI思考自己对某事件影响时构造并计算收益,
|
||||
--- 因此很容易发生事件嵌套现象。为防止AI思考过久,必须对事件嵌套层数加以限制,
|
||||
--- 比如限制最多思考到两层嵌套;毕竟没算力只能让AI蠢点了
|
||||
---@class AIGameEvent: Object
|
||||
---@field public ai SmartAI
|
||||
---@field public logic AIGameLogic
|
||||
---@field public player ServerPlayer
|
||||
---@field public data any
|
||||
local AIGameEvent = class("AIGameEvent")
|
||||
|
||||
---@param ai_logic AIGameLogic
|
||||
function AIGameEvent:initialize(ai_logic, ...)
|
||||
self.room = ai_logic
|
||||
self.logic = ai_logic
|
||||
self.ai = ai_logic.ai
|
||||
self.player = self.ai.player
|
||||
self.data = { ... }
|
||||
end
|
||||
|
||||
-- 真正的收益计算函数:子类重写这个
|
||||
function AIGameEvent:exec()
|
||||
end
|
||||
|
||||
local _depth = 0
|
||||
|
||||
-- 用做API的收益计算函数,不要重写
|
||||
function AIGameEvent:getBenefit()
|
||||
local ret = true
|
||||
_depth = _depth + 1
|
||||
if _depth <= 30 then
|
||||
ret = self:exec()
|
||||
end
|
||||
_depth = _depth - 1
|
||||
return ret
|
||||
end
|
||||
|
||||
-- hp.lua
|
||||
|
||||
local ChangeHp = AIGameEvent:subclass("AIGameEvent.ChangeHp")
|
||||
fk.ai_events.ChangeHp = ChangeHp
|
||||
function ChangeHp:exec()
|
||||
local logic = self.logic
|
||||
local player, num, reason, skillName, damageStruct = table.unpack(self.data)
|
||||
|
||||
---@type HpChangedData
|
||||
local data = {
|
||||
num = num,
|
||||
reason = reason,
|
||||
skillName = skillName,
|
||||
damageEvent = damageStruct,
|
||||
}
|
||||
|
||||
if logic:trigger(fk.BeforeHpChanged, player, data) then
|
||||
return true
|
||||
end
|
||||
|
||||
logic:setPlayerProperty(player, "hp", math.min(player.hp + data.num, player.maxHp))
|
||||
logic:trigger(fk.HpChanged, player, data)
|
||||
end
|
||||
|
||||
function AIGameLogic:changeHp(player, num, reason, skillName, damageStruct)
|
||||
return not ChangeHp:new(self, player, num, reason, skillName, damageStruct):getBenefit()
|
||||
end
|
||||
|
||||
local Damage = AIGameEvent:subclass("AIGameEvent.Damage")
|
||||
fk.ai_events.Damage = Damage
|
||||
function Damage:exec()
|
||||
local logic = self.logic
|
||||
local damageStruct = table.unpack(self.data)
|
||||
if (not damageStruct.chain) and (not damageStruct.chain_table) and Fk:canChain(damageStruct.damageType) then
|
||||
damageStruct.chain_table = table.filter(self.ai.room:getOtherPlayers(damageStruct.to), function(p)
|
||||
return p.chained
|
||||
end)
|
||||
end
|
||||
|
||||
local stages = {}
|
||||
if not damageStruct.isVirtualDMG then
|
||||
stages = {
|
||||
{ fk.PreDamage, "from"},
|
||||
{ fk.DamageCaused, "from" },
|
||||
{ fk.DamageInflicted, "to" },
|
||||
}
|
||||
end
|
||||
|
||||
for _, struct in ipairs(stages) do
|
||||
local event, player = table.unpack(struct)
|
||||
if logic:trigger(event, damageStruct[player], damageStruct) then
|
||||
return true
|
||||
end
|
||||
if damageStruct.damage < 1 then return true end
|
||||
end
|
||||
|
||||
if not damageStruct.isVirtualDMG then
|
||||
ChangeHp:new(logic, damageStruct.to, -damageStruct.damage,
|
||||
"damage", damageStruct.skillName, damageStruct):getBenefit()
|
||||
end
|
||||
|
||||
logic:trigger(fk.Damage, damageStruct.from, damageStruct)
|
||||
logic:trigger(fk.Damaged, damageStruct.to, damageStruct)
|
||||
logic:trigger(fk.DamageFinished, damageStruct.to, damageStruct)
|
||||
|
||||
if damageStruct.chain_table and #damageStruct.chain_table > 0 then
|
||||
for _, p in ipairs(damageStruct.chain_table) do
|
||||
local dmg = {
|
||||
from = damageStruct.from,
|
||||
to = p,
|
||||
damage = damageStruct.damage,
|
||||
damageType = damageStruct.damageType,
|
||||
card = damageStruct.card,
|
||||
skillName = damageStruct.skillName,
|
||||
chain = true,
|
||||
}
|
||||
|
||||
Damage:new(logic, dmg):getBenefit()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function AIGameLogic:damage(damageStruct)
|
||||
return not Damage:new(self, damageStruct):getBenefit()
|
||||
end
|
||||
|
||||
local LoseHp = AIGameEvent:subclass("AIGameEvent.LoseHp")
|
||||
fk.ai_events.LoseHp = LoseHp
|
||||
LoseHp.exec = AIParser.parseEventFunc(GameEvent.LoseHp.main)
|
||||
|
||||
function AIGameLogic:loseHp(player, num, skillName)
|
||||
return not LoseHp:new(self, player, num, skillName):getBenefit()
|
||||
end
|
||||
|
||||
local Recover = AIGameEvent:subclass("AIGameEvent.Recover")
|
||||
fk.ai_events.Recover = Recover
|
||||
Recover.exec = AIParser.parseEventFunc(GameEvent.Recover.main)
|
||||
|
||||
function AIGameLogic:recover(recoverStruct)
|
||||
return not Recover:new(self, recoverStruct):getBenefit()
|
||||
end
|
||||
|
||||
-- skill.lua
|
||||
|
||||
local SkillEffect = AIGameEvent:subclass("AIGameEvent.SkillEffect")
|
||||
fk.ai_events.SkillEffect = SkillEffect
|
||||
function SkillEffect:exec()
|
||||
local logic = self.logic
|
||||
local effect_cb, player, skill, skill_data = table.unpack(self.data)
|
||||
local main_skill = skill.main_skill and skill.main_skill or skill
|
||||
|
||||
logic:trigger(fk.SkillEffect, player, main_skill)
|
||||
effect_cb()
|
||||
logic:trigger(fk.AfterSkillEffect, player, main_skill)
|
||||
end
|
||||
|
||||
function AIGameLogic:useSkill(player, skill, effect_cb, skill_data)
|
||||
return not SkillEffect:new(self, effect_cb, player, skill, skill_data or Util.DummyTable):getBenefit()
|
||||
end
|
||||
|
||||
-- movecard.lua
|
||||
local MoveCards = AIGameEvent:subclass("AIGameEvent.MoveCards")
|
||||
fk.ai_events.MoveCards = MoveCards
|
||||
function MoveCards:exec()
|
||||
local args = self.data
|
||||
local logic = self.logic
|
||||
local cardsMoveStructs = {}
|
||||
|
||||
for _, cardsMoveInfo in ipairs(args) do
|
||||
if #cardsMoveInfo.ids > 0 then
|
||||
---@type MoveInfo[]
|
||||
local infos = {}
|
||||
for _, id in ipairs(cardsMoveInfo.ids) do
|
||||
table.insert(infos, {
|
||||
cardId = id,
|
||||
fromArea = cardsMoveInfo.fromArea or self.ai.room:getCardArea(id),
|
||||
fromSpecialName = cardsMoveInfo.from and logic:getPlayerById(cardsMoveInfo.from):getPileNameOfId(id),
|
||||
})
|
||||
end
|
||||
|
||||
cardsMoveInfo.moveInfo = infos
|
||||
table.insert(cardsMoveStructs, cardsMoveInfo)
|
||||
end
|
||||
end
|
||||
|
||||
if logic:trigger(fk.BeforeCardsMove, nil, cardsMoveStructs) then
|
||||
return true
|
||||
end
|
||||
|
||||
for _, data in ipairs(cardsMoveStructs) do
|
||||
for _, info in ipairs(data.moveInfo) do
|
||||
logic:applyMoveInfo(data, info)
|
||||
end
|
||||
end
|
||||
|
||||
logic:trigger(fk.AfterCardsMove, nil, cardsMoveStructs)
|
||||
end
|
||||
|
||||
function AIGameLogic:getNCards(num, from)
|
||||
local cardIds = {}
|
||||
for _ = 1, num do
|
||||
table.insert(cardIds, 1)
|
||||
end
|
||||
return cardIds
|
||||
end
|
||||
|
||||
function AIGameLogic:moveCards(...)
|
||||
return not MoveCards:new(self, ...):getBenefit()
|
||||
end
|
||||
AIGameLogic.moveCardTo = GameEventWrappers.moveCardTo
|
||||
AIGameLogic.obtainCard = GameEventWrappers.obtainCard
|
||||
AIGameLogic.drawCards = GameEventWrappers.drawCards
|
||||
AIGameLogic.throwCard = GameEventWrappers.throwCard
|
||||
function AIGameLogic: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
|
||||
})
|
||||
return self:drawCards(who, #card_ids, skillName)
|
||||
end
|
||||
|
||||
-- usecard.lua
|
||||
|
||||
local UseCard = AIGameEvent:subclass("AIGameEvent.UseCard")
|
||||
fk.ai_events.UseCard = UseCard
|
||||
function UseCard:exec()
|
||||
local ai = self.ai
|
||||
local room = ai.room
|
||||
local logic = self.logic
|
||||
local cardUseEvent = table.unpack(self.data)
|
||||
|
||||
if logic:trigger(fk.PreCardUse, room:getPlayerById(cardUseEvent.from), cardUseEvent) then
|
||||
return true
|
||||
end
|
||||
logic:moveCardTo(cardUseEvent.card, Card.Processing, nil, fk.ReasonUse)
|
||||
|
||||
for _, event in ipairs({ fk.AfterCardUseDeclared, fk.AfterCardTargetDeclared, fk.CardUsing }) do
|
||||
if not cardUseEvent.toCard and #TargetGroup:getRealTargets(cardUseEvent.tos) == 0 then
|
||||
break
|
||||
end
|
||||
|
||||
logic:trigger(event, room:getPlayerById(cardUseEvent.from), cardUseEvent)
|
||||
if event == fk.CardUsing then
|
||||
logic:doCardUseEffect(cardUseEvent)
|
||||
end
|
||||
end
|
||||
|
||||
logic:trigger(fk.CardUseFinished, room:getPlayerById(cardUseEvent.from), cardUseEvent)
|
||||
logic:moveCards{
|
||||
fromArea = Card.Processing,
|
||||
toArea = Card.DiscardPile,
|
||||
ids = Card:getIdList(cardUseEvent.card),
|
||||
moveReason = fk.ReasonUse,
|
||||
}
|
||||
end
|
||||
|
||||
function AIGameLogic:useCard(cardUseEvent)
|
||||
return not UseCard:new(self, cardUseEvent):getBenefit()
|
||||
end
|
||||
|
||||
function AIGameLogic:deadPlayerFilter(playerIds)
|
||||
local newPlayerIds = {}
|
||||
for _, playerId in ipairs(playerIds) do
|
||||
if self:getPlayerById(playerId):isAlive() then
|
||||
table.insert(newPlayerIds, playerId)
|
||||
end
|
||||
end
|
||||
|
||||
return newPlayerIds
|
||||
end
|
||||
|
||||
AIGameLogic.doCardUseEffect = GameEventWrappers.doCardUseEffect
|
||||
|
||||
local CardEffect = AIGameEvent:subclass("AIGameEvent.CardEffect")
|
||||
fk.ai_events.CardEffect = CardEffect
|
||||
CardEffect.exec = AIParser.parseEventFunc(GameEvent.CardEffect.main)
|
||||
|
||||
function AIGameLogic:doCardEffect(cardEffectEvent)
|
||||
return not CardEffect:new(self, cardEffectEvent):getBenefit()
|
||||
end
|
||||
|
||||
function AIGameLogic:handleCardEffect(event, cardEffectEvent)
|
||||
-- 不考虑闪与无懈 100%生效
|
||||
-- 闪和无懈早该重构重构了
|
||||
if event == fk.CardEffecting then
|
||||
if cardEffectEvent.card.skill then
|
||||
SkillEffect:new(self, function()
|
||||
local skill = cardEffectEvent.card.skill
|
||||
local ai = fk.ai_skills[skill.name]
|
||||
if ai then
|
||||
ai:onEffect(self, cardEffectEvent)
|
||||
end
|
||||
end, self:getPlayerById(cardEffectEvent.from), cardEffectEvent.card.skill):getBenefit()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return AIGameLogic, AIGameEvent
|
|
@ -1,7 +0,0 @@
|
|||
-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
local MonteCarlo = class("MonteCarlo")
|
||||
|
||||
|
||||
|
||||
return MonteCarlo
|
58
lua/server/ai/parser.lua
Normal file
58
lua/server/ai/parser.lua
Normal file
|
@ -0,0 +1,58 @@
|
|||
--- 用于从on_use/on_effect等函数自动生成AI推理用的模拟流程
|
||||
|
||||
---@class AIParser
|
||||
local AIParser = {}
|
||||
|
||||
---@type table<string, string[]> 文件名-lines
|
||||
local loaded_files = {}
|
||||
|
||||
local function getLines(filename)
|
||||
if loaded_files[filename] then return loaded_files[filename] end
|
||||
if UsingNewCore then
|
||||
if filename:startsWith("./lua") then
|
||||
filename = "./packages/freekill-core/" .. filename
|
||||
end
|
||||
FileIO.cd("../..")
|
||||
end
|
||||
|
||||
local t = {}
|
||||
for line in io.lines(filename) do
|
||||
table.insert(t, line)
|
||||
end
|
||||
loaded_files[filename] = t
|
||||
|
||||
if UsingNewCore then
|
||||
FileIO.cd("packages/freekill-core")
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
local function getFunctionSource(fn)
|
||||
local info = debug.getinfo(fn, "S")
|
||||
local lines = getLines(info.short_src)
|
||||
return table.slice(lines, info.linedefined, info.lastlinedefined + 1)
|
||||
end
|
||||
|
||||
-- 最简单替换:breakEvent改成return
|
||||
function AIParser.parseEventFunc(fn)
|
||||
local sources = getFunctionSource(fn)
|
||||
local parsed = {}
|
||||
for i, line in ipairs(sources) do
|
||||
if i == 1 then
|
||||
table.insert(parsed, "return function(self)")
|
||||
else
|
||||
if line:find(":breakEvent%(") then
|
||||
line = "return true"
|
||||
end
|
||||
table.insert(parsed, line)
|
||||
end
|
||||
end
|
||||
return load(table.concat(parsed, '\n'))()
|
||||
end
|
||||
|
||||
function AIParser.parseEventWrapper(wrapperFn)
|
||||
local sources = getFunctionSource(wrapperFn)
|
||||
print(table.concat(sources, "\n"))
|
||||
end
|
||||
|
||||
return AIParser
|
|
@ -1,405 +0,0 @@
|
|||
-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
---@class RandomAI: AI
|
||||
local RandomAI = AI:subclass("RandomAI")
|
||||
|
||||
---@param self RandomAI
|
||||
---@param skill ActiveSkill
|
||||
---@param card? 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
|
||||
|
||||
if self.command == "PlayCard" and (not skill:canUse(player, card) or (card and player:prohibitUse(card))) then
|
||||
return ""
|
||||
end
|
||||
|
||||
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 = {}
|
||||
|
||||
-- TODO: ng that 'must_targets' & 'exclusive_targets' should be rebuilt later
|
||||
local limited_targets = {}
|
||||
for _, name in ipairs({"must_targets","exclusive_targets","include_targets"}) do
|
||||
if type(extra_data[name]) == "table" then
|
||||
table.insertTableIfNeed(limited_targets, extra_data[name])
|
||||
end
|
||||
end
|
||||
|
||||
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,
|
||||
}
|
||||
return ret
|
||||
end
|
||||
return ""
|
||||
end
|
||||
|
||||
|
||||
---@param skill ViewAsSkill
|
||||
---@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
|
||||
cancelable = cancelable or (cancelable == nil)
|
||||
extra_data = extra_data or Util.DummyTable
|
||||
if not skill then return "" end
|
||||
|
||||
if not pattern then
|
||||
precondition = skill:enabledAtPlay(player)
|
||||
if not precondition then return "" end
|
||||
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
|
||||
end
|
||||
for _, n in ipairs(cnames) do
|
||||
local c = Fk:cloneCard(n)
|
||||
precondition = c.skill:canUse(player, c, extra_data)
|
||||
if precondition then break end
|
||||
end
|
||||
else
|
||||
precondition = skill:enabledAtResponse(player, cardResponsing) and Exppattern:Parse(pattern):matchExp(skill.pattern)
|
||||
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
|
||||
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))
|
||||
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
|
||||
|
||||
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>
|
||||
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]
|
||||
for k, v in pairs(extra_data) do
|
||||
skill[k] = v
|
||||
end
|
||||
if skill:isInstanceOf(ViewAsSkill) then
|
||||
return self:useVSSkill(skill, nil, cancelable, extra_data)
|
||||
end
|
||||
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)
|
||||
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)
|
||||
local player = self.player
|
||||
local data = json.decode(jsonData)
|
||||
local card_name = data[1]
|
||||
local pattern = data[2] or card_name
|
||||
local prompt = data[3]
|
||||
local cancelable = data[4]
|
||||
local extra_data = data[5] or Util.DummyTable
|
||||
|
||||
if card_name == "peach" then
|
||||
if type(extra_data.must_targets) == "table" and extra_data.must_targets[1] ~= player.id and math.random() < 0.8 then
|
||||
return ""
|
||||
end
|
||||
end
|
||||
|
||||
if (cancelable and math.random() < 0.15) then return "" end
|
||||
|
||||
local cards = table.map(self.player:getCardIds("he&"), Util.Id2CardMapper)
|
||||
local exp = Exppattern:Parse(pattern)
|
||||
cards = table.filter(cards, function(c)
|
||||
return exp:match(c) and not player:prohibitUse(c)
|
||||
end)
|
||||
local vss = table.filter(player:getAllSkills(), function(s)
|
||||
return s:isInstanceOf(ViewAsSkill)
|
||||
end)
|
||||
table.insertTable(cards, vss)
|
||||
|
||||
while #cards > 0 do
|
||||
local sth = table.remove(cards, math.random(#cards))
|
||||
if sth:isInstanceOf(Card) then
|
||||
local ret = self:useActiveSkill(sth.skill, sth, extra_data)
|
||||
if ret ~= "" then return ret end
|
||||
else
|
||||
local ret = self:useVSSkill(sth, pattern, cancelable, extra_data)
|
||||
if ret ~= "" then return ret end
|
||||
end
|
||||
end
|
||||
|
||||
return ""
|
||||
end
|
||||
|
||||
random_cb["AskForResponseCard"] = function(self, jsonData)
|
||||
local data = json.decode(jsonData)
|
||||
local pattern = data[2]
|
||||
local cancelable = 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)
|
||||
cards = table.filter(cards, function(c)
|
||||
return exp:match(c) and not player:prohibitResponse(c)
|
||||
end)
|
||||
|
||||
local vss = table.filter(player:getAllSkills(), function(s)
|
||||
return s:isInstanceOf(ViewAsSkill)
|
||||
end)
|
||||
table.insertTable(cards, vss)
|
||||
|
||||
while #cards > 0 do
|
||||
local sth = table.remove(cards, math.random(#cards))
|
||||
if sth:isInstanceOf(Card) then
|
||||
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("h&"), Util.Id2CardMapper)
|
||||
local actives = table.filter(self.player:getAllSkills(), function(s)
|
||||
return s:isInstanceOf(ActiveSkill)
|
||||
end)
|
||||
local vss = table.filter(self.player:getAllSkills(), function(s)
|
||||
return s:isInstanceOf(ViewAsSkill)
|
||||
end)
|
||||
table.insertTable(cards, actives)
|
||||
table.insertTable(cards, vss)
|
||||
|
||||
while #cards > 0 do
|
||||
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
|
||||
end
|
||||
elseif sth:isInstanceOf(ActiveSkill) then
|
||||
local active = sth
|
||||
if math.random() > 0.30 then
|
||||
local ret = RandomAI.useActiveSkill(self, active, nil)
|
||||
if ret ~= "" then return ret end
|
||||
end
|
||||
else
|
||||
local vs = sth
|
||||
if math.random() > 0.20 then
|
||||
local ret = self:useVSSkill(vs)
|
||||
if ret ~= "" then return ret end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return ""
|
||||
end
|
||||
|
||||
-- FIXME: for smart ai
|
||||
RandomAI.cb_table = random_cb
|
||||
|
||||
function RandomAI:initialize(player)
|
||||
AI.initialize(self, player)
|
||||
self.cb_table = random_cb
|
||||
end
|
||||
|
||||
return RandomAI
|
82
lua/server/ai/skill.lua
Normal file
82
lua/server/ai/skill.lua
Normal file
|
@ -0,0 +1,82 @@
|
|||
--- 关于某个技能如何在AI中处理。
|
||||
---
|
||||
--- 相关方法分为三类,分别是如何搜索、如何计算收益、如何进行收益推理
|
||||
---
|
||||
--- 所谓搜索,就是如何确定下一步该选择哪张卡牌/哪名角色等。
|
||||
--- 默认情况下,AI选择收益最高的选项作为下一步,如果遇到了死胡同就返回考虑下一种。
|
||||
--- 所谓死胡同就是什么都不能点击,也不能点确定的状态,必须取消某些的选择。
|
||||
---
|
||||
--- 所谓的收益计算就是估算这个选项在这个技能的语境下,他大概会带来多少收益。
|
||||
--- 限于算力,我们不能编写太复杂的收益计算。默认情况下,收益可以通过推理完成,
|
||||
--- 而推理的步骤需要Modder向AI给出提示。
|
||||
---
|
||||
--- 所谓的给出提示就是上面的“如何进行收益推理”。拓展可以针对点击某张卡牌或者
|
||||
--- 点击某个角色,告诉AI这么点击了可能会发生某种事件。AI根据事件以及游戏内包含的
|
||||
--- 其他技能进行计算,得出收益值。若不想让它这样计算,也可以在上一步直接指定
|
||||
--- 固定的收益值。
|
||||
---
|
||||
--- 所谓的“可能发生某种事件”大致类似GameEvent,但是内部功能大幅简化了(因为
|
||||
--- 只是用于简单的推理)。详见同文件夹下event.lua内容。
|
||||
---@class SkillAI: Object
|
||||
---@field public skill ActiveSkill
|
||||
local SkillAI = class("SkillAI")
|
||||
|
||||
--- 收益估计
|
||||
---@param ai SmartAI
|
||||
---@return integer?
|
||||
function SkillAI:getEstimatedBenefit(ai) end
|
||||
|
||||
--- 要返回一个结果,以及收益值
|
||||
---@param ai SmartAI
|
||||
---@return any?, integer?
|
||||
function SkillAI:think(ai) end
|
||||
|
||||
---@param skill string
|
||||
function SkillAI:initialize(skill)
|
||||
self.skill = Fk.skills[skill]
|
||||
end
|
||||
|
||||
-- 搜索类方法:怎么走下一步?
|
||||
|
||||
---@param ai SmartAI
|
||||
function SkillAI:chooseInteraction(ai) end
|
||||
|
||||
---@param ai SmartAI
|
||||
function SkillAI:chooseCards(ai) end
|
||||
|
||||
---@param ai SmartAI
|
||||
---@return any, integer?
|
||||
function SkillAI:chooseTargets(ai) end
|
||||
|
||||
-- 流程模拟类方法:为了让AIGameLogic开心
|
||||
|
||||
--- 对触发技生效的模拟
|
||||
---@param logic AIGameLogic
|
||||
---@param event Event @ TriggerEvent
|
||||
---@param target ServerPlayer @ Player who triggered this event
|
||||
---@param player ServerPlayer @ Player who is operating
|
||||
---@param data any @ useful data of the event
|
||||
function SkillAI:onTriggerUse(logic, event, target, player, data) end
|
||||
|
||||
--- 对主动技生效/卡牌被使用时的模拟
|
||||
---@param logic AIGameLogic
|
||||
---@param event CardUseStruct | SkillEffectEvent
|
||||
function SkillAI:onUse(logic, event) end
|
||||
|
||||
--- 对卡牌生效的模拟
|
||||
---@param logic AIGameLogic
|
||||
---@param cardEffectEvent CardEffectEvent | SkillEffectEvent
|
||||
function SkillAI:onEffect(logic, cardEffectEvent) end
|
||||
|
||||
--- 最后效仿一下fk_ex故事
|
||||
---@class SkillAISpec
|
||||
---@field estimated_benefit? integer|fun(self: SkillAI, ai: SmartAI): integer?
|
||||
---@field think? fun(self: SkillAI, ai: SmartAI): any?, integer?
|
||||
---@field choose_interaction? fun(self: SkillAI, ai: SmartAI): boolean?
|
||||
---@field choose_cards? fun(self: SkillAI, ai: SmartAI): boolean?
|
||||
---@field choose_targets? fun(self: SkillAI, ai: SmartAI): any, integer?
|
||||
---@field on_trigger_use? fun(self: SkillAI, logic: AIGameLogic, event: Event, target: ServerPlayer?, player: ServerPlayer, data: any)
|
||||
---@field on_use? fun(self: SkillAI, logic: AIGameLogic, effect: SkillEffectEvent | CardEffectEvent)
|
||||
---@field on_effect? fun(self: SkillAI, logic: AIGameLogic, effect: SkillEffectEvent | CardEffectEvent)
|
||||
|
||||
return SkillAI
|
|
@ -1,41 +1,32 @@
|
|||
-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
--[[
|
||||
关于SmartAI: 一款参考神杀基本AI架构的AI体系。
|
||||
该文件加载了AI常用的种种表以及实用函数等,并提供了可供拓展自定义AI逻辑的接口。
|
||||
|
||||
AI的核心在于编程实现对各种交互的回应(或者说应付各种room:askForXXX),
|
||||
所以本文件的直接目的是编写出合适的函数充实smart_cb表以实现合理的答复,
|
||||
但为了实现这个目的就还要去额外实现敌友判断、收益计算等等功能。
|
||||
为了便于各个拓展快速编写AI,还要封装一些AI判断时常用的函数。
|
||||
一套基于收益论和简易收益预测的AI框架
|
||||
|
||||
本文件包含以下内容:
|
||||
1. 基本策略代码:定义各种全局表,以及smart_cb表
|
||||
2. 敌我相关代码:关于如何判断敌我以及更新意向值等
|
||||
3. 十分常用的各种函数(?)
|
||||
|
||||
-- TODO: 优化底层逻辑,防止AI每次操作之前都要json.decode一下。
|
||||
-- TODO: 更加详细的文档
|
||||
--]]
|
||||
|
||||
---@class SmartAI: AI
|
||||
---@class SmartAI: TrustAI
|
||||
---@field private _memory table<string, any> @ AI底层的空间换时间机制
|
||||
---@field public friends ServerPlayer[] @ 队友
|
||||
---@field public enemies ServerPlayer[] @ 敌人
|
||||
local SmartAI = AI:subclass("SmartAI")
|
||||
local SmartAI = TrustAI:subclass("SmartAI") -- 哦,我懒得写出闪之类的,不得不继承一下,饶了我吧
|
||||
|
||||
---@type table<string, fun(self: SmartAI, jsonData: string): string>
|
||||
local smart_cb = {}
|
||||
AIParser = require 'lua.server.ai.parser'
|
||||
SkillAI = require "lua.server.ai.skill"
|
||||
TriggerSkillAI = require "lua.server.ai.trigger_skill"
|
||||
|
||||
---@type table<string, AIGameEvent>
|
||||
fk.ai_events = {}
|
||||
AIGameLogic, AIGameEvent = require "lua.server.ai.logic"
|
||||
|
||||
function SmartAI:initialize(player)
|
||||
AI.initialize(self, player)
|
||||
self.cb_table = smart_cb
|
||||
self.player = player
|
||||
TrustAI.initialize(self, player)
|
||||
end
|
||||
|
||||
function SmartAI:makeReply()
|
||||
self._memory = {}
|
||||
return AI.makeReply(self)
|
||||
self._memory = setmetatable({}, { __mode = "k" })
|
||||
return TrustAI.makeReply(self)
|
||||
end
|
||||
|
||||
function SmartAI:__index(k)
|
||||
|
@ -56,201 +47,214 @@ function SmartAI:__index(k)
|
|||
return ret
|
||||
end
|
||||
|
||||
-- AI框架中常用的模式化函数。
|
||||
-- 先从表中选函数,若无则调用默认的。点点点是参数
|
||||
function SmartAI:callFromTable(func_table, default_func, key, ...)
|
||||
local f = func_table[key]
|
||||
if type(f) == "function" then
|
||||
return f(...)
|
||||
elseif type(default_func) == "function" then
|
||||
return default_func(...)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
-- 面板相关交互:对应操控手牌区、技能面板、直接选择目标的交互
|
||||
-- 对应UI中的"responding"状态和"playing"状态
|
||||
-- AI代码需要像实际操作UI那样完成以下几个任务:
|
||||
-- * 点击技能按钮(出牌阶段或者打算使用ViewAsSkill)
|
||||
-- * 技能如果带有interaction,则选择interaction
|
||||
-- * 如果需要的话点选手牌
|
||||
-- * 点击技能按钮,完成interaction与子卡选择;或者直接点可用手牌
|
||||
-- * 选择目标
|
||||
-- * 点确定
|
||||
-- 这些步骤归结起来,就是让AI想办法返回如下定义的UseReply
|
||||
-- 或者返回nil表示点取消
|
||||
--===================================================
|
||||
|
||||
---@class UseReply
|
||||
---@field card? integer|string @ string情况下是json.encode后
|
||||
---@field targets? integer[]
|
||||
---@field special_skill string @ 出牌阶段空闲点使用实体卡特有
|
||||
---@field interaction_data any @ 因技能而异,一般都是nil
|
||||
-- 考虑为triggerSkill设置收益修正函数
|
||||
|
||||
---@param card integer|table
|
||||
---@param targets? integer[]
|
||||
---@param special_skill? string
|
||||
---@param interaction_data? any
|
||||
function SmartAI:buildUseReply(card, targets, special_skill, interaction_data)
|
||||
if type(card) == "table" then card = json.encode(card) end
|
||||
return {
|
||||
card = card,
|
||||
targets = targets or {},
|
||||
special_skill = special_skill,
|
||||
interaction_data = interaction_data,
|
||||
--@field ask_use_card? fun(skill: ActiveSkill, ai: SmartAI): any
|
||||
--@field ask_response? fun(skill: ActiveSkill, ai: SmartAI): any
|
||||
|
||||
---@type table<string, SkillAI>
|
||||
fk.ai_skills = {}
|
||||
|
||||
---@param key string
|
||||
---@param spec SkillAISpec
|
||||
---@param inherit? string
|
||||
function SmartAI.static:setSkillAI(key, spec, inherit)
|
||||
if not fk.ai_skills[key] then
|
||||
fk.ai_skills[key] = SkillAI:new(key)
|
||||
end
|
||||
local ai = fk.ai_skills[key]
|
||||
local qsgs_wisdom_map = {
|
||||
estimated_benefit = "getEstimatedBenefit",
|
||||
think = "think",
|
||||
choose_interaction = "chooseInteraction",
|
||||
choose_cards = "chooseCards",
|
||||
choose_targets = "chooseTargets",
|
||||
|
||||
on_trigger_use = "onTriggerUse",
|
||||
on_use = "onUse",
|
||||
on_effect = "onEffect",
|
||||
}
|
||||
if inherit then
|
||||
local ai2 = fk.ai_skills[inherit]
|
||||
for _, k in pairs(qsgs_wisdom_map) do
|
||||
ai[k] = ai2[k]
|
||||
end
|
||||
end
|
||||
for k, v in pairs(spec) do
|
||||
local key2 = qsgs_wisdom_map[k]
|
||||
if key2 then ai[key2] = type(v) == "function" and v or function() return v end end
|
||||
end
|
||||
end
|
||||
|
||||
-- AskForUseActiveSkill: 询问发动主动技/视为技
|
||||
-- * 此处 UseReply.card 必定由 json.encode 而来
|
||||
-- * 且原型为 { skill = skillName, subcards = integer[] }
|
||||
----------------------------------------------------------
|
||||
|
||||
---@type table<string, fun(self: SmartAI, prompt: string, cancelable?: boolean, data: any): UseReply?>
|
||||
fk.ai_active_skill = {}
|
||||
|
||||
smart_cb["AskForUseActiveSkill"] = function(self, jsonData)
|
||||
local data = json.decode(jsonData)
|
||||
local skillName, prompt, cancelable, extra_data = table.unpack(data)
|
||||
|
||||
local skill = Fk.skills[skillName]
|
||||
skill._extra_data = extra_data
|
||||
|
||||
local ret = self:callFromTable(fk.ai_active_skill, nil, skillName,
|
||||
self, prompt, cancelable, extra_data)
|
||||
|
||||
if ret then return json.encode(ret) end
|
||||
if cancelable then return "" end
|
||||
return RandomAI.cb_table["AskForUseActiveSkill"](self, jsonData)
|
||||
--- 将spec中的键值保存到这个技能的ai中
|
||||
---@param key string
|
||||
---@param spec SkillAISpec 表
|
||||
---@param inherit? string 可以直接复用某个技能已有的函数 自然spec中更加优先
|
||||
---@diagnostic disable-next-line
|
||||
function SmartAI:setSkillAI(key, spec, inherit)
|
||||
error("This is a static method. Please use SmartAI:setSkillAI(...)")
|
||||
end
|
||||
|
||||
-- AskForUseCard: 询问使用卡牌
|
||||
-- 判断函数一样返回UseReply,此时卡牌可能是integer或者string
|
||||
-- 为string的话肯定是由ViewAsSkill转化而来
|
||||
-- 真的要考虑ViewAsSkill吗,害怕
|
||||
---------------------------------------------------------
|
||||
|
||||
-- 使用牌相关——同时见于主动使用和响应式使用。
|
||||
--- 键是prompt的第一项或者牌名,优先prompt,其次name,实在不行trueName。
|
||||
---@type table<string, fun(self: SmartAI, pattern: string, prompt: string, cancelable?: boolean, extra_data?: UseExtraData): UseReply?>
|
||||
fk.ai_use_card = setmetatable({}, {
|
||||
__index = function(_, k)
|
||||
-- FIXME: 感觉不妥
|
||||
local c = Fk.all_card_types[k]
|
||||
if not c then return nil end
|
||||
if c.type == Card.TypeEquip then
|
||||
return function(self, pattern, prompt, cancelable, extra_data)
|
||||
local slashes = self:getCards(k, "use", extra_data)
|
||||
if #slashes == 0 then return nil end
|
||||
|
||||
return self:buildUseReply(slashes[1].id)
|
||||
SmartAI:setSkillAI("__card_skill", {
|
||||
choose_targets = function(self, ai)
|
||||
local targets = ai:getEnabledTargets()
|
||||
local logic = AIGameLogic:new(ai)
|
||||
local val_func = function(p)
|
||||
logic.benefit = 0
|
||||
logic:useCard({
|
||||
from = ai.player.id,
|
||||
tos = { { p.id } },
|
||||
card = ai:getSelectedCard(),
|
||||
})
|
||||
verbose("目前状况下,对%s的预测收益为%d", tostring(p), logic.benefit)
|
||||
return logic.benefit
|
||||
end
|
||||
for _, p, val in fk.sorted_pairs(targets, val_func) do
|
||||
if val > 0 then
|
||||
ai:selectTarget(p, true)
|
||||
return ai:doOKButton(), val
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
think = function(self, ai)
|
||||
local skill_name = self.skill.name
|
||||
local pattern = skill_name:sub(1, #skill_name - 6)
|
||||
local cards = ai:getEnabledCards(pattern)
|
||||
cards = table.random(cards, math.min(#cards, 5)) --[[@as integer[] ]]
|
||||
-- local cid = table.random(cards)
|
||||
|
||||
local best_ret, best_val = nil, -100000
|
||||
for _, cid in ipairs(cards) do
|
||||
ai:selectCard(cid, true)
|
||||
local ret, val = self:chooseTargets(ai)
|
||||
val = val or -100000
|
||||
if not best_ret or (best_val < val) then
|
||||
best_ret, best_val = ret, val
|
||||
end
|
||||
ai:unSelectAll()
|
||||
end
|
||||
|
||||
return best_ret, best_val
|
||||
end,
|
||||
})
|
||||
|
||||
local defauld_use_card = function(self, pattern, _, cancelable, exdata)
|
||||
if cancelable then return nil end
|
||||
local cards = self:getCards(pattern, "use", exdata)
|
||||
if #cards == 0 then return nil end
|
||||
|
||||
-- TODO: 目标
|
||||
return self:buildUseReply(cards[1].id)
|
||||
function SmartAI.static:setCardSkillAI(key, spec)
|
||||
SmartAI:setSkillAI(key, spec, "__card_skill")
|
||||
end
|
||||
|
||||
--- 请求使用,先试图使用prompt,再试图使用card_name,最后交给随机AI
|
||||
smart_cb["AskForUseCard"] = function(self, jsonData)
|
||||
local data = json.decode(jsonData)
|
||||
local card_name, pattern, prompt, cancelable, extra_data = table.unpack(data)
|
||||
-- 等价于SmartAI:setCardSkillAI(key, spec, "__card_skill")
|
||||
---@param key string
|
||||
---@param spec SkillAISpec 表
|
||||
function SmartAI:setCardSkillAI(key, spec)
|
||||
error("This is a static method. Please use SmartAI:setCardSkillAI(...)")
|
||||
end
|
||||
|
||||
local prompt_prefix = prompt:split(":")[1]
|
||||
local key
|
||||
if fk.ai_use_card[prompt_prefix] then
|
||||
key = prompt_prefix
|
||||
elseif fk.ai_use_card[card_name] then
|
||||
key = card_name
|
||||
else
|
||||
local tmp = card_name:split("__")
|
||||
key = tmp[#tmp]
|
||||
---@type table<string, TriggerSkillAI>
|
||||
fk.ai_trigger_skills = {}
|
||||
|
||||
---@param spec TriggerSkillAISpec
|
||||
function SmartAI.static:setTriggerSkillAI(key, spec)
|
||||
if not fk.ai_trigger_skills[key] then
|
||||
fk.ai_trigger_skills[key] = TriggerSkillAI:new(key)
|
||||
end
|
||||
local ret = self:callFromTable(fk.ai_use_card, defauld_use_card, key,
|
||||
self, pattern, prompt, cancelable, extra_data)
|
||||
|
||||
if ret then return json.encode(ret) end
|
||||
if cancelable then return "" end
|
||||
return RandomAI.cb_table["AskForUseCard"](self, jsonData)
|
||||
end
|
||||
|
||||
-- AskForResponseCard: 询问打出卡牌
|
||||
-- 注意事项同前
|
||||
-------------------------------------
|
||||
|
||||
-- 一样的牌名或者prompt做键优先prompt
|
||||
---@type table<string, fun(self: SmartAI, pattern: string, prompt: string, cancelable?: boolean, extra_data: any): UseReply?>
|
||||
fk.ai_response_card = {}
|
||||
|
||||
local defauld_response_card = function(self, pattern, _, cancelable)
|
||||
if cancelable then return nil end
|
||||
local cards = self:getCards(pattern, "response")
|
||||
if #cards == 0 then return nil end
|
||||
return self:buildUseReply(cards[1].id)
|
||||
end
|
||||
|
||||
-- 同前
|
||||
smart_cb["AskForResponseCard"] = function(self, jsonData)
|
||||
local data = json.decode(jsonData)
|
||||
local card_name, pattern, prompt, cancelable, extra_data = table.unpack(data)
|
||||
|
||||
local prompt_prefix = prompt:split(":")[1]
|
||||
local key
|
||||
if fk.ai_response_card[prompt_prefix] then
|
||||
key = prompt_prefix
|
||||
elseif fk.ai_response_card[card_name] then
|
||||
key = card_name
|
||||
else
|
||||
local tmp = card_name:split("__")
|
||||
key = tmp[#tmp]
|
||||
local ai = fk.ai_trigger_skills[key]
|
||||
if spec.correct_func then
|
||||
ai.getCorrect = spec.correct_func
|
||||
end
|
||||
local ret = self:callFromTable(fk.ai_response_card, defauld_response_card, key,
|
||||
self, pattern, prompt, cancelable, extra_data)
|
||||
|
||||
if ret then return json.encode(ret) end
|
||||
if cancelable then return "" end
|
||||
return RandomAI.cb_table["AskForResponseCard"](self, jsonData)
|
||||
end
|
||||
|
||||
-- PlayCard: 出牌阶段空闲时间点使用牌/技能
|
||||
-- 老规矩得丢一个UseReply回来,但是自由度就高得多了
|
||||
-- 需要完成的任务:从众多亮着的卡、技能中选一个
|
||||
-- 考虑要不要用?用的话就用,否则选下个
|
||||
-- 至于如何使用,可以复用askFor中的函数
|
||||
-----------------------------------------------
|
||||
smart_cb["PlayCard"] = function(self)
|
||||
local extra_use_data = { playing = true }
|
||||
local cards = self:getCards(".", "use", extra_use_data)
|
||||
--- 将spec中的键值保存到这个技能的ai中
|
||||
---@param key string
|
||||
---@param spec TriggerSkillAISpec
|
||||
---@diagnostic disable-next-line
|
||||
function SmartAI:setTriggerSkillAI(key, spec)
|
||||
error("This is a static method. Please use SmartAI:setTriggerSkillAI(...)")
|
||||
end
|
||||
|
||||
local card_names = {}
|
||||
for _, cd in ipairs(cards) do
|
||||
-- TODO: 视为技
|
||||
-- 视为技对应的function一般会返回一张印出来的卡,又要纳入新的考虑范围了
|
||||
-- 不过这种根据牌名判断的逻辑而言 可能需要调用多次视为技函数了
|
||||
-- 要用好空间换时间
|
||||
table.insertIfNeed(card_names, cd.name)
|
||||
---@param cid_or_skill integer|string
|
||||
function SmartAI:getBasicBenefit(cid_or_skill)
|
||||
end
|
||||
|
||||
local function hasKey(t1, t2, key)
|
||||
if (t1 and t1[key]) or (t2 and t2[key]) then return true end
|
||||
end
|
||||
|
||||
local function callFromTables(tab, backup, key, ...)
|
||||
local fn
|
||||
if tab and tab[key] then
|
||||
fn = tab[key]
|
||||
elseif backup and backup[key] then
|
||||
fn = backup[key]
|
||||
end
|
||||
-- TODO: 主动技
|
||||
if not fn then return end
|
||||
return fn(...)
|
||||
end
|
||||
|
||||
-- 第二步:考虑使用其中之一
|
||||
local value_func = function(str) return #str end
|
||||
for _, name in fk.sorted_pairs(card_names, value_func) do
|
||||
if true then
|
||||
local ret = self:callFromTable(fk.ai_use_card, nil,
|
||||
fk.ai_use_card[name] and name or name:split("__")[2],
|
||||
self, name, "", true, extra_use_data)
|
||||
function SmartAI:handleAskForUseActiveSkill()
|
||||
local name = self.handler.skill_name
|
||||
local current_skill = self:currentSkill()
|
||||
|
||||
if ret then return json.encode(ret) end
|
||||
local ai
|
||||
if current_skill then ai = fk.ai_skills[current_skill.name] end
|
||||
if not ai then ai = fk.ai_skills[name] end
|
||||
if not ai then return "" end
|
||||
return ai:think(self)
|
||||
end
|
||||
|
||||
function SmartAI:handlePlayCard()
|
||||
local card_ids = self:getEnabledCards()
|
||||
local skill_ai_list = {}
|
||||
for _, id in ipairs(card_ids) do
|
||||
local cd = Fk:getCardById(id)
|
||||
local ai = fk.ai_skills[cd.skill.name]
|
||||
if ai then
|
||||
table.insertIfNeed(skill_ai_list, ai)
|
||||
end
|
||||
end
|
||||
for _, sname in ipairs(self:getEnabledSkills()) do
|
||||
local ai = fk.ai_skills[sname]
|
||||
if ai then
|
||||
table.insertIfNeed(skill_ai_list, ai)
|
||||
end
|
||||
end
|
||||
verbose("======== %s: 开始计算出牌阶段 ========", tostring(self))
|
||||
verbose("待选技能:[%s]", table.concat(table.map(skill_ai_list, function(ai) return ai.skill.name end), ", "))
|
||||
|
||||
local value_func = function(ai)
|
||||
if not ai then return -500 end
|
||||
local val = ai:getEstimatedBenefit(self)
|
||||
return val or 0
|
||||
end
|
||||
|
||||
local cancel_val = math.min(-90 * (self.player:getMaxCards() - self.player:getHandcardNum()), 0)
|
||||
|
||||
local best_ret, best_val
|
||||
for _, ai, val in fk.sorted_pairs(skill_ai_list, value_func) do
|
||||
verbose("[*] 考虑 %s (预估收益%d)", ai.skill.name, val)
|
||||
if val < cancel_val then
|
||||
verbose("由于预估收益小于取消的收益,不再思考")
|
||||
break
|
||||
end
|
||||
local ret, real_val = ai:think(self)
|
||||
-- if ret and ret ~= "" then return ret end
|
||||
if not best_ret or (best_val < real_val) then
|
||||
best_ret, best_val = ret, real_val
|
||||
end
|
||||
self:unSelectAll()
|
||||
end
|
||||
|
||||
if best_ret and best_ret ~= "" then return best_ret end
|
||||
return ""
|
||||
end
|
||||
|
||||
|
@ -265,24 +269,33 @@ end
|
|||
-- 函数返回true或者false即可。
|
||||
-----------------------------
|
||||
|
||||
---@type table<string, boolean | fun(self: SmartAI, extra_data: any, prompt: string): bool>
|
||||
fk.ai_skill_invoke = {}
|
||||
--[[
|
||||
---@type table<string, boolean | fun(self: SmartAI, prompt: string): bool>
|
||||
fk.ai_skill_invoke = { AskForLuckCard = false }
|
||||
|
||||
smart_cb["AskForSkillInvoke"] = function(self, jsonData)
|
||||
local data = json.decode(jsonData)
|
||||
function SmartAI:handleAskForSkillInvoke(data)
|
||||
local skillName, prompt = data[1], data[2]
|
||||
local ask = fk.ai_skill_invoke[skillName]
|
||||
local skill = Fk.skills[skillName]
|
||||
local spec = fk.ai_skills[skillName]
|
||||
local ask
|
||||
if spec then
|
||||
ask = spec.skill_invoke
|
||||
else
|
||||
ask = fk.ai_skill_invoke[skillName]
|
||||
end
|
||||
|
||||
|
||||
if type(ask) == "function" then
|
||||
return ask(self, prompt) and "1" or ""
|
||||
return ask(skill, self) and "1" or ""
|
||||
elseif type(ask) == "boolean" then
|
||||
return ask and "1" or ""
|
||||
elseif Fk.skills[skillName].frequency == Skill.Frequent then
|
||||
return "1"
|
||||
else
|
||||
return RandomAI.cb_table["AskForSkillInvoke"](self, jsonData)
|
||||
return math.random() < 0.5 and "1" or ""
|
||||
end
|
||||
end
|
||||
--]]
|
||||
|
||||
-- 敌友判断相关。
|
||||
-- 目前才开始,做个明身份打牌的就行了。
|
||||
|
@ -309,38 +322,7 @@ end
|
|||
|
||||
-- sorted_pairs 见 core/util.lua
|
||||
|
||||
-- 合法性检测相关
|
||||
-- 以后估计会单开个合法性类然后改成套壳吧
|
||||
-- 基于事件的收益推理;内置事件
|
||||
--=================================================
|
||||
|
||||
-- TODO: 这东西估计会变成一个单独模块
|
||||
local invalid_func_table = {
|
||||
use = function(player, card, extra_data)
|
||||
local playing = extra_data and extra_data.playing
|
||||
return Player.prohibitUse(player, card) or (playing and not player:canUse(card))
|
||||
end,
|
||||
response = Player.prohibitResponse,
|
||||
discard = Player.prohibitDiscard,
|
||||
}
|
||||
|
||||
--- 根据pattern获得所有手中的牌。
|
||||
---@param pattern string
|
||||
---@param validator? string @ 合法检测,须为use, response, discard之一或空
|
||||
---@param extra_data? UseExtraData @ 出牌阶段用
|
||||
---@return Card[]
|
||||
function SmartAI:getCards(pattern, validator, extra_data)
|
||||
validator = validator or ""
|
||||
extra_data = extra_data or Util.DummyTable
|
||||
local invalid_func = invalid_func_table[validator] or Util.FalseFunc
|
||||
local exp = Exppattern:Parse(pattern)
|
||||
|
||||
local cards = table.map(self.player:getHandlyIds(), Util.Id2CardMapper)
|
||||
local ret = table.filter(cards, function(c)
|
||||
return exp:match(c) and not invalid_func(self.player, c, extra_data)
|
||||
end)
|
||||
|
||||
-- TODO: 考虑视为技,这里可以再返回一些虚拟牌
|
||||
return ret
|
||||
end
|
||||
|
||||
return SmartAI
|
||||
|
|
29
lua/server/ai/trigger_skill.lua
Normal file
29
lua/server/ai/trigger_skill.lua
Normal file
|
@ -0,0 +1,29 @@
|
|||
|
||||
--- 关于某个触发技在AI中如何影响基于事件的收益推理。
|
||||
---
|
||||
--- 类似于真正的触发技,这种技能AI也需要指定触发时机,以及在某个时机之下
|
||||
--- 如何进行收益计算。收益计算中亦可返回true,表明事件被这个技能终止,
|
||||
--- 也就是不再进行后续其他技能的计算。
|
||||
---
|
||||
--- 触发技本身又会不断触发新的事件,比如刚烈反伤、反馈拿牌等。对于衍生事件
|
||||
--- 亦可进一步进行推理,但是AI会限制自己的搜索深度,所以推理结果不一定准确。
|
||||
---@class TriggerSkillAI
|
||||
---@field public skill TriggerSkill
|
||||
local TriggerSkillAI = class("TriggerSkillAI")
|
||||
|
||||
---@param skill string
|
||||
function TriggerSkillAI:initialize(skill)
|
||||
self.skill = Fk.skills[skill]
|
||||
end
|
||||
|
||||
--- 获取触发技对收益评测的影响,通过基于logic触发更多模拟事件来模拟收益的变化
|
||||
---
|
||||
--- 返回true表示打断后续收益判断逻辑
|
||||
---@return boolean?
|
||||
function TriggerSkillAI:getCorrect(logic, event, target, player, data)
|
||||
end
|
||||
|
||||
---@class TriggerSkillAISpec
|
||||
---@field correct_func fun(self: TriggerSkillAI, logic: AIGameLogic, event: Event, target: ServerPlayer?, player: ServerPlayer, data: any): boolean?
|
||||
|
||||
return TriggerSkillAI
|
|
@ -1,15 +1,47 @@
|
|||
-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
-- Trust AI
|
||||
-- 需要打出牌时,有的话就打出
|
||||
-- 需要使用闪、对自己使用无懈、酒、桃时,只要有就使用
|
||||
-- 除此之外什么都不做
|
||||
|
||||
---@class TrustAI: AI
|
||||
local TrustAI = AI:subclass("TrustAI")
|
||||
|
||||
local trust_cb = {}
|
||||
|
||||
function TrustAI:initialize(player)
|
||||
AI.initialize(self, player)
|
||||
self.cb_table = trust_cb
|
||||
end
|
||||
|
||||
function TrustAI:handleAskForUseCard(data)
|
||||
local pattern = data[2]
|
||||
local prompt = data[3]
|
||||
|
||||
local wontuse = true
|
||||
if pattern == "jink" then
|
||||
wontuse = false
|
||||
elseif pattern == "nullification" then
|
||||
wontuse = prompt:split(":")[3] ~= tostring(self.player.id)
|
||||
elseif pattern == "peach" or pattern == "peach,analeptic" then
|
||||
wontuse = not prompt:startsWith("#AskForPeachesSelf")
|
||||
end
|
||||
if wontuse then return "" end
|
||||
|
||||
local cards = self:getEnabledCards()
|
||||
for _, cd in ipairs(cards) do
|
||||
self:selectCard(cd, true) -- 默认按下卡牌后直接可确定 懒得管了
|
||||
return self:doOKButton()
|
||||
end
|
||||
return ""
|
||||
end
|
||||
|
||||
function TrustAI:handleAskForResponseCard(data)
|
||||
-- local cancelable = data[4] -- 算了,不按取消
|
||||
local cards = self:getEnabledCards()
|
||||
for _, cd in ipairs(cards) do
|
||||
self:selectCard(cd, true) -- 默认按下卡牌后直接可确定 懒得管了
|
||||
return self:doOKButton()
|
||||
end
|
||||
return ""
|
||||
end
|
||||
|
||||
return TrustAI
|
||||
|
|
|
@ -48,6 +48,7 @@ fk.FinishJudge = 27
|
|||
|
||||
fk.RoundStart = 28
|
||||
fk.RoundEnd = 29
|
||||
fk.AfterRoundEnd = 85
|
||||
|
||||
fk.BeforeTurnOver = 79
|
||||
fk.TurnedOver = 30
|
||||
|
@ -126,7 +127,7 @@ fk.AfterSkillEffect = 82
|
|||
|
||||
-- 83 = PreTurnStart
|
||||
-- 84 = AfterTurnEnd
|
||||
-- 85 = xxx
|
||||
-- 85 = AfterRoundEnd
|
||||
-- 86 = AfterPhaseEnd
|
||||
|
||||
fk.AreaAborted = 87
|
||||
|
|
|
@ -80,11 +80,11 @@ function DrawInitial:main()
|
|||
return
|
||||
end
|
||||
|
||||
room:notifyMoveFocus(room.alive_players, "AskForLuckCard")
|
||||
local request = Request:new("AskForSkillInvoke", room.alive_players)
|
||||
local request = Request:new(room.alive_players, "AskForSkillInvoke")
|
||||
for _, p in ipairs(room.alive_players) do
|
||||
request:setData(p, { "AskForLuckCard", "#AskForLuckCard:::" .. room.settings.luckTime })
|
||||
end
|
||||
request.focus_text = "AskForLuckCard"
|
||||
request.luck_data = luck_data
|
||||
request.accept_cancel = true
|
||||
request:ask()
|
||||
|
@ -101,28 +101,22 @@ 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()
|
||||
local currentPlayer
|
||||
|
||||
while true do
|
||||
GameEvent.Turn:create(room.current):exec()
|
||||
if room.game_finished then break end
|
||||
|
||||
local changingData = { from = room.current, to = room.current:getNextAlive(true, nil, true), skipRoundPlus = false }
|
||||
local changingData = { from = room.current, to = room.current.next, 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
|
||||
local nextTurnOwner = changingData.to
|
||||
if room.current.seat > nextTurnOwner.seat and not changingData.skipRoundPlus then
|
||||
break
|
||||
else
|
||||
room.current = nextAlive
|
||||
room.current = nextTurnOwner
|
||||
end
|
||||
until p.seat >= (nextTurnOwner or p:getNextAlive(true, nil, true)).seat and not skipRoundPlus
|
||||
end
|
||||
end
|
||||
|
||||
function Round:main()
|
||||
|
@ -153,7 +147,8 @@ function Round:main()
|
|||
|
||||
logic:trigger(fk.RoundStart, room.current)
|
||||
self:action()
|
||||
logic:trigger(fk.RoundEnd, p)
|
||||
logic:trigger(fk.RoundEnd, room.current)
|
||||
logic:trigger(fk.AfterRoundEnd, room.current)
|
||||
end
|
||||
|
||||
function Round:clear()
|
||||
|
@ -188,7 +183,10 @@ local Turn = GameEvent:subclass("GameEvent.Turn")
|
|||
function Turn:prepare()
|
||||
local room = self.room
|
||||
local logic = room.logic
|
||||
local player = room.current
|
||||
local player = self.data[1]---@type ServerPlayer
|
||||
if self.data[2] == nil then self.data[2] = {} end
|
||||
local data = self.data[2]---@type TurnStruct
|
||||
data.reason = data.reason or "game_rule"
|
||||
|
||||
if player.rest > 0 and player.rest < 999 then
|
||||
room:setPlayerRest(player, player.rest - 1)
|
||||
|
@ -208,21 +206,24 @@ function Turn:prepare()
|
|||
return true
|
||||
end
|
||||
|
||||
return logic:trigger(fk.BeforeTurnStart, player)
|
||||
return logic:trigger(fk.BeforeTurnStart, player, data)
|
||||
end
|
||||
|
||||
function Turn:main()
|
||||
local room = self.room
|
||||
room.current.phase = Player.PhaseNone
|
||||
room.logic:trigger(fk.TurnStart, room.current)
|
||||
room.current.phase = Player.NotActive
|
||||
room.current:play()
|
||||
local player = self.data[1]---@type ServerPlayer
|
||||
local data = self.data[2]---@type TurnStruct
|
||||
player.phase = Player.PhaseNone
|
||||
room.logic:trigger(fk.TurnStart, player, data)
|
||||
player.phase = Player.NotActive
|
||||
player:play(data.phase_table)
|
||||
end
|
||||
|
||||
function Turn:clear()
|
||||
local room = self.room
|
||||
local current = self.data[1]---@type ServerPlayer
|
||||
local data = self.data[2]---@type TurnStruct
|
||||
|
||||
local current = room.current
|
||||
local logic = room.logic
|
||||
if self.interrupted then
|
||||
if current.phase ~= Player.NotActive then
|
||||
|
@ -239,8 +240,8 @@ function Turn:clear()
|
|||
end
|
||||
|
||||
current.phase = Player.PhaseNone
|
||||
logic:trigger(fk.TurnEnd, current, nil, self.interrupted)
|
||||
logic:trigger(fk.AfterTurnEnd, current, nil, self.interrupted)
|
||||
logic:trigger(fk.TurnEnd, current, data, self.interrupted)
|
||||
logic:trigger(fk.AfterTurnEnd, current, data, self.interrupted)
|
||||
current.phase = Player.NotActive
|
||||
|
||||
room:setTag("endTurn", false)
|
||||
|
@ -275,7 +276,7 @@ function Phase:main()
|
|||
local room = self.room
|
||||
local logic = room.logic
|
||||
|
||||
local player = self.data[1] ---@type Player
|
||||
local player = self.data[1] ---@type ServerPlayer
|
||||
if not logic:trigger(fk.EventPhaseStart, player) then
|
||||
if player.phase ~= Player.NotActive then
|
||||
logic:trigger(fk.EventPhaseProceeding, player)
|
||||
|
@ -322,7 +323,7 @@ function Phase:main()
|
|||
}
|
||||
room.logic:trigger(fk.DrawNCards, player, data)
|
||||
if not player._phase_end then
|
||||
room:drawCards(player, data.n, "game_rule")
|
||||
room:drawCards(player, data.n, "phase_draw")
|
||||
end
|
||||
room.logic:trigger(fk.AfterDrawNCards, player, data)
|
||||
end,
|
||||
|
@ -332,8 +333,8 @@ function Phase:main()
|
|||
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)
|
||||
|
||||
local result = Request:new(player, "PlayCard"):getResult(player)
|
||||
if result == "" then break end
|
||||
|
||||
local useResult = room:handleUseCardReply(player, result)
|
||||
|
@ -354,7 +355,7 @@ function Phase:main()
|
|||
) - player:getMaxCards()
|
||||
room:broadcastProperty(player, "MaxCards")
|
||||
if discardNum > 0 then
|
||||
room:askForDiscard(player, discardNum, discardNum, false, "game_rule", false)
|
||||
room:askForDiscard(player, discardNum, discardNum, false, "phase_discard", false)
|
||||
end
|
||||
end,
|
||||
[Player.Finish] = function()
|
||||
|
|
|
@ -100,29 +100,17 @@ end
|
|||
---@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 move1 = {} ---@type CardsMoveInfo
|
||||
move1.ids = { card:getEffectiveId() }
|
||||
move1.from = self.owner_map[card:getEffectiveId()]
|
||||
move1.toArea = Card.Processing
|
||||
move1.moveReason = fk.ReasonJustMove
|
||||
move1.skillName = skillName
|
||||
self:moveCards(move1)
|
||||
|
||||
local oldJudge = judge.card
|
||||
judge.card = card
|
||||
local rebyre = judge.retrial_by_response
|
||||
judge.retrial_by_response = player
|
||||
|
||||
self:sendLog{
|
||||
type = "#ChangedJudge",
|
||||
|
@ -134,16 +122,18 @@ function JudgeEventWrappers:retrial(card, player, judge, skillName, exchange)
|
|||
|
||||
Fk:filterCard(judge.card.id, judge.who, judge)
|
||||
|
||||
exchange = exchange and not player.dead
|
||||
if self:getCardArea(oldJudge) == Card.Processing then
|
||||
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
|
||||
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
|
||||
|
||||
self:moveCards(move2)
|
||||
end
|
||||
|
||||
return { Judge, JudgeEventWrappers }
|
||||
|
|
|
@ -397,16 +397,17 @@ end
|
|||
---@param cards integer|integer[] @ 移动的牌
|
||||
---@param skillName? string @ 技能名
|
||||
---@param convert? boolean @ 是否可以替换装备(默认可以)
|
||||
---@param proposer? ServerPlayer @ 操作者
|
||||
---@param proposer? ServerPlayer | integer @ 操作者
|
||||
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}
|
||||
proposer = type(proposer) == "number" and self:getPlayerById(proposer) or proposer
|
||||
local proposerId = proposer and proposer.id or nil
|
||||
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})
|
||||
|
|
|
@ -42,17 +42,19 @@ function Pindian:main()
|
|||
table.insert(targets, pindianData.from)
|
||||
pindianData.from.request_data = json.encode(data)
|
||||
else
|
||||
local _pindianCard = pindianData.fromCard
|
||||
local pindianCard = _pindianCard:clone(_pindianCard.suit, _pindianCard.number)
|
||||
pindianCard:addSubcard(_pindianCard.id)
|
||||
if not pindianData._fromCard then
|
||||
local _pindianCard = pindianData.fromCard
|
||||
local pindianCard = _pindianCard:clone(_pindianCard.suit, _pindianCard.number)
|
||||
pindianCard:addSubcard(_pindianCard.id)
|
||||
|
||||
pindianData.fromCard = pindianCard
|
||||
pindianData._fromCard = _pindianCard
|
||||
pindianData.fromCard = pindianCard
|
||||
pindianData._fromCard = _pindianCard
|
||||
end
|
||||
|
||||
table.insert(moveInfos, {
|
||||
ids = { _pindianCard.id },
|
||||
from = room.owner_map[_pindianCard.id],
|
||||
fromArea = room:getCardArea(_pindianCard.id),
|
||||
ids = { pindianData._fromCard.id },
|
||||
from = room.owner_map[pindianData._fromCard.id],
|
||||
fromArea = room:getCardArea(pindianData._fromCard.id),
|
||||
toArea = Card.Processing,
|
||||
moveReason = fk.ReasonPut,
|
||||
skillName = pindianData.reason,
|
||||
|
@ -61,17 +63,19 @@ function Pindian:main()
|
|||
end
|
||||
for _, to in ipairs(pindianData.tos) do
|
||||
if pindianData.results[to.id] and pindianData.results[to.id].toCard then
|
||||
local _pindianCard = pindianData.results[to.id].toCard
|
||||
local pindianCard = _pindianCard:clone(_pindianCard.suit, _pindianCard.number)
|
||||
pindianCard:addSubcard(_pindianCard.id)
|
||||
if not pindianData.results[to.id]._toCard then
|
||||
local _pindianCard = pindianData.results[to.id].toCard
|
||||
local pindianCard = _pindianCard:clone(_pindianCard.suit, _pindianCard.number)
|
||||
pindianCard:addSubcard(_pindianCard.id)
|
||||
|
||||
pindianData.results[to.id].toCard = pindianCard
|
||||
pindianData.results[to.id]._toCard = _pindianCard
|
||||
pindianData.results[to.id].toCard = pindianCard
|
||||
pindianData.results[to.id]._toCard = _pindianCard
|
||||
end
|
||||
|
||||
table.insert(moveInfos, {
|
||||
ids = { _pindianCard.id },
|
||||
from = room.owner_map[_pindianCard.id],
|
||||
fromArea = room:getCardArea(_pindianCard.id),
|
||||
ids = { pindianData.results[to.id]._toCard.id },
|
||||
from = room.owner_map[pindianData.results[to.id]._toCard.id],
|
||||
fromArea = room:getCardArea(pindianData.results[to.id]._toCard.id),
|
||||
toArea = Card.Processing,
|
||||
moveReason = fk.ReasonPut,
|
||||
skillName = pindianData.reason,
|
||||
|
@ -79,48 +83,51 @@ function Pindian:main()
|
|||
})
|
||||
else
|
||||
table.insert(targets, to)
|
||||
to.request_data = json.encode(data)
|
||||
end
|
||||
end
|
||||
|
||||
room:notifyMoveFocus(targets, "AskForPindian")
|
||||
room:doBroadcastRequest("AskForUseActiveSkill", targets)
|
||||
if #targets ~= 0 then
|
||||
local req = Request:new(targets, "AskForUseActiveSkill")
|
||||
for _, p in ipairs(targets) do req:setData(p, data) end
|
||||
req.focus_text = "AskForPindian"
|
||||
|
||||
for _, p in ipairs(targets) do
|
||||
local _pindianCard
|
||||
if p.reply_ready then
|
||||
local replyCard = json.decode(p.client_reply).card
|
||||
_pindianCard = Fk:getCardById(json.decode(replyCard).subcards[1])
|
||||
else
|
||||
_pindianCard = Fk:getCardById(p:getCardIds(Player.Hand)[1])
|
||||
for _, p in ipairs(targets) do
|
||||
local _pindianCard
|
||||
local result = req:getResult(p)
|
||||
if result ~= "" then
|
||||
local replyCard = result.card
|
||||
_pindianCard = Fk:getCardById(json.decode(replyCard).subcards[1])
|
||||
else
|
||||
_pindianCard = Fk:getCardById(p:getCardIds(Player.Hand)[1])
|
||||
end
|
||||
|
||||
local pindianCard = _pindianCard:clone(_pindianCard.suit, _pindianCard.number)
|
||||
pindianCard:addSubcard(_pindianCard.id)
|
||||
|
||||
if p == pindianData.from then
|
||||
pindianData.fromCard = pindianCard
|
||||
pindianData._fromCard = _pindianCard
|
||||
else
|
||||
pindianData.results[p.id] = pindianData.results[p.id] or {}
|
||||
pindianData.results[p.id].toCard = pindianCard
|
||||
pindianData.results[p.id]._toCard = _pindianCard
|
||||
end
|
||||
|
||||
table.insert(moveInfos, {
|
||||
ids = { _pindianCard.id },
|
||||
from = p.id,
|
||||
toArea = Card.Processing,
|
||||
moveReason = fk.ReasonPut,
|
||||
skillName = pindianData.reason,
|
||||
moveVisible = true,
|
||||
})
|
||||
|
||||
room:sendLog{
|
||||
type = "#ShowPindianCard",
|
||||
from = p.id,
|
||||
arg = _pindianCard:toLogString(),
|
||||
}
|
||||
end
|
||||
|
||||
local pindianCard = _pindianCard:clone(_pindianCard.suit, _pindianCard.number)
|
||||
pindianCard:addSubcard(_pindianCard.id)
|
||||
|
||||
if p == pindianData.from then
|
||||
pindianData.fromCard = pindianCard
|
||||
pindianData._fromCard = _pindianCard
|
||||
else
|
||||
pindianData.results[p.id] = pindianData.results[p.id] or {}
|
||||
pindianData.results[p.id].toCard = pindianCard
|
||||
pindianData.results[p.id]._toCard = _pindianCard
|
||||
end
|
||||
|
||||
table.insert(moveInfos, {
|
||||
ids = { _pindianCard.id },
|
||||
from = p.id,
|
||||
toArea = Card.Processing,
|
||||
moveReason = fk.ReasonPut,
|
||||
skillName = pindianData.reason,
|
||||
moveVisible = true,
|
||||
})
|
||||
|
||||
room:sendLog{
|
||||
type = "#ShowPindianCard",
|
||||
from = p.id,
|
||||
card = { _pindianCard.id },
|
||||
}
|
||||
end
|
||||
|
||||
room:moveCards(table.unpack(moveInfos))
|
||||
|
|
|
@ -110,7 +110,7 @@ function SkillEventWrappers:handleAddLoseSkills(player, skill_names, source_skil
|
|||
if #skill_names == 0 then return end
|
||||
local losts = {} ---@type boolean[]
|
||||
local triggers = {} ---@type Skill[]
|
||||
local lost_piles = {} ---@type integer[]
|
||||
-- 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)
|
||||
|
@ -132,11 +132,11 @@ function SkillEventWrappers:handleAddLoseSkills(player, skill_names, source_skil
|
|||
|
||||
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
|
||||
-- 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
|
||||
|
@ -169,19 +169,26 @@ function SkillEventWrappers:handleAddLoseSkills(player, skill_names, source_skil
|
|||
|
||||
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])
|
||||
if losts[i] then
|
||||
local skill = triggers[i]
|
||||
skill:onLose(player)
|
||||
self.logic:trigger(fk.EventLoseSkill, player, skill)
|
||||
else
|
||||
local skill = triggers[i]
|
||||
self.logic:trigger(fk.EventAcquireSkill, player, skill)
|
||||
skill:onAcquire(player)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if #lost_piles > 0 then
|
||||
self:moveCards({
|
||||
ids = lost_piles,
|
||||
from = player.id,
|
||||
toArea = Card.DiscardPile,
|
||||
moveReason = fk.ReasonPutIntoDiscardPile,
|
||||
})
|
||||
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 }
|
||||
|
|
|
@ -858,6 +858,9 @@ function UseCardEventWrappers:handleCardEffect(event, cardEffectEvent)
|
|||
extra_data = { useEventId = parentUseEvent.id, effectTo = cardEffectEvent.to }
|
||||
end
|
||||
end
|
||||
if #players > 0 and cardEffectEvent.card.trueName == "nullification" then
|
||||
self:animDelay(2)
|
||||
end
|
||||
local use = self:askForNullification(players, nil, nil, prompt, true, extra_data, cardEffectEvent)
|
||||
if use then
|
||||
use.toCard = cardEffectEvent.card
|
||||
|
|
|
@ -181,7 +181,7 @@ local function bin_search(events, from, to, n, func)
|
|||
end
|
||||
|
||||
-- 从某个区间中,找出类型符合且符合func函数检测的至多n个事件。
|
||||
---@param eventType integer @ 要查找的事件类型
|
||||
---@param eventType GameEvent @ 要查找的事件类型
|
||||
---@param n integer @ 最多找多少个
|
||||
---@param func fun(e: GameEvent): boolean? @ 过滤用的函数
|
||||
---@param endEvent? GameEvent @ 区间终止点,默认为本事件结束
|
||||
|
|
|
@ -132,29 +132,18 @@ function GameLogic:chooseGenerals()
|
|||
|
||||
local nonlord = room:getOtherPlayers(lord, true)
|
||||
local generals = table.random(room.general_pile, #nonlord * generalNum)
|
||||
|
||||
local req = Request:new(nonlord, "AskForGeneral")
|
||||
for i, p in ipairs(nonlord) do
|
||||
local arg = table.slice(generals, (i - 1) * generalNum + 1, i * generalNum + 1)
|
||||
p.request_data = json.encode{ arg, n }
|
||||
p.default_reply = table.random(arg, n)
|
||||
req:setData(p, { arg, n })
|
||||
req:setDefaultReply(p, table.random(arg, n))
|
||||
end
|
||||
|
||||
room:notifyMoveFocus(nonlord, "AskForGeneral")
|
||||
room:doBroadcastRequest("AskForGeneral", nonlord)
|
||||
|
||||
for _, p in ipairs(nonlord) do
|
||||
local general, deputy
|
||||
if p.general == "" and p.reply_ready then
|
||||
local general_ret = json.decode(p.client_reply)
|
||||
general = general_ret[1]
|
||||
deputy = general_ret[2]
|
||||
else
|
||||
general = p.default_reply[1]
|
||||
deputy = p.default_reply[2]
|
||||
end
|
||||
room:findGeneral(general)
|
||||
room:findGeneral(deputy)
|
||||
local result = req:getResult(p)
|
||||
local general, deputy = result[1], result[2]
|
||||
room:prepareGeneral(p, general, deputy)
|
||||
p.default_reply = ""
|
||||
end
|
||||
|
||||
room:askForChooseKingdom(nonlord)
|
||||
|
@ -233,7 +222,9 @@ function GameLogic:attachSkillToPlayers()
|
|||
return
|
||||
end
|
||||
|
||||
room:handleAddLoseSkills(player, skillName, nil, false)
|
||||
room:handleAddLoseSkills(player, skillName, nil, false, true)
|
||||
self:trigger(fk.EventAcquireSkill, player, skill)
|
||||
skill:onAcquire(player, true)
|
||||
end
|
||||
for _, p in ipairs(room.alive_players) do
|
||||
local skills = Fk.generals[p.general].skills
|
||||
|
@ -278,6 +269,8 @@ function GameLogic:action()
|
|||
while true do
|
||||
execGameEvent(GameEvent.Round)
|
||||
if room.game_finished then break end
|
||||
if table.every(room.players, function(p) return p.dead and p.rest == 0 end) then room:gameOver("") end
|
||||
room.current = room.players[1]
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -570,9 +563,10 @@ function GameLogic:getMostRecentEvent(eventType)
|
|||
end
|
||||
|
||||
--- 如果当前事件刚好是技能生效事件,就返回那个技能名,否则返回空串。
|
||||
---@return string|nil
|
||||
function GameLogic:getCurrentSkillName()
|
||||
local skillEvent = self:getCurrentEvent()
|
||||
local ret = ""
|
||||
local ret = nil
|
||||
if skillEvent.event == GameEvent.SkillEffect then
|
||||
local _, _, _skill = table.unpack(skillEvent.data)
|
||||
local skill = _skill.main_skill and _skill.main_skill or _skill
|
||||
|
@ -605,7 +599,7 @@ function GameLogic:getEventsOfScope(eventType, n, func, scope)
|
|||
end
|
||||
|
||||
-- 在指定历史范围中找符合条件的事件(逆序)
|
||||
---@param eventType integer @ 要查找的事件类型
|
||||
---@param eventType GameEvent @ 要查找的事件类型
|
||||
---@param func fun(e: GameEvent): boolean @ 过滤用的函数
|
||||
---@param n integer @ 最多找多少个
|
||||
---@param end_id integer @ 查询历史范围:从最后的事件开始逆序查找直到id为end_id的事件(不含)
|
||||
|
|
|
@ -29,7 +29,7 @@ MarkEnum.BypassTimesLimitTo = "BypassTimesLimitTo"
|
|||
MarkEnum.BypassDistancesLimitTo = "BypassDistancesLimitTo"
|
||||
---非锁定技失效
|
||||
MarkEnum.UncompulsoryInvalidity = "UncompulsoryInvalidity"
|
||||
---失效技能表
|
||||
---失效技能表(用``Room:addTableMark``和``Room:removeTableMark``控制)
|
||||
MarkEnum.InvalidSkills = "InvalidSkills"
|
||||
---不可明置(值为表,m - 主将, d - 副将)
|
||||
MarkEnum.RevealProhibited = "RevealProhibited"
|
||||
|
|
|
@ -1,19 +1,23 @@
|
|||
---@class Request : Object
|
||||
---@field public room Room
|
||||
---@field public players ServerPlayer[]
|
||||
---@field public n integer @ n个人做出回复后,询问结束
|
||||
---@field public accept_cancel? boolean @ 是否将取消也算作是收到回复
|
||||
---@field public n integer @ 产生n个winner后,询问直接结束
|
||||
---@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 public default_reply table<integer, any> @ 玩家id - 默认回复内容 默认空串
|
||||
---@field public send_encode boolean? @ 是否需要对data使用json.encode,默认true
|
||||
---@field public receive_decode boolean? @ 是否需要对reply使用json.decode,默认true
|
||||
---@field private send_success table<fk.ServerPlayer, boolean> @ 数据是否发送成功,不成功的后面全部视为AI
|
||||
---@field public result table<integer, any> @ 玩家id - 回复内容 nil表示完全未回复
|
||||
---@field public winners ServerPlayer[] @ 按肯定回复先后顺序排序 由于有概率所有人烧条 可能会空
|
||||
---@field public luck_data any? @ 是否是询问手气卡 TODO: 有需求的话把这个通用化一点
|
||||
---@field private pending_requests table<fk.ServerPlayer, integer[]> @ 一控多时暂存的请求
|
||||
---@field private _asked boolean? @ 是否询问过了
|
||||
---@field public focus_players? ServerPlayer[] @ 要moveFocus的玩家们 默认参与者
|
||||
---@field public focus_text? string @ 要moveFocus的文字 默认self.command
|
||||
local Request = class("Request")
|
||||
|
||||
-- TODO: 懒得思考了
|
||||
|
@ -23,9 +27,10 @@ local Request = class("Request")
|
|||
-- 若还能再用一次,那就重新发Request并继续等
|
||||
|
||||
---@param command string
|
||||
---@param players ServerPlayer[]
|
||||
---@param players ServerPlayer|ServerPlayer[]
|
||||
---@param n? integer
|
||||
function Request:initialize(command, players, n)
|
||||
function Request:initialize(players, command, n)
|
||||
if (not players[1]) and players.class then players = { players } end
|
||||
assert(#players > 0)
|
||||
self.command = command
|
||||
self.players = players
|
||||
|
@ -36,29 +41,43 @@ function Request:initialize(command, players, n)
|
|||
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.send_encode = true
|
||||
self.receive_decode = true -- 除了几个特殊字符串之外都decode
|
||||
|
||||
self.pending_requests = setmetatable({}, { __mode = "k" })
|
||||
self.send_success = setmetatable({}, { __mode = "k" })
|
||||
self.result = {}
|
||||
self.winners = {}
|
||||
end
|
||||
|
||||
function Request:__tostring()
|
||||
return "<Request>"
|
||||
return string.format("<Request '%s'>", self.command)
|
||||
end
|
||||
|
||||
---@param player ServerPlayer
|
||||
---@param data any
|
||||
function Request:setData(player, data)
|
||||
self.data[player.id] = data
|
||||
end
|
||||
|
||||
---@param player ServerPlayer
|
||||
---@param data any @ 注意不要json.encode
|
||||
function Request:setDefaultReply(player, data)
|
||||
self.default_reply[player.id] = data
|
||||
end
|
||||
|
||||
--- 获取本次Request中此人的回复,若还未询问过,那么先询问
|
||||
--- * <any>: 成功发出回复 获取的是decode后的回复
|
||||
--- * "" (空串): 发出了“取消” 或者烧完了绳子 反正就是取消
|
||||
---@param player ServerPlayer
|
||||
---@return any
|
||||
function Request:getResult(player)
|
||||
if not self._asked then self:ask() end
|
||||
return self.result[player.id]
|
||||
end
|
||||
|
||||
-- 将相应请求数据发给player
|
||||
-- 不能向thinking中的玩家发送,这种情况下暂存起来等待收到答复后
|
||||
---@param player ServerPlayer
|
||||
|
@ -86,7 +105,7 @@ function Request:_sendPacket(player)
|
|||
|
||||
-- 发送请求数据并将控制者标记为烧条中
|
||||
local jsonData = self.data[player.id]
|
||||
if self.send_json then jsonData = json.encode(jsonData) end
|
||||
if self.send_encode 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)
|
||||
|
@ -142,34 +161,21 @@ function Request:_checkReply(player, use_ai)
|
|||
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
|
||||
player.ai.data = self.data[player.id]
|
||||
reply = Pcall(player.ai.makeReply, player.ai)
|
||||
else
|
||||
-- 还没轮到AI呢,所以需要标记为未答复
|
||||
reply = "__notready"
|
||||
end
|
||||
end
|
||||
|
||||
if reply == '' then reply = '__cancel' 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()
|
||||
if self._asked then return end
|
||||
|
||||
local room = self.room
|
||||
-- 0. 设置计时器,防止因无人回复一直等下去
|
||||
room.room:setRequestTimer(self.timeout * 1000 + 500)
|
||||
|
@ -183,6 +189,10 @@ function Request:ask()
|
|||
p.serverplayer:setThinking(false)
|
||||
end
|
||||
|
||||
-- 发送focus
|
||||
room:notifyMoveFocus(self.focus_players or self.players, self.focus_text or self.command,
|
||||
math.floor(self.timeout * 1000))
|
||||
|
||||
-- 1. 向所有人发送询问请求
|
||||
for _, p in ipairs(players) do
|
||||
self:_sendPacket(p)
|
||||
|
@ -191,10 +201,9 @@ function Request:ask()
|
|||
-- 2. 进入循环等待,结束条件为已有n个回复或者超时或者有人点了
|
||||
-- 若很多人都取消了导致最多回复数达不到n了,那么也结束
|
||||
local replied_players = 0
|
||||
local ready_players = 0
|
||||
while true do
|
||||
local changed = false
|
||||
-- 判断1:若投降则直接结束全部询问,若超时则踢掉所有人类玩家(这样AI还可计算)
|
||||
-- 若投降则直接结束全部询问,若超时则踢掉所有人类玩家(让AI还可计算)
|
||||
if room.hasSurrendered then break end
|
||||
local elapsed = os.time() - currentTime
|
||||
if self.timeout - elapsed <= 0 or resume_reason == "request_timer" then
|
||||
|
@ -205,22 +214,23 @@ function Request:ask()
|
|||
end
|
||||
end
|
||||
|
||||
-- 若players中只剩人机,那么允许人机进行计算
|
||||
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
|
||||
|
||||
-- 轮询所有参与回答的玩家,如果作出了答复,那么就把他从名单移除;
|
||||
-- 然后如果作出的是“肯定”答复,那么添加到winner里面
|
||||
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
|
||||
if reply ~= "__cancel" and (self.receive_decode and not use_ai) then
|
||||
reply = json.decode(reply)
|
||||
end
|
||||
self.result[player.id] = reply
|
||||
|
@ -229,35 +239,39 @@ function Request:ask()
|
|||
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
|
||||
table.insert(self.winners, player)
|
||||
if #self.winners >= self.n then
|
||||
-- winner数量已经足够,剩下的人不用算了
|
||||
for _, p in ipairs(players) do
|
||||
-- 避免触发后续的烧条检测
|
||||
if self.result[p.id] == nil then
|
||||
self.result[p.id] = "__failed_in_race"
|
||||
end
|
||||
end
|
||||
players = {} -- 清空参与者名单
|
||||
break -- 注意外面还有一层循环
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if #players + ready_players < self.n then break end
|
||||
if ready_players >= self.n then break end
|
||||
if #players == 0 then break end
|
||||
if #self.winners >= self.n then break end
|
||||
|
||||
-- 防止万一,如果AI算完后还是有机器人notready的话也别等了
|
||||
-- 不然就永远别想被唤醒了
|
||||
if self.ai_start_time then break end
|
||||
|
||||
-- 需要等待呢,等待被唤醒吧
|
||||
-- 需要等待呢,等待被唤醒吧,唤醒后继续下一次轮询检测
|
||||
if not changed then
|
||||
resume_reason = coroutine.yield("__handleRequest")
|
||||
end
|
||||
end
|
||||
|
||||
room.room:destroyRequestTimer()
|
||||
self:finish()
|
||||
self:_finish()
|
||||
|
||||
self._asked = true
|
||||
end
|
||||
|
||||
local function surrenderCheck(room)
|
||||
|
@ -283,15 +297,19 @@ local function surrenderCheck(room)
|
|||
end
|
||||
|
||||
-- 善后工作,主要是result规范化、投降检测等
|
||||
function Request:finish()
|
||||
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)
|
||||
-- 这个什么timewaste_count也该扔了
|
||||
if self.result[p.id] == "__failed_in_race" then
|
||||
p:doNotify("CancelRequest", "")
|
||||
self.result[p.id] = self.default_reply[p.id] or ""
|
||||
end
|
||||
if self.result[p.id] == nil then
|
||||
self.result[p.id] = self.default_reply[p.id]
|
||||
self.result[p.id] = self.default_reply[p.id] or ""
|
||||
p._timewaste_count = p._timewaste_count + 1
|
||||
if p._timewaste_count >= 3 and p.serverplayer:getState() == fk.Player_Online then
|
||||
p._timewaste_count = 0
|
||||
|
@ -301,10 +319,7 @@ function Request:finish()
|
|||
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
|
||||
self.result[p.id] = (not self.accept_cancel) and self.default_reply[p.id] or ""
|
||||
end
|
||||
end
|
||||
room.last_request = self
|
||||
|
@ -312,7 +327,7 @@ function Request:finish()
|
|||
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
|
||||
local to_delay = 800 - (os.getms() - self.ai_start_time) / 1000
|
||||
room:delay(to_delay)
|
||||
break
|
||||
end
|
||||
|
|
|
@ -15,18 +15,20 @@
|
|||
---@field public game_finished boolean @ 游戏是否已经结束
|
||||
---@field public tag table<string, any> @ Tag清单,其实跟Player的标记是差不多的东西
|
||||
---@field public general_pile string[] @ 武将牌堆,这是可用武将名的数组
|
||||
---@field public disabled_packs string[] @ 未开启的扩展包名(是小包名,不是大包名)
|
||||
---@field public logic GameLogic @ 这个房间使用的游戏逻辑,可能根据游戏模式而变动
|
||||
---@field public request_queue table<userdata, table>
|
||||
---@field public request_self table<integer, integer>
|
||||
---@field public last_request Request @ 上一次完成的request
|
||||
---@field public skill_costs table<string, any> @ 存放skill.cost_data用
|
||||
---@field public card_marks table<integer, any> @ 存放card.mark之用
|
||||
---@field public current_cost_skill TriggerSkill? @ AI用
|
||||
local Room = AbstractRoom:subclass("Room")
|
||||
|
||||
-- load classes used by the game
|
||||
Request = require "server.network"
|
||||
GameEvent = require "server.gameevent"
|
||||
local GameEventWrappers = require "lua/server/events"
|
||||
GameEventWrappers = require "lua/server/events"
|
||||
Room:include(GameEventWrappers)
|
||||
GameLogic = require "server.gamelogic"
|
||||
ServerPlayer = require "server.serverplayer"
|
||||
|
@ -508,10 +510,11 @@ function Room:setDeputyGeneral(player, general)
|
|||
self:notifyProperty(player, player, "deputyGeneral")
|
||||
end
|
||||
|
||||
--- 为角色设置武将,并从武将池中抽出,若有隐匿技变为隐匿将。注意此时不会进行选择势力,请随后自行处理
|
||||
---@param player ServerPlayer
|
||||
---@param general string
|
||||
---@param deputy string
|
||||
---@param broadcast boolean|nil
|
||||
---@param general string @ 主将名
|
||||
---@param deputy? string @ 副将名
|
||||
---@param broadcast? boolean @ 是否公示,默认否
|
||||
function Room:prepareGeneral(player, general, deputy, broadcast)
|
||||
self:findGeneral(general)
|
||||
self:findGeneral(deputy)
|
||||
|
@ -597,9 +600,9 @@ end
|
|||
function Room:doRequest(player, command, jsonData, wait)
|
||||
-- fk.qCritical("Room:doRequest is deprecated!")
|
||||
if wait == true then error("wait can't be true") end
|
||||
local request = Request:new(command, {player})
|
||||
request.send_json = false -- 因为参数已经json.encode过了,该死的兼容性
|
||||
request.receive_json = false
|
||||
local request = Request:new(player, command)
|
||||
request.send_encode = false -- 因为参数已经json.encode过了,该死的兼容性
|
||||
request.receive_decode = false
|
||||
request.accept_cancel = true
|
||||
request:setData(player, jsonData)
|
||||
request:ask()
|
||||
|
@ -613,9 +616,9 @@ end
|
|||
function Room:doBroadcastRequest(command, players, jsonData)
|
||||
-- fk.qCritical("Room:doBroadcastRequest is deprecated!")
|
||||
players = players or self.players
|
||||
local request = Request:new(command, players)
|
||||
request.send_json = false -- 因为参数已经json.encode过了
|
||||
request.receive_json = false
|
||||
local request = Request:new(players, command)
|
||||
request.send_encode = false -- 因为参数已经json.encode过了
|
||||
request.receive_decode = false
|
||||
request.accept_cancel = true
|
||||
for _, p in ipairs(players) do
|
||||
request:setData(p, jsonData or p.request_data)
|
||||
|
@ -635,14 +638,14 @@ end
|
|||
function Room:doRaceRequest(command, players, jsonData)
|
||||
-- fk.qCritical("Room:doRaceRequest is deprecated!")
|
||||
players = players or self.players
|
||||
local request = Request:new(command, players, 1)
|
||||
request.send_json = false -- 因为参数已经json.encode过了
|
||||
request.receive_json = false
|
||||
local request = Request:new(players, command, 1)
|
||||
request.send_encode = false -- 因为参数已经json.encode过了
|
||||
request.receive_decode = false
|
||||
for _, p in ipairs(players) do
|
||||
request:setData(p, jsonData or p.request_data)
|
||||
end
|
||||
request:ask()
|
||||
return request:getWinners()[1]
|
||||
return request.winners[1]
|
||||
end
|
||||
|
||||
--- 延迟一段时间。
|
||||
|
@ -656,6 +659,15 @@ function Room:delay(ms)
|
|||
coroutine.yield("__handleRequest", ms)
|
||||
end
|
||||
|
||||
--- 延迟一段时间。界面上会显示所有人读条了。注意这个只能延迟多少秒。
|
||||
---@param sec integer @ 要延迟的秒数
|
||||
function Room:animDelay(sec)
|
||||
local req = Request:new(self.alive_players, "EmptyRequest")
|
||||
req.focus_text = ''
|
||||
req.timeout = sec
|
||||
req:ask()
|
||||
end
|
||||
|
||||
--- 向多名玩家告知一次移牌行为。
|
||||
---@param players? ServerPlayer[] @ 要被告知的玩家列表,默认为全员
|
||||
---@param card_moves CardsMoveStruct[] @ 要告知的移牌信息列表
|
||||
|
@ -691,7 +703,8 @@ end
|
|||
--- 形象点说,就是在那些玩家下面显示一个“弃牌 思考中...”之类的烧条提示。
|
||||
---@param players ServerPlayer | ServerPlayer[] @ 要获得焦点的一名或者多名角色
|
||||
---@param command string @ 烧条的提示文字
|
||||
function Room:notifyMoveFocus(players, command)
|
||||
---@param timeout integer? @ focus的烧条时长
|
||||
function Room:notifyMoveFocus(players, command, timeout)
|
||||
if (players.class) then
|
||||
players = {players}
|
||||
end
|
||||
|
@ -712,7 +725,8 @@ function Room:notifyMoveFocus(players, command)
|
|||
|
||||
self:doBroadcastNotify("MoveFocus", json.encode{
|
||||
ids,
|
||||
command
|
||||
command,
|
||||
timeout
|
||||
})
|
||||
end
|
||||
|
||||
|
@ -896,18 +910,20 @@ function Room:askForUseActiveSkill(player, skill_name, prompt, cancelable, extra
|
|||
end
|
||||
|
||||
local command = "AskForUseActiveSkill"
|
||||
self:notifyMoveFocus(player, extra_data.skillName or skill_name) -- for display skill name instead of command name
|
||||
local data = {skill_name, prompt, cancelable, extra_data}
|
||||
|
||||
Fk.currentResponseReason = extra_data.skillName
|
||||
local result = self:doRequest(player, command, json.encode(data))
|
||||
local req = Request:new(player, command)
|
||||
req:setData(player, data)
|
||||
req.focus_text = extra_data.skillName or skill_name
|
||||
local result = req:getResult(player)
|
||||
Fk.currentResponseReason = nil
|
||||
|
||||
if result == "" then
|
||||
return false
|
||||
end
|
||||
|
||||
data = json.decode(result)
|
||||
data = result
|
||||
local card = data.card
|
||||
local targets = data.targets
|
||||
local card_data = json.decode(card)
|
||||
|
@ -969,7 +985,7 @@ function Room:askForDiscard(player, minNum, maxNum, includeEquip, skillName, can
|
|||
return false
|
||||
end
|
||||
end
|
||||
if skillName == "game_rule" then
|
||||
if skillName == "phase_discard" then
|
||||
status_skills = Fk:currentRoom().status_skills[MaxCardsSkill] or Util.DummyTable
|
||||
for _, skill in ipairs(status_skills) do
|
||||
if skill:excludeFrom(player, card) then
|
||||
|
@ -1213,7 +1229,7 @@ end
|
|||
---@param minNum? integer @ 最少交出的卡牌数,默认0
|
||||
---@param maxNum? integer @ 最多交出的卡牌数,默认所有牌
|
||||
---@param prompt? string @ 询问提示信息
|
||||
---@param expand_pile? string @ 可选私人牌堆名称,如要分配你武将牌上的牌请填写
|
||||
---@param expand_pile? string|integer[] @ 可选私人牌堆名称,如要分配你武将牌上的牌请填写
|
||||
---@param skipMove? boolean @ 是否跳过移动。默认不跳过
|
||||
---@param single_max? integer|table @ 限制每人能获得的最大牌数。输入整数或(以角色id为键以整数为值)的表
|
||||
---@return table<integer, integer[]> @ 返回一个表,键为角色id转字符串,值为分配给其的牌id数组
|
||||
|
@ -1346,7 +1362,7 @@ function Room:returnToGeneralPile(g, position)
|
|||
end
|
||||
|
||||
--- 抽特定名字的武将(抽了就没了)
|
||||
---@param name string @ 武将name,如找不到则查找truename,再找不到则返回nil
|
||||
---@param name string? @ 武将name,如找不到则查找truename,再找不到则返回nil
|
||||
---@return string? @ 抽出的武将名
|
||||
function Room:findGeneral(name)
|
||||
if not Fk.generals[name] then return nil end
|
||||
|
@ -1384,25 +1400,18 @@ end
|
|||
---@return string|string[] @ 选择的武将
|
||||
function Room:askForGeneral(player, generals, n, noConvert)
|
||||
local command = "AskForGeneral"
|
||||
self:notifyMoveFocus(player, command)
|
||||
|
||||
n = n or 1
|
||||
if #generals == n then return n == 1 and generals[1] or generals end
|
||||
local defaultChoice = table.random(generals, n)
|
||||
|
||||
if (player.serverplayer:getState() == fk.Player_Online) then
|
||||
local result = self:doRequest(player, command, json.encode{ generals, n, noConvert })
|
||||
local choices
|
||||
if result == "" then
|
||||
choices = defaultChoice
|
||||
else
|
||||
choices = json.decode(result)
|
||||
end
|
||||
if #choices == 1 then return choices[1] end
|
||||
return choices
|
||||
end
|
||||
|
||||
return n == 1 and defaultChoice[1] or defaultChoice
|
||||
local req = Request:new(player, command)
|
||||
local data = { generals, n, noConvert }
|
||||
req:setData(player, data)
|
||||
req:setDefaultReply(player, defaultChoice)
|
||||
local choices = req:getResult(player)
|
||||
if #choices == 1 then return choices[1] end
|
||||
return choices
|
||||
end
|
||||
|
||||
--- 询问玩家若为神将、双势力需选择一个势力。
|
||||
|
@ -1414,7 +1423,9 @@ function Room:askForChooseKingdom(players)
|
|||
end)
|
||||
|
||||
if #specialKingdomPlayers > 0 then
|
||||
local choiceMap = {}
|
||||
local req = Request:new(specialKingdomPlayers, "AskForChoice")
|
||||
req.focus_text = "AskForKingdom"
|
||||
req.receive_decode = false
|
||||
for _, p in ipairs(specialKingdomPlayers) do
|
||||
local allKingdoms = {}
|
||||
local curGeneral = Fk.generals[p.general]
|
||||
|
@ -1424,25 +1435,13 @@ function Room:askForChooseKingdom(players)
|
|||
allKingdoms = Fk:getKingdomMap(p.kingdom)
|
||||
end
|
||||
if #allKingdoms > 0 then
|
||||
choiceMap[p.id] = allKingdoms
|
||||
|
||||
local data = json.encode({ allKingdoms, allKingdoms, "AskForKingdom", "#ChooseInitialKingdom" })
|
||||
p.request_data = data
|
||||
req:setData(p, { allKingdoms, allKingdoms, "AskForKingdom", "#ChooseInitialKingdom" })
|
||||
req:setDefaultReply(p, allKingdoms[1])
|
||||
end
|
||||
end
|
||||
|
||||
self:notifyMoveFocus(players, "AskForKingdom")
|
||||
self:doBroadcastRequest("AskForChoice", specialKingdomPlayers)
|
||||
|
||||
for _, p in ipairs(specialKingdomPlayers) do
|
||||
local kingdomChosen
|
||||
if p.reply_ready then
|
||||
kingdomChosen = p.client_reply
|
||||
else
|
||||
kingdomChosen = choiceMap[p.id][1]
|
||||
end
|
||||
|
||||
p.kingdom = kingdomChosen
|
||||
p.kingdom = req:getResult(p)
|
||||
self:notifyProperty(p, p, "kingdom")
|
||||
end
|
||||
end
|
||||
|
@ -1458,9 +1457,10 @@ end
|
|||
function Room:askForCardChosen(chooser, target, flag, reason, prompt)
|
||||
local command = "AskForCardChosen"
|
||||
prompt = prompt or ""
|
||||
self:notifyMoveFocus(chooser, command)
|
||||
local data = {target.id, flag, reason, prompt}
|
||||
local result = self:doRequest(chooser, command, json.encode(data))
|
||||
local req = Request:new(chooser, command)
|
||||
req:setData(chooser, data)
|
||||
local result = req:getResult(chooser)
|
||||
|
||||
if result == "" then
|
||||
local areas = {}
|
||||
|
@ -1478,8 +1478,6 @@ function Room:askForCardChosen(chooser, target, flag, reason, prompt)
|
|||
end
|
||||
if #handcards == 0 then return end
|
||||
result = handcards[math.random(1, #handcards)]
|
||||
else
|
||||
result = tonumber(result)
|
||||
end
|
||||
|
||||
if result == -1 then
|
||||
|
@ -1507,18 +1505,20 @@ function Room:askForPoxi(player, poxi_type, data, extra_data, cancelable)
|
|||
if not poxi then return {} end
|
||||
|
||||
local command = "AskForPoxi"
|
||||
self:notifyMoveFocus(player, poxi_type)
|
||||
local result = self:doRequest(player, command, json.encode {
|
||||
local req = Request:new(player, command)
|
||||
req.focus_text = poxi_type
|
||||
req:setData(player, {
|
||||
type = poxi_type,
|
||||
data = data,
|
||||
extra_data = extra_data,
|
||||
cancelable = (cancelable == nil) and true or cancelable
|
||||
})
|
||||
local result = req:getResult(player)
|
||||
|
||||
if result == "" then
|
||||
return poxi.default_choice(data, extra_data)
|
||||
else
|
||||
return poxi.post_select(json.decode(result), data, extra_data)
|
||||
return poxi.post_select(result, data, extra_data)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1535,7 +1535,7 @@ end
|
|||
---@return integer[] @ 选择的id
|
||||
function Room:askForCardsChosen(chooser, target, min, max, flag, reason, prompt)
|
||||
if min == 1 and max == 1 then
|
||||
return { self:askForCardChosen(chooser, target, flag, reason) }
|
||||
return { self:askForCardChosen(chooser, target, flag, reason, prompt) }
|
||||
end
|
||||
|
||||
local cards
|
||||
|
@ -1607,10 +1607,15 @@ function Room:askForChoice(player, choices, skill_name, prompt, detailed, all_ch
|
|||
local command = "AskForChoice"
|
||||
prompt = prompt or ""
|
||||
all_choices = all_choices or choices
|
||||
self:notifyMoveFocus(player, skill_name)
|
||||
local result = self:doRequest(player, command, json.encode{
|
||||
|
||||
local req = Request:new(player, command)
|
||||
req.focus_text = skill_name
|
||||
req.receive_decode = false -- 这个不用decode
|
||||
req:setData(player, {
|
||||
choices, all_choices, skill_name, prompt, detailed
|
||||
})
|
||||
local result = req:getResult(player)
|
||||
|
||||
if result == "" then
|
||||
if table.contains(choices, "Cancel") then
|
||||
result = "Cancel"
|
||||
|
@ -1637,15 +1642,19 @@ function Room:askForChoices(player, choices, minNum, maxNum, skill_name, prompt,
|
|||
if #choices <= minNum and not all_choices and not cancelable then return choices end
|
||||
assert(minNum <= maxNum)
|
||||
assert(not all_choices or table.every(choices, function(c) return table.contains(all_choices, c) end))
|
||||
|
||||
local command = "AskForChoices"
|
||||
skill_name = skill_name or ""
|
||||
prompt = prompt or ""
|
||||
all_choices = all_choices or choices
|
||||
detailed = detailed or false
|
||||
self:notifyMoveFocus(player, skill_name)
|
||||
local result = self:doRequest(player, command, json.encode{
|
||||
|
||||
local req = Request:new(player, command)
|
||||
req.focus_text = skill_name
|
||||
req:setData(player, {
|
||||
choices, all_choices, {minNum, maxNum}, cancelable, skill_name, prompt, detailed
|
||||
})
|
||||
local result = req:getResult(player)
|
||||
if result == "" then
|
||||
if cancelable then
|
||||
return {}
|
||||
|
@ -1653,7 +1662,7 @@ function Room:askForChoices(player, choices, minNum, maxNum, skill_name, prompt,
|
|||
return table.random(choices, math.min(minNum, #choices))
|
||||
end
|
||||
end
|
||||
return json.decode(result)
|
||||
return result
|
||||
end
|
||||
|
||||
--- 询问玩家是否发动技能。
|
||||
|
@ -1664,11 +1673,11 @@ end
|
|||
---@return boolean
|
||||
function Room:askForSkillInvoke(player, skill_name, data, prompt)
|
||||
local command = "AskForSkillInvoke"
|
||||
self:notifyMoveFocus(player, skill_name)
|
||||
local invoked = false
|
||||
local result = self:doRequest(player, command, json.encode{ skill_name, prompt })
|
||||
if result ~= "" then invoked = true end
|
||||
return invoked
|
||||
local req = Request:new(player, command)
|
||||
req.focus_text = skill_name
|
||||
req.receive_decode = false -- 这个返回的都是"1" 不用decode
|
||||
req:setData(player, { skill_name, prompt })
|
||||
return req:getResult(player) ~= ""
|
||||
end
|
||||
|
||||
-- 获取使用牌的合法额外目标(【借刀杀人】等带副目标的卡牌除外)
|
||||
|
@ -1795,7 +1804,9 @@ function Room:askForArrangeCards(player, skillname, cardMap, prompt, free_arrang
|
|||
poxi_type = poxi_type or "",
|
||||
cancelable = ((pattern ~= "." or poxi_type ~= "") and (default_choice == nil))
|
||||
}
|
||||
local result = self:doRequest(player, command, json.encode(data))
|
||||
local req = Request:new(player, command)
|
||||
req:setData(player, data)
|
||||
local result = req:getResult(player)
|
||||
-- local result = player.room:askForCustomDialog(player, skillname,
|
||||
-- "RoomElement/ArrangeCardsBox.qml", {
|
||||
-- cardMap, prompt, box_size, max_limit, min_limit, free_arrange or false, areaNames,
|
||||
|
@ -1829,7 +1840,7 @@ function Room:askForArrangeCards(player, skillname, cardMap, prompt, free_arrang
|
|||
end
|
||||
return cardMap
|
||||
end
|
||||
return json.decode(result)
|
||||
return result
|
||||
end
|
||||
|
||||
-- TODO: guanxing type
|
||||
|
@ -1867,7 +1878,6 @@ function Room:askForGuanxing(player, cards, top_limit, bottom_limit, customNotif
|
|||
areaNames = { "Top", "Bottom" }
|
||||
end
|
||||
local command = "AskForGuanxing"
|
||||
self:notifyMoveFocus(player, customNotify or command)
|
||||
local max_top = top_limit[2]
|
||||
local card_map = {}
|
||||
if max_top > 0 then
|
||||
|
@ -1888,10 +1898,13 @@ function Room:askForGuanxing(player, cards, top_limit, bottom_limit, customNotif
|
|||
bottom_area_name = areaNames[2],
|
||||
}
|
||||
|
||||
local result = self:doRequest(player, command, json.encode(data))
|
||||
local req = Request:new(player, command)
|
||||
req.focus_text = customNotify
|
||||
req:setData(player, data)
|
||||
local result = req:getResult(player)
|
||||
local top, bottom
|
||||
if result ~= "" then
|
||||
local d = json.decode(result)
|
||||
local d = result
|
||||
if top_limit[2] == 0 then
|
||||
top = Util.DummyTable
|
||||
bottom = d[1]
|
||||
|
@ -1942,15 +1955,17 @@ function Room:askForExchange(player, piles, piles_name, customNotify)
|
|||
elseif x < 0 then
|
||||
piles_name = table.slice(piles_name, 1, #piles + 1)
|
||||
end
|
||||
self:notifyMoveFocus(player, customNotify or command)
|
||||
local data = {
|
||||
piles = piles,
|
||||
piles_name = piles_name,
|
||||
}
|
||||
local result = self:doRequest(player, command, json.encode(data))
|
||||
|
||||
local req = Request:new(player, command)
|
||||
req.focus_text = customNotify
|
||||
req:setData(player, data)
|
||||
local result = req:getResult(player)
|
||||
if result ~= "" then
|
||||
local d = json.decode(result)
|
||||
return d
|
||||
return result
|
||||
else
|
||||
return piles
|
||||
end
|
||||
|
@ -1960,7 +1975,7 @@ end
|
|||
---@param data string
|
||||
---@return CardUseStruct
|
||||
function Room:handleUseCardReply(player, data)
|
||||
data = json.decode(data)
|
||||
-- data = json.decode(data)
|
||||
local card = data.card
|
||||
local targets = data.targets
|
||||
if type(card) == "string" then
|
||||
|
@ -2062,7 +2077,6 @@ function Room:askForUseCard(player, card_name, pattern, prompt, cancelable, extr
|
|||
end
|
||||
|
||||
local command = "AskForUseCard"
|
||||
self:notifyMoveFocus(player, card_name)
|
||||
cancelable = (cancelable == nil) and true or cancelable
|
||||
extra_data = extra_data or Util.DummyTable
|
||||
prompt = prompt or ""
|
||||
|
@ -2088,7 +2102,12 @@ function Room:askForUseCard(player, card_name, pattern, prompt, cancelable, extr
|
|||
|
||||
Fk.currentResponsePattern = pattern
|
||||
self.logic:trigger(fk.HandleAskForPlayCard, nil, askForUseCardData, true)
|
||||
local result = self:doRequest(player, command, json.encode(data))
|
||||
|
||||
local req = Request:new(player, command)
|
||||
req.focus_text = card_name or ""
|
||||
req:setData(player, data)
|
||||
local result = req:getResult(player)
|
||||
|
||||
askForUseCardData.afterRequest = true
|
||||
self.logic:trigger(fk.HandleAskForPlayCard, nil, askForUseCardData, true)
|
||||
Fk.currentResponsePattern = nil
|
||||
|
@ -2124,7 +2143,6 @@ function Room:askForResponse(player, card_name, pattern, prompt, cancelable, ext
|
|||
end
|
||||
|
||||
local command = "AskForResponseCard"
|
||||
self:notifyMoveFocus(player, card_name)
|
||||
cancelable = (cancelable == nil) and true or cancelable
|
||||
extra_data = extra_data or Util.DummyTable
|
||||
pattern = pattern or card_name
|
||||
|
@ -2152,7 +2170,12 @@ function Room:askForResponse(player, card_name, pattern, prompt, cancelable, ext
|
|||
Fk.currentResponsePattern = pattern
|
||||
eventData.isResponse = true
|
||||
self.logic:trigger(fk.HandleAskForPlayCard, nil, eventData, true)
|
||||
local result = self:doRequest(player, command, json.encode(data))
|
||||
|
||||
local req = Request:new(player, command)
|
||||
req.focus_text = card_name or ""
|
||||
req:setData(player, data)
|
||||
local result = req:getResult(player)
|
||||
|
||||
eventData.afterRequest = true
|
||||
self.logic:trigger(fk.HandleAskForPlayCard, nil, eventData, true)
|
||||
Fk.currentResponsePattern = nil
|
||||
|
@ -2206,8 +2229,6 @@ function Room:askForNullification(players, card_name, pattern, prompt, cancelabl
|
|||
|
||||
repeat
|
||||
useResult = nil
|
||||
self:notifyMoveFocus(self.alive_players, card_name)
|
||||
self:doBroadcastNotify("WaitForNullification", "")
|
||||
|
||||
local data = {card_name, pattern, prompt, cancelable, extra_data, disabledSkillNames}
|
||||
|
||||
|
@ -2220,12 +2241,19 @@ function Room:askForNullification(players, card_name, pattern, prompt, cancelabl
|
|||
eventData = effectData,
|
||||
}
|
||||
self.logic:trigger(fk.HandleAskForPlayCard, nil, eventData, true)
|
||||
local winner = self:doRaceRequest(command, players, json.encode(data))
|
||||
|
||||
local req = Request:new(players, command, 1)
|
||||
req.focus_players = self.alive_players
|
||||
req.focus_text = card_name
|
||||
for _, p in ipairs(players) do req:setData(p, data) end
|
||||
req:ask()
|
||||
local winner = req.winners[1]
|
||||
|
||||
eventData.afterRequest = true
|
||||
self.logic:trigger(fk.HandleAskForPlayCard, nil, eventData, true)
|
||||
|
||||
if winner then
|
||||
local result = winner.client_reply
|
||||
local result = req:getResult(winner)
|
||||
useResult = self:handleUseCardReply(winner, result)
|
||||
|
||||
if type(useResult) == "string" and useResult ~= "" then
|
||||
|
@ -2259,13 +2287,17 @@ function Room:askForAG(player, id_list, cancelable, reason)
|
|||
end
|
||||
|
||||
local command = "AskForAG"
|
||||
self:notifyMoveFocus(player, reason or command)
|
||||
|
||||
local data = { id_list, cancelable, reason }
|
||||
local ret = self:doRequest(player, command, json.encode(data))
|
||||
local req = Request:new(player, command)
|
||||
req.focus_text = reason
|
||||
req:setData(player, data)
|
||||
local ret = req:getResult(player)
|
||||
|
||||
if ret == "" and not cancelable then
|
||||
ret = table.random(id_list)
|
||||
end
|
||||
return tonumber(ret)
|
||||
return ret
|
||||
end
|
||||
|
||||
--- 给player发一条消息,在他的窗口中用一系列卡牌填充一个AG。
|
||||
|
@ -2356,22 +2388,22 @@ function Room:askForMiniGame(players, focus, game_type, data_table)
|
|||
local command = "MiniGame"
|
||||
local game = Fk.mini_games[game_type]
|
||||
if #players == 0 or not game then return end
|
||||
|
||||
local req = Request:new(players, command)
|
||||
req.focus_text = focus
|
||||
req.receive_decode = false -- 和customDialog同理
|
||||
|
||||
for _, p in ipairs(players) do
|
||||
local data = data_table[p.id]
|
||||
p.mini_game_data = { type = game_type, data = data }
|
||||
p.request_data = json.encode(p.mini_game_data)
|
||||
p.default_reply = game.default_choice and json.encode(game.default_choice(p, data)) or ""
|
||||
req:setData(p, p.mini_game_data)
|
||||
req:setDefaultReply(p, game.default_choice and json.encode(game.default_choice(p, data)))
|
||||
end
|
||||
|
||||
self:notifyMoveFocus(players, focus)
|
||||
self:doBroadcastRequest(command, players)
|
||||
req:ask()
|
||||
|
||||
for _, p in ipairs(players) do
|
||||
p.mini_game_data = nil
|
||||
if not p.reply_ready then
|
||||
p.client_reply = p.default_reply
|
||||
p.reply_ready = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -2386,11 +2418,14 @@ end
|
|||
---@return string
|
||||
function Room:askForCustomDialog(player, focustxt, qmlPath, extra_data)
|
||||
local command = "CustomDialog"
|
||||
self:notifyMoveFocus(player, focustxt)
|
||||
return self:doRequest(player, command, json.encode{
|
||||
local req = Request:new(player, command)
|
||||
req.focus_text = focustxt
|
||||
req.receive_decode = false -- 没法知道要不要decode,所以我写false (json.decode该杀啊)
|
||||
req:setData(player, {
|
||||
path = qmlPath,
|
||||
data = extra_data,
|
||||
})
|
||||
return req:getResult(player)
|
||||
end
|
||||
|
||||
--- 询问移动场上的一张牌
|
||||
|
@ -2475,14 +2510,13 @@ function Room:askForMoveCardInBoard(player, targetOne, targetTwo, skillName, fla
|
|||
playerIds = { targetOne.id, targetTwo.id }
|
||||
}
|
||||
local command = "AskForMoveCardInBoard"
|
||||
self:notifyMoveFocus(player, command)
|
||||
local result = self:doRequest(player, command, json.encode(data))
|
||||
local req = Request:new(player, command)
|
||||
req:setData(player, data)
|
||||
local result = req:getResult(player)
|
||||
|
||||
if result == "" then
|
||||
local randomIndex = math.random(1, #cards)
|
||||
result = { cardId = cards[randomIndex], pos = cardsPosition[randomIndex] }
|
||||
else
|
||||
result = json.decode(result)
|
||||
end
|
||||
|
||||
local from, to
|
||||
|
@ -2942,10 +2976,5 @@ function Room:removeTableMark(sth, mark, value)
|
|||
end
|
||||
end
|
||||
|
||||
function Room:__index(k)
|
||||
if k == "room_settings" then
|
||||
return self.settings
|
||||
end
|
||||
end
|
||||
|
||||
return Room
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
local Room = require "server.room"
|
||||
Room = require "server.room"
|
||||
|
||||
-- 所有当前正在运行的房间(即游戏尚未结束的房间)
|
||||
---@type table<integer, Room>
|
||||
|
|
|
@ -30,13 +30,6 @@ function ServerPlayer:initialize(_self)
|
|||
self.id = _self:getId()
|
||||
self.room = nil
|
||||
|
||||
-- Below are for doBroadcastRequest
|
||||
-- 但是几乎全部被船新request杀了
|
||||
self.request_data = ""
|
||||
--self.client_reply = ""
|
||||
self.default_reply = ""
|
||||
--self.reply_ready = false
|
||||
--self.reply_cancel = false
|
||||
self.phases = {}
|
||||
self.skipped_phases = {}
|
||||
self.phase_state = {}
|
||||
|
@ -47,7 +40,7 @@ function ServerPlayer:initialize(_self)
|
|||
self._prelighted_skills = {}
|
||||
|
||||
self._timewaste_count = 0
|
||||
self.ai = RandomAI:new(self)
|
||||
self.ai = SmartAI:new(self)
|
||||
end
|
||||
|
||||
---@param command string
|
||||
|
@ -284,6 +277,9 @@ end
|
|||
function ServerPlayer:play(phase_table)
|
||||
phase_table = phase_table or {}
|
||||
if #phase_table > 0 then
|
||||
if not table.contains(phase_table, Player.RoundStart) then
|
||||
table.insert(phase_table, 1, Player.RoundStart)
|
||||
end
|
||||
if not table.contains(phase_table, Player.NotActive) then
|
||||
table.insert(phase_table, Player.NotActive)
|
||||
end
|
||||
|
@ -384,17 +380,19 @@ function ServerPlayer:endCurrentPhase()
|
|||
end
|
||||
|
||||
--- 获得一个额外回合
|
||||
---@param delay? boolean
|
||||
---@param skillName? string
|
||||
function ServerPlayer:gainAnExtraTurn(delay, skillName)
|
||||
---@param delay? boolean @ 是否延迟到当前回合结束再开启额外回合,默认是
|
||||
---@param skillName? string @ 额外回合原因
|
||||
---@param turnData? TurnStruct @ 额外回合的信息
|
||||
function ServerPlayer:gainAnExtraTurn(delay, skillName, turnData)
|
||||
local room = self.room
|
||||
delay = (delay == nil) and true or delay
|
||||
skillName = (skillName == nil) and room.logic:getCurrentSkillName() or skillName
|
||||
skillName = skillName or room.logic:getCurrentSkillName() or "game_rule"
|
||||
turnData = turnData or {}
|
||||
turnData.reason = skillName
|
||||
if delay then
|
||||
local logic = room.logic
|
||||
local turn = logic:getCurrentEvent():findParent(GameEvent.Turn, true)
|
||||
local turn = room.logic:getCurrentEvent():findParent(GameEvent.Turn, true)
|
||||
if turn then
|
||||
turn:prependExitFunc(function() self:gainAnExtraTurn(false, skillName) end)
|
||||
turn:prependExitFunc(function() self:gainAnExtraTurn(false, skillName, turnData) end)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
@ -407,13 +405,15 @@ function ServerPlayer:gainAnExtraTurn(delay, skillName)
|
|||
local current = room.current
|
||||
room.current = self
|
||||
|
||||
self.tag["_extra_turn_count"] = self.tag["_extra_turn_count"] or {}
|
||||
local ex_tag = self.tag["_extra_turn_count"]
|
||||
table.insert(ex_tag, skillName)
|
||||
room:addTableMark(self, "_extra_turn_count", skillName)
|
||||
|
||||
GameEvent.Turn:create(self):exec()
|
||||
GameEvent.Turn:create(self, turnData):exec()
|
||||
|
||||
table.remove(ex_tag)
|
||||
local mark = self:getTableMark("_extra_turn_count")
|
||||
if #mark > 0 then
|
||||
table.remove(mark)
|
||||
room:setPlayerMark(self, "_extra_turn_count", mark)
|
||||
end
|
||||
|
||||
room.current = current
|
||||
end
|
||||
|
@ -421,17 +421,14 @@ end
|
|||
--- 当前是否处于额外的回合。
|
||||
--- @return boolean
|
||||
function ServerPlayer:insideExtraTurn()
|
||||
return self.tag["_extra_turn_count"] and #self.tag["_extra_turn_count"] > 0
|
||||
return self:getCurrentExtraTurnReason() ~= "game_rule"
|
||||
end
|
||||
|
||||
--- 当前额外回合的技能原因。
|
||||
--- 当前额外回合的技能原因。非额外回合则为game_rule
|
||||
---@return string
|
||||
function ServerPlayer:getCurrentExtraTurnReason()
|
||||
local ex_tag = self.tag["_extra_turn_count"]
|
||||
if (not ex_tag) or #ex_tag == 0 then
|
||||
return "game_rule"
|
||||
end
|
||||
return ex_tag[#ex_tag]
|
||||
local mark = self:getTableMark("_extra_turn_count")
|
||||
return mark[#mark] or "game_rule"
|
||||
end
|
||||
|
||||
--- 角色摸牌。
|
||||
|
@ -461,6 +458,7 @@ function ServerPlayer:bury()
|
|||
self:throwAllCards()
|
||||
self:throwAllMarks()
|
||||
self:clearPiles()
|
||||
self:onAllSkillLose()
|
||||
self:reset()
|
||||
end
|
||||
|
||||
|
@ -482,6 +480,12 @@ function ServerPlayer:throwAllCards(flag)
|
|||
self.room:throwCard(cardIds, "", self)
|
||||
end
|
||||
|
||||
function ServerPlayer:onAllSkillLose()
|
||||
for _, skill in ipairs(self:getAllSkills()) do
|
||||
skill:onLose(self, true)
|
||||
end
|
||||
end
|
||||
|
||||
function ServerPlayer:throwAllMarks()
|
||||
for name, _ in pairs(self.mark) do
|
||||
self.room:setPlayerMark(self, name, 0)
|
||||
|
|
|
@ -246,6 +246,11 @@ fk.IceDamage = 4
|
|||
---@field public skillName string @ 技能名
|
||||
---@field public fromPlace "top"|"bottom" @ 摸牌的位置
|
||||
|
||||
---@--- TurnStruct 回合事件的数据
|
||||
---@class TurnStruct
|
||||
---@field public reason string? @ 当前额外回合的原因,不为额外回合则为game_rule
|
||||
---@field public phase_table? Phase[] @ 此回合将进行的阶段,填空则为正常流程
|
||||
|
||||
--- 移动理由
|
||||
---@alias CardMoveReason integer
|
||||
fk.ReasonJustMove = 1
|
||||
|
|
|
@ -4,6 +4,11 @@ local SelectableItem = base.SelectableItem
|
|||
---@class CardItem: SelectableItem
|
||||
local CardItem = SelectableItem:subclass("CardItem")
|
||||
|
||||
function CardItem:initialize(scene, id)
|
||||
SelectableItem.initialize(self, scene, id)
|
||||
Fk:filterCard(id, Fk:currentRoom():getCardOwner(id))
|
||||
end
|
||||
|
||||
---@class Photo: SelectableItem
|
||||
---@field public state string
|
||||
local Photo = SelectableItem:subclass("Photo")
|
||||
|
|
|
@ -1,180 +1,10 @@
|
|||
--[[
|
||||
fk.ai_card.thunder__slash = fk.ai_card.slash
|
||||
fk.ai_use_play.thunder__slash = fk.ai_use_play.slash
|
||||
fk.ai_card.fire__slash = fk.ai_card.slash
|
||||
fk.ai_use_play.fire__slash = fk.ai_use_play.slash
|
||||
fk.ai_card.analeptic = {
|
||||
intention = 60, -- 身份值
|
||||
value = 5, -- 卡牌价值
|
||||
priority = 3 -- 使用优先值
|
||||
}
|
||||
local slash = fk.ai_skills["slash_skill"]
|
||||
local just_use = fk.ai_skills["__just_use"]
|
||||
local use_to_enemy = fk.ai_skills["__use_to_enemy"]
|
||||
SmartAI:setSkillAI("thunder__slash_skill", slash)
|
||||
SmartAI:setSkillAI("fire__slash_skill", slash)
|
||||
SmartAI:setSkillAI("analeptic_skill", just_use)
|
||||
SmartAI:setSkillAI("iron_chain_skill", just_use)
|
||||
SmartAI:setSkillAI("fire_attack_skill", use_to_enemy)
|
||||
SmartAI:setSkillAI("supply_shortage_skill", use_to_enemy)
|
||||
|
||||
fk.ai_use_play["analeptic"] = function(self, card)
|
||||
local cards = table.map(self.player:getCardIds("&he"), function(id)
|
||||
return Fk:getCardById(id)
|
||||
end)
|
||||
self:sortValue(cards)
|
||||
for _, sth in ipairs(self:getActives("slash")) do
|
||||
local slash = nil
|
||||
if sth:isInstanceOf(Card) then
|
||||
if sth.skill:canUse(self.player, sth) and not self.player:prohibitUse(sth) then
|
||||
slash = sth
|
||||
end
|
||||
else
|
||||
local selected = {}
|
||||
for _, c in ipairs(cards) do
|
||||
if sth:cardFilter(c.id, selected) then
|
||||
table.insert(selected, c.id)
|
||||
end
|
||||
end
|
||||
local tc = sth:viewAs(selected)
|
||||
if tc and tc:matchPattern("slash") and tc.skill:canUse(self.player, tc) and not self.player:prohibitUse(tc) then
|
||||
slash = tc
|
||||
end
|
||||
end
|
||||
if slash then
|
||||
fk.ai_use_play.slash(self, slash)
|
||||
if self.use_id then
|
||||
self.use_id = card.id
|
||||
self.use_tos = {}
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
fk.ai_card.iron_chain = {
|
||||
intention = function(self, card, from)
|
||||
if self.player.chained then
|
||||
return -80
|
||||
end
|
||||
return 80
|
||||
end, -- 身份值
|
||||
value = 2, -- 卡牌价值
|
||||
priority = 3 -- 使用优先值
|
||||
}
|
||||
|
||||
fk.ai_use_play["iron_chain"] = function(self, card)
|
||||
for _, p in ipairs(self.friends) do
|
||||
if card.skill:targetFilter(p.id, self.use_tos, {}, card) and p.chained then
|
||||
table.insert(self.use_tos, p.id)
|
||||
end
|
||||
end
|
||||
self:sort(self.enemies)
|
||||
for _, p in ipairs(self.enemies) do
|
||||
if card.skill:targetFilter(p.id, self.use_tos, {}, card) and not p.chained then
|
||||
table.insert(self.use_tos, p.id)
|
||||
end
|
||||
end
|
||||
if #self.use_tos < 2 then
|
||||
self.use_tos = {}
|
||||
else
|
||||
self.use_id = card.id
|
||||
end
|
||||
end
|
||||
|
||||
fk.ai_use_play["recast"] = function(self, card)
|
||||
if self.command == "PlayCard" then
|
||||
self.use_id = card.id
|
||||
self.special_skill = "recast"
|
||||
end
|
||||
end
|
||||
|
||||
fk.ai_card.fire_attack = {
|
||||
intention = 90, -- 身份值
|
||||
value = 3, -- 卡牌价值
|
||||
priority = 4 -- 使用优先值
|
||||
}
|
||||
|
||||
fk.ai_use_play["fire_attack"] = function(self, card)
|
||||
self:sort(self.enemies)
|
||||
for _, p in ipairs(self.enemies) do
|
||||
if card.skill:targetFilter(p.id, self.use_tos, {}, card) and #self.player:getCardIds("h") > 2 then
|
||||
self.use_id = card.id
|
||||
table.insert(self.use_tos, p.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
fk.ai_discard["fire_attack_skill"] = function(self, min_num, num, include_equip, cancelable, pattern, prompt)
|
||||
local use = self:eventData("UseCard")
|
||||
for _, p in ipairs(TargetGroup:getRealTargets(use.tos)) do
|
||||
if self:isEnemy(p) then
|
||||
local cards = table.map(self.player:getCardIds("h"), function(id)
|
||||
return Fk:getCardById(id)
|
||||
end)
|
||||
local exp = Exppattern:Parse(pattern)
|
||||
cards = table.filter(cards, function(c)
|
||||
return exp:match(c)
|
||||
end)
|
||||
if #cards > 0 then
|
||||
self:sortValue(cards)
|
||||
return { cards[1].id }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
fk.ai_nullification.fire_attack = function(self, card, to, from, positive)
|
||||
if positive then
|
||||
if self:isFriend(to) and #to:getCardIds("h") > 0 and #from:getCardIds("h") > 0 then
|
||||
if #self.avail_cards > 1 or self:isWeak(to) or to.id == self.player.id then
|
||||
self.use_id = self.avail_cards[1]
|
||||
end
|
||||
end
|
||||
else
|
||||
if self:isEnemy(to) and #to:getCardIds("h") > 0 and #from:getCardIds("h") > 1 then
|
||||
if #self.avail_cards > 1 or self:isWeak(to) then
|
||||
self.use_id = self.avail_cards[1]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
fk.ai_card.fire_attack = {
|
||||
intention = 120, -- 身份值
|
||||
value = 2, -- 卡牌价值
|
||||
priority = 2 -- 使用优先值
|
||||
}
|
||||
|
||||
fk.ai_use_play["supply_shortage"] = function(self, card)
|
||||
self:sort(self.enemies)
|
||||
for _, p in ipairs(self.enemies) do
|
||||
if card.skill:targetFilter(p.id, self.use_tos, {}, card) and not p.chained then
|
||||
self.use_id = card.id
|
||||
table.insert(self.use_tos, p.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
fk.ai_nullification.supply_shortage = function(self, card, to, from, positive)
|
||||
if positive then
|
||||
if self:isFriend(to) then
|
||||
if #self.avail_cards > 1 or self:isWeak(to) or to.id == self.player.id then
|
||||
self.use_id = self.avail_cards[1]
|
||||
end
|
||||
end
|
||||
else
|
||||
if self:isEnemy(to) then
|
||||
if #self.avail_cards > 1 or self:isWeak(to) then
|
||||
self.use_id = self.avail_cards[1]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
fk.ai_card.supply_shortage = {
|
||||
intention = 130, -- 身份值
|
||||
value = 2, -- 卡牌价值
|
||||
priority = 1 -- 使用优先值
|
||||
}
|
||||
|
||||
fk.ai_skill_invoke["#fan_skill"] = function(self)
|
||||
local use = self:eventData("UseCard")
|
||||
for _, p in ipairs(TargetGroup:getRealTargets(use.tos)) do
|
||||
if not self:isFriend(p) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
--]]
|
||||
|
|
|
@ -1,55 +1,11 @@
|
|||
-- aux_skill的AI文件。aux_skill的重量级程度无需多说。
|
||||
-- 这个文件说是第二个smart_ai.lua也不为过。
|
||||
SmartAI:setSkillAI("discard_skill", {
|
||||
choose_targets = function(_, ai)
|
||||
return ai:doOKButton()
|
||||
end,
|
||||
})
|
||||
|
||||
-- discard_skill: 弃牌相关AI
|
||||
-----------------------------
|
||||
|
||||
--- 弃牌相关判定函数的表。键为技能名,值为原型如下的函数。
|
||||
---@type table<string, fun(self: SmartAI, min_num: number, num: number, include_equip?: boolean, cancelable?: boolean, pattern: string, prompt: string): integer[]?>
|
||||
fk.ai_discard = {}
|
||||
|
||||
local default_discard = function(self, min_num, num, include_equip, cancelable, pattern, prompt)
|
||||
if cancelable then return nil end
|
||||
local flag = "h"
|
||||
if include_equip then
|
||||
flag = "he"
|
||||
end
|
||||
local ret = {}
|
||||
local cards = self.player:getCardIds(flag)
|
||||
for _, cid in ipairs(cards) do
|
||||
table.insert(ret, cid)
|
||||
if #ret >= min_num then
|
||||
break
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
fk.ai_active_skill["discard_skill"] = function(self, prompt, cancelable, data)
|
||||
local ret = self:callFromTable(fk.ai_discard, not cancelable and default_discard, data.skillName,
|
||||
self, data.min_num, data.num, data.include_equip, cancelable, data.pattern, prompt)
|
||||
|
||||
if ret == nil or #ret < data.min_num then return nil end
|
||||
|
||||
return self:buildUseReply { skill = "discard_skill", subcards = ret }
|
||||
end
|
||||
|
||||
-- choose_players_skill: 选人相关AI
|
||||
-------------------------------------
|
||||
|
||||
---@class ChoosePlayersReply
|
||||
---@field cardId? integer
|
||||
---@field targets integer[]
|
||||
|
||||
--- 选人相关判定函数的表。键为技能名,值为原型如下的函数。
|
||||
---@type table<string, fun(self: SmartAI, targets: integer[], min_num: number, num: number, cancelable?: boolean): ChoosePlayersReply?>
|
||||
fk.ai_choose_players = {}
|
||||
|
||||
fk.ai_active_skill["choose_players_skill"] = function(self, prompt, cancelable, data)
|
||||
local ret = self:callFromTable(fk.ai_choose_players, nil, data.skillName,
|
||||
self, data.targets, data.min_num, data.num, cancelable)
|
||||
|
||||
if ret then
|
||||
return self:buildUseReply({ skill = "choose_players_skill", subcards = { ret.cardId } }, ret.targets)
|
||||
end
|
||||
end
|
||||
SmartAI:setSkillAI("choose_cards_skill", {
|
||||
choose_targets = function(_, ai)
|
||||
return ai:doOKButton()
|
||||
end,
|
||||
})
|
||||
|
|
|
@ -1,80 +1,186 @@
|
|||
require "packages.standard.ai.aux_skills"
|
||||
SmartAI:setSkillAI("ganglie", {
|
||||
think = function(self, ai)
|
||||
-- 刚烈的think中要处理两种情况:一是askForSkillInvoke的确定取消,二是被刚烈的人决定是否弃置2牌
|
||||
if ai:getPrompt():startsWith("#AskForDiscard") then
|
||||
-- 权衡一下弃牌与扣血的收益
|
||||
-- local cancel_val = 模拟自己扣血的收益
|
||||
-- local ok_val = 模拟弃两张最垃圾牌的收益
|
||||
-- 比如说,等于discard_skill_ai:think()的收益什么的
|
||||
-- if ok_val > cancel_val then
|
||||
-- return ai:doOKButton()
|
||||
-- else
|
||||
-- return ""
|
||||
-- end
|
||||
else
|
||||
-- 模拟一下self.skill:use 计算收益是否为正
|
||||
return false
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
SmartAI:setTriggerSkillAI("dawu", {
|
||||
correct_func = function(self, logic, event, target, player, data)
|
||||
if event ~= fk.DamageInflicted then return end
|
||||
return self.skill:triggerable(event, target, player, data)
|
||||
end,
|
||||
})
|
||||
|
||||
--[=[
|
||||
if UsingNewCore then
|
||||
require "standard.ai.aux_skills"
|
||||
else
|
||||
require "packages.standard.ai.aux_skills"
|
||||
end
|
||||
|
||||
local true_invoke = { skill_invoke = true }
|
||||
local enemy_damage_invoke = {
|
||||
skill_invoke = function(skill, ai)
|
||||
local room = ai.room
|
||||
local logic = room.logic
|
||||
|
||||
local event = logic:getCurrentEvent()
|
||||
local dmg = event.data[1]
|
||||
return ai:isEnemy(dmg.from)
|
||||
end
|
||||
}
|
||||
---@type SmartAISkillSpec
|
||||
local active_random_select_card = {
|
||||
will_use = Util.TrueFunc,
|
||||
---@param skill ViewAsSkill
|
||||
choose_cards = function(skill, ai)
|
||||
repeat
|
||||
local cids = ai:getEnabledCards()
|
||||
if #cids == 0 then return ai:okButtonEnabled() end
|
||||
ai:selectCard(cids[1], true)
|
||||
until ai:okButtonEnabled() or ai:hasEnabledTarget()
|
||||
return true
|
||||
end,
|
||||
}
|
||||
|
||||
local use_to_enemy = fk.ai_skills["__use_to_enemy"]
|
||||
local use_to_friend = fk.ai_skills["__use_to_friend"]
|
||||
local just_use = fk.ai_skills["__just_use"]
|
||||
|
||||
-- 魏国
|
||||
|
||||
fk.ai_skill_invoke["jianxiong"] = true
|
||||
SmartAI:setSkillAI("jianxiong", true_invoke)
|
||||
-- TODO: hujia
|
||||
|
||||
-- TODO: guicai 关于如何界定判定的好坏 需要向AI中单独说明
|
||||
SmartAI:setSkillAI("fankui", enemy_damage_invoke)
|
||||
|
||||
fk.ai_skill_invoke["fankui"] = function(self)
|
||||
local room = self.room
|
||||
local logic = room.logic
|
||||
SmartAI:setSkillAI("ganglie", {
|
||||
skill_invoke = function(skill, ai)
|
||||
local room = ai.room
|
||||
local logic = room.logic
|
||||
|
||||
-- 询问反馈时,处于on_cost环节,当前事件必是damage且有from
|
||||
local event = logic:getCurrentEvent()
|
||||
local dmg = event.data[1]
|
||||
return self:isEnemy(dmg.from)
|
||||
end
|
||||
local event = logic:getCurrentEvent()
|
||||
local dmg = event.data[1]
|
||||
return ai:isEnemy(dmg.from)
|
||||
end,
|
||||
choose_cards = function(skill, ai)
|
||||
local cards = ai:getEnabledCards()
|
||||
if #cards > 2 then
|
||||
for i = 1, 2 do ai:selectCard(cards[i], true) end
|
||||
return true
|
||||
end
|
||||
return false -- 直接按取消键
|
||||
end,
|
||||
-- choose_targets只有个按ok 复用默认
|
||||
})
|
||||
|
||||
fk.ai_skill_invoke["ganglie"] = fk.ai_skill_invoke["fankui"]
|
||||
SmartAI:setSkillAI("tuxi", {
|
||||
choose_targets = function(skill, ai)
|
||||
local targets = ai:getEnabledTargets()
|
||||
local i = 0
|
||||
for _, p in ipairs(targets) do
|
||||
if ai:isEnemy(p) then
|
||||
ai:selectTarget(p, true)
|
||||
i = i + 1
|
||||
if i >= 2 then return ai:doOKButton() end
|
||||
end
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
-- TODO: tuxi
|
||||
SmartAI:setSkillAI("luoyi", { skill_invoke = false })
|
||||
|
||||
fk.ai_skill_invoke["luoyi"] = function(self)
|
||||
return false
|
||||
end
|
||||
SmartAI:setSkillAI("tiandu", true_invoke)
|
||||
SmartAI:setSkillAI("yiji", {
|
||||
skill_invoke = true,
|
||||
-- ask_active = function
|
||||
})
|
||||
|
||||
fk.ai_skill_invoke["tiandu"] = true
|
||||
|
||||
-- TODO: yiji
|
||||
|
||||
fk.ai_skill_invoke["luoshen"] = true
|
||||
|
||||
-- TODO: qingguo
|
||||
SmartAI:setSkillAI("luoshen", true_invoke)
|
||||
SmartAI:setSkillAI("qingguo", active_random_select_card)
|
||||
|
||||
-- 蜀国
|
||||
-- TODO: rende
|
||||
SmartAI:setSkillAI("rende", active_random_select_card)
|
||||
SmartAI:setSkillAI("rende", use_to_friend)
|
||||
|
||||
-- TODO: jijiang
|
||||
-- TODO: wusheng
|
||||
SmartAI:setSkillAI("wusheng", active_random_select_card)
|
||||
|
||||
-- TODO: guanxing
|
||||
|
||||
-- TODO: longdan
|
||||
SmartAI:setSkillAI("longdan", active_random_select_card)
|
||||
|
||||
fk.ai_skill_invoke["tieqi"] = function(self)
|
||||
local room = self.room
|
||||
local logic = room.logic
|
||||
SmartAI:setSkillAI("tieqi", {
|
||||
skill_invoke = function(skill, ai)
|
||||
local room = ai.room
|
||||
local logic = room.logic
|
||||
|
||||
-- 询问反馈时,处于on_cost环节,当前事件必是damage且有from
|
||||
local event = logic:getCurrentEvent()
|
||||
local use = event.data[1] ---@type CardUseStruct
|
||||
return table.find(use.tos, function(t)
|
||||
return self:isEnemy(room:getPlayerById(t[1]))
|
||||
end)
|
||||
end
|
||||
-- 询问反馈时,处于on_cost环节,当前事件必是damage且有from
|
||||
local event = logic:getCurrentEvent()
|
||||
local use = event.data[1] ---@type CardUseStruct
|
||||
return table.find(use.tos, function(t)
|
||||
return ai:isEnemy(room:getPlayerById(t[1]))
|
||||
end)
|
||||
end
|
||||
})
|
||||
|
||||
fk.ai_skill_invoke["jizhi"] = true
|
||||
SmartAI:setSkillAI("jizhi", true_invoke)
|
||||
|
||||
-- 吴国
|
||||
-- TODO: zhiheng
|
||||
SmartAI:setSkillAI("zhiheng", {
|
||||
choose_cards = function(self, ai)
|
||||
for _, cid in ipairs(ai:getEnabledCards()) do
|
||||
ai:selectCard(cid, true)
|
||||
end
|
||||
return true
|
||||
end,
|
||||
})
|
||||
SmartAI:setSkillAI("zhiheng", just_use)
|
||||
|
||||
-- TODO: qixi
|
||||
SmartAI:setSkillAI("qixi", active_random_select_card)
|
||||
|
||||
fk.ai_skill_invoke["keji"] = true
|
||||
SmartAI:setSkillAI("keji", true_invoke)
|
||||
|
||||
-- TODO: kurou
|
||||
SmartAI:setSkillAI("kurou", just_use)
|
||||
|
||||
fk.ai_skill_invoke["yingzi"] = true
|
||||
SmartAI:setSkillAI("yingzi", true_invoke)
|
||||
|
||||
-- TODO: fanjian
|
||||
-- TODO: guose
|
||||
SmartAI:setSkillAI("fanjian", use_to_enemy)
|
||||
|
||||
SmartAI:setSkillAI("guose", active_random_select_card)
|
||||
-- TODO: liuli
|
||||
|
||||
fk.ai_skill_invoke["lianying"] = true
|
||||
fk.ai_skill_invoke["xiaoji"] = true
|
||||
SmartAI:setSkillAI("lianying", true_invoke)
|
||||
|
||||
-- TODO: jieyin
|
||||
SmartAI:setSkillAI("xiaoji", true_invoke)
|
||||
|
||||
SmartAI:setSkillAI("jieyin", active_random_select_card)
|
||||
SmartAI:setSkillAI("jieyin", use_to_friend)
|
||||
|
||||
-- 群雄
|
||||
-- TODO: qingnang
|
||||
SmartAI:setSkillAI("qingnang", active_random_select_card)
|
||||
SmartAI:setSkillAI("qingnang", use_to_friend)
|
||||
|
||||
-- TODO: jijiu
|
||||
-- TODO: wushuang
|
||||
SmartAI:setSkillAI("qingnang", active_random_select_card)
|
||||
|
||||
-- TODO: lijian
|
||||
fk.ai_skill_invoke["biyue"] = true
|
||||
SmartAI:setSkillAI("biyue", true_invoke)
|
||||
--]=]
|
||||
|
|
|
@ -27,7 +27,7 @@ local discardSkill = fk.CreateActiveSkill{
|
|||
return false
|
||||
end
|
||||
end
|
||||
if Fk.currentResponseReason == "game_rule" then
|
||||
if Fk.currentResponseReason == "phase_discard" then
|
||||
status_skills = Fk:currentRoom().status_skills[MaxCardsSkill] or Util.DummyTable
|
||||
for _, skill in ipairs(status_skills) do
|
||||
if skill:excludeFrom(Self, card) then
|
||||
|
|
|
@ -1,14 +1,5 @@
|
|||
-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
---@param killer ServerPlayer
|
||||
local function rewardAndPunish(killer, victim)
|
||||
if killer.dead then return end
|
||||
if victim.role == "rebel" then
|
||||
killer:drawCards(3, "kill")
|
||||
elseif victim.role == "loyalist" and killer.role == "lord" then
|
||||
killer:throwAllCards("he")
|
||||
end
|
||||
end
|
||||
|
||||
GameRule = fk.CreateTriggerSkill{
|
||||
name = "game_rule",
|
||||
|
@ -90,14 +81,11 @@ GameRule = fk.CreateTriggerSkill{
|
|||
end,
|
||||
[fk.BuryVictim] = function()
|
||||
player:bury()
|
||||
if room.tag["SkipNormalDeathProcess"] or player.rest > 0 then
|
||||
if room.tag["SkipNormalDeathProcess"] or player.rest > 0 or (data.extra_data and data.extra_data.skip_reward_punish) then
|
||||
return false
|
||||
end
|
||||
local damage = data.damage
|
||||
if damage and damage.from then
|
||||
local killer = damage.from
|
||||
rewardAndPunish(killer, player);
|
||||
end
|
||||
Fk.game_modes[room.settings.gameMode]:deathRewardAndPunish(player, damage and damage.from)
|
||||
end,
|
||||
default = function()
|
||||
print("game_rule: Event=" .. event)
|
||||
|
|
|
@ -222,7 +222,7 @@ Fk:loadTranslationTable({
|
|||
["revealMain"] = "Reveal main character %arg",
|
||||
["revealDeputy"] = "Reveal deputy character %arg",
|
||||
|
||||
["game_rule"] = "Discard",
|
||||
["game_rule"] = "GameRule",
|
||||
}, "en_US")
|
||||
|
||||
-- init
|
||||
|
|
|
@ -298,6 +298,8 @@ Fk:loadTranslationTable{
|
|||
["lijian"] = "离间",
|
||||
[":lijian"] = "出牌阶段限一次,你可以弃置一张牌并选择两名其他男性角色,后选择的角色视为对先选择的角色使用了一张不能被【无懈可击】的【决斗】。",
|
||||
["#lijian-active"] = "发动 离间,弃置一张手牌并选择两名其他男性角色,后选择的角色视为对先选择的角色使用了一张不能被【无懈可击】的【决斗】",
|
||||
["lijian_tip_1"] = "先出杀",
|
||||
["lijian_tip_2"] = "后出杀",
|
||||
["$biyue1"] = "失礼了~",
|
||||
["$biyue2"] = "羡慕吧~",
|
||||
["biyue"] = "闭月",
|
||||
|
@ -529,13 +531,23 @@ Fk:loadTranslationTable{
|
|||
["ex__choose_skill"] = "选择",
|
||||
["distribution_select_skill"] = "分配",
|
||||
["choose_players_to_move_card_in_board"] = "选择角色",
|
||||
|
||||
["AskForUseActiveSkill"] = "使用技能",
|
||||
["AskForSkillInvoke"] = "发动技能",
|
||||
["AskForUseCard"] = "使用",
|
||||
["AskForResponseCard"] = "打出",
|
||||
["AskForDiscard"] = "弃牌",
|
||||
["AskForCardChosen"] = "选牌",
|
||||
["AskForCardsChosen"] = "选牌",
|
||||
["AskForPindian"] = "拼点",
|
||||
|
||||
["reveal_skill&"] = "亮将",
|
||||
["#reveal_skill&"] = "选择一个武将亮将(点击左侧选择框展开)",
|
||||
[":reveal_skill&"] = "出牌阶段,你可明置一张有锁定技的武将。",
|
||||
["revealMain"] = "明置主将 %arg",
|
||||
["revealDeputy"] = "明置副将 %arg",
|
||||
|
||||
["game_rule"] = "弃牌阶段",
|
||||
["game_rule"] = "游戏规则",
|
||||
["replace_equip"] = "替换装备",
|
||||
["#EquipmentChoice"] = "%arg",
|
||||
["#GameRuleReplaceEquipment"] = "请选择要置入的区域",
|
||||
|
|
|
@ -1097,6 +1097,14 @@ local lijian = fk.CreateActiveSkill{
|
|||
}
|
||||
room:useCard(new_use)
|
||||
end,
|
||||
target_tip = function(self, to_select, selected, _, __, selectable, ____)
|
||||
if not selectable then return end
|
||||
if #selected == 0 or (#selected > 0 and selected[1] == to_select) then
|
||||
return "lijian_tip_1"
|
||||
else
|
||||
return "lijian_tip_2"
|
||||
end
|
||||
end,
|
||||
}
|
||||
local biyue = fk.CreateTriggerSkill{
|
||||
name = "biyue",
|
||||
|
@ -1215,30 +1223,20 @@ local role_getlogic = function()
|
|||
end
|
||||
|
||||
local nonlord = room:getOtherPlayers(lord, true)
|
||||
local req = Request:new(nonlord, "AskForGeneral")
|
||||
local generals = table.random(room.general_pile, #nonlord * generalNum)
|
||||
for i, p in ipairs(nonlord) do
|
||||
local arg = table.slice(generals, (i - 1) * generalNum + 1, i * generalNum + 1)
|
||||
p.request_data = json.encode{ arg, n }
|
||||
p.default_reply = table.random(arg, n)
|
||||
req:setData(p, { arg, n })
|
||||
req:setDefaultReply(p, table.random(arg, n))
|
||||
end
|
||||
|
||||
room:notifyMoveFocus(nonlord, "AskForGeneral")
|
||||
room:doBroadcastRequest("AskForGeneral", nonlord)
|
||||
|
||||
for _, p in ipairs(nonlord) do
|
||||
local general, deputy
|
||||
if p.general == "" and p.reply_ready then
|
||||
local general_ret = json.decode(p.client_reply)
|
||||
general = general_ret[1]
|
||||
deputy = general_ret[2]
|
||||
else
|
||||
general = p.default_reply[1]
|
||||
deputy = p.default_reply[2]
|
||||
end
|
||||
local result = req:getResult(p)
|
||||
local general, deputy = result[1], result[2]
|
||||
room:findGeneral(general)
|
||||
room:findGeneral(deputy)
|
||||
room:prepareGeneral(p, general, deputy)
|
||||
p.default_reply = ""
|
||||
end
|
||||
|
||||
room:askForChooseKingdom(nonlord)
|
||||
|
@ -1252,6 +1250,7 @@ local role_mode = fk.CreateGameMode{
|
|||
minPlayer = 2,
|
||||
maxPlayer = 8,
|
||||
logic = role_getlogic,
|
||||
main_mode = "role_mode",
|
||||
is_counted = function(self, room)
|
||||
return #room.players >= 5
|
||||
end,
|
||||
|
|
|
@ -1,87 +1,71 @@
|
|||
-- TODO: 合法性的方便函数
|
||||
-- TODO: 关于如何选择多个目标
|
||||
-- TODO: 关于装备牌
|
||||
SmartAI:setCardSkillAI("slash_skill", {
|
||||
estimated_benefit = 120,
|
||||
|
||||
-- 基本牌:杀,闪,桃
|
||||
on_effect = function(self, logic, effect)
|
||||
self.skill:onEffect(logic, effect)
|
||||
end,
|
||||
})
|
||||
|
||||
---@param from ServerPlayer
|
||||
---@param to ServerPlayer
|
||||
---@param card Card
|
||||
local function tgtValidator(from, to, card)
|
||||
return not from:prohibitUse(card) and
|
||||
not from:isProhibited(to, card) and
|
||||
true -- feasible
|
||||
end
|
||||
SmartAI:setTriggerSkillAI("#nioh_shield_skill", {
|
||||
correct_func = function(self, logic, event, target, player, data)
|
||||
return self.skill:triggerable(event, target, player, data)
|
||||
end,
|
||||
})
|
||||
|
||||
local function justUse(self, card_name, extra_data)
|
||||
local slashes = self:getCards(card_name, "use", extra_data)
|
||||
if #slashes == 0 then return nil end
|
||||
--[=====[
|
||||
local just_use = {
|
||||
name = "__just_use",
|
||||
will_use = Util.TrueFunc,
|
||||
choose_targets = function(skill, ai, card)
|
||||
return ai:doOKButton()
|
||||
end,
|
||||
}
|
||||
|
||||
return self:buildUseReply(slashes[1].id)
|
||||
end
|
||||
local use_to_friend = {
|
||||
name = "__use_to_friend",
|
||||
will_use = Util.TrueFunc,
|
||||
choose_targets = function(skill, ai, card)
|
||||
local targets = ai:getEnabledTargets()
|
||||
for _, p in ipairs(targets) do
|
||||
if ai:isFriend(p) then
|
||||
ai:selectTarget(p, true)
|
||||
break
|
||||
end
|
||||
end
|
||||
return ai:doOKButton()
|
||||
end,
|
||||
}
|
||||
|
||||
---@param self SmartAI
|
||||
---@param card_name string
|
||||
local function useToEnemy(self, card_name, extra_data)
|
||||
local slashes = self:getCards(card_name, "use", extra_data)
|
||||
if #slashes == 0 then return nil end
|
||||
local use_to_enemy = {
|
||||
name = "__use_to_enemy",
|
||||
will_use = Util.TrueFunc,
|
||||
choose_targets = function(skill, ai, card)
|
||||
local targets = ai:getEnabledTargets()
|
||||
for _, p in ipairs(targets) do
|
||||
if ai:isEnemy(p) then
|
||||
ai:selectTarget(p, true)
|
||||
break
|
||||
end
|
||||
end
|
||||
return ai:doOKButton()
|
||||
end,
|
||||
}
|
||||
|
||||
-- TODO: 目标合法性
|
||||
local targets = {}
|
||||
if self.enemies[1] then
|
||||
table.insert(targets, self.enemies[1].id)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
|
||||
return self:buildUseReply(slashes[1].id, targets)
|
||||
end
|
||||
|
||||
fk.ai_use_card["slash"] = function(self, pattern, prompt, cancelable, extra_data)
|
||||
return useToEnemy(self, "slash", extra_data)
|
||||
end
|
||||
|
||||
fk.ai_use_card["jink"] = function(self, pattern, prompt, cancelable, extra_data)
|
||||
return justUse(self, "jink", extra_data)
|
||||
end
|
||||
|
||||
fk.ai_use_card["peach"] = function(self, _, _, _, extra_data)
|
||||
local cards = self:getCards("peach", "use", extra_data)
|
||||
if #cards == 0 then return nil end
|
||||
|
||||
return self:buildUseReply(cards[1].id)
|
||||
end
|
||||
|
||||
-- 自救见军争卡牌AI
|
||||
fk.ai_use_card["#AskForPeaches"] = function(self)
|
||||
local room = self.room
|
||||
local deathEvent = room.logic:getCurrentEvent()
|
||||
local data = deathEvent.data[1] ---@type DyingStruct
|
||||
|
||||
-- TODO: 关于救不回来、神关羽之类的更复杂逻辑
|
||||
-- TODO: 这些逻辑感觉不能写死在此函数里面,得想出更加多样的办法
|
||||
if self:isFriend(room:getPlayerById(data.who)) then
|
||||
return fk.ai_use_card["peach"](self)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
fk.ai_use_card["dismantlement"] = function(self, pattern, prompt, cancelable, extra_data)
|
||||
return useToEnemy(self, "dismantlement", extra_data)
|
||||
end
|
||||
|
||||
fk.ai_use_card["snatch"] = function(self, pattern, prompt, cancelable, extra_data)
|
||||
return useToEnemy(self, "snatch", extra_data)
|
||||
end
|
||||
|
||||
fk.ai_use_card["duel"] = function(self, pattern, prompt, cancelable, extra_data)
|
||||
return useToEnemy(self, "duel", extra_data)
|
||||
end
|
||||
|
||||
fk.ai_use_card["ex_nihilo"] = function(self, pattern, prompt, cancelable, extra_data)
|
||||
return justUse(self, "ex_nihilo", extra_data)
|
||||
end
|
||||
|
||||
fk.ai_use_card["indulgence"] = function(self, pattern, prompt, cancelable, extra_data)
|
||||
return useToEnemy(self, "indulgence", extra_data)
|
||||
end
|
||||
SmartAI:setSkillAI("__just_use", just_use)
|
||||
SmartAI:setSkillAI("__use_to_enemy", use_to_enemy)
|
||||
SmartAI:setSkillAI("__use_to_friend", use_to_friend)
|
||||
SmartAI:setSkillAI("slash_skill", use_to_enemy)
|
||||
SmartAI:setSkillAI("dismantlement_skill", use_to_enemy)
|
||||
SmartAI:setSkillAI("snatch_skill", use_to_enemy)
|
||||
SmartAI:setSkillAI("duel_skill", use_to_enemy)
|
||||
SmartAI:setSkillAI("indulgence_skill", use_to_enemy)
|
||||
SmartAI:setSkillAI("jink_skill", just_use)
|
||||
SmartAI:setSkillAI("peach_skill", just_use)
|
||||
SmartAI:setSkillAI("ex_nihilo_skill", just_use)
|
||||
SmartAI:setSkillAI("savage_assault_skill", just_use)
|
||||
SmartAI:setSkillAI("archery_attack_skill", just_use)
|
||||
SmartAI:setSkillAI("god_salvation_skill", just_use)
|
||||
SmartAI:setSkillAI("amazing_grace_skill", just_use)
|
||||
SmartAI:setSkillAI("lightning_skill", just_use)
|
||||
SmartAI:setSkillAI("default_equip_skill", just_use)
|
||||
--]=====]
|
||||
|
|
|
@ -26,10 +26,12 @@ Fk:loadTranslationTable{
|
|||
["basic_char"] = "基",
|
||||
["trick_char"] = "锦",
|
||||
["equip_char"] = "装",
|
||||
["non_basic_char"] = "非基",
|
||||
|
||||
["basic"] = "基本牌",
|
||||
["trick"] = "锦囊牌",
|
||||
["equip"] = "装备牌",
|
||||
["non_basic"] = "非基本牌",
|
||||
["weapon"] = "武器牌",
|
||||
["armor"] = "防具牌",
|
||||
["defensive_horse"] = "防御坐骑牌",
|
||||
|
|
|
@ -462,7 +462,6 @@ extension:addCards({
|
|||
local nullificationSkill = fk.CreateActiveSkill{
|
||||
name = "nullification_skill",
|
||||
can_use = Util.FalseFunc,
|
||||
on_use = function() RoomInstance:delay(1200) end,
|
||||
on_effect = function(self, room, effect)
|
||||
if effect.responseToEvent then
|
||||
effect.responseToEvent.isCancellOut = true
|
||||
|
@ -713,7 +712,7 @@ local lightningSkill = fk.CreateActiveSkill{
|
|||
}
|
||||
|
||||
room:moveCards{
|
||||
ids = Card:getIdList(effect.card),
|
||||
ids = room:getSubcardsByRule(effect.card, { Card.Processing }),
|
||||
toArea = Card.DiscardPile,
|
||||
moveReason = fk.ReasonUse
|
||||
}
|
||||
|
|
|
@ -46,9 +46,19 @@ Client::~Client() {
|
|||
}
|
||||
|
||||
void Client::connectToHost(const QString &server, ushort port) {
|
||||
start_connent_timestamp = QDateTime::currentMSecsSinceEpoch();
|
||||
router->getSocket()->connectToHost(server, port);
|
||||
}
|
||||
|
||||
void Client::setupServerLag(qint64 server_time) {
|
||||
auto now = QDateTime::currentMSecsSinceEpoch();
|
||||
auto ping = now - start_connent_timestamp;
|
||||
auto lag = now - server_time;
|
||||
server_lag = lag - ping / 2;
|
||||
}
|
||||
|
||||
qint64 Client::getServerLag() const { return server_lag; }
|
||||
|
||||
void Client::replyToServer(const QString &command, const QString &jsonData) {
|
||||
int type = Router::TYPE_REPLY | Router::SRC_CLIENT | Router::DEST_SERVER;
|
||||
router->reply(type, command, jsonData);
|
||||
|
|
|
@ -17,6 +17,8 @@ public:
|
|||
~Client();
|
||||
|
||||
void connectToHost(const QString &server, ushort port);
|
||||
void setupServerLag(qint64 server_time);
|
||||
qint64 getServerLag() const;
|
||||
|
||||
Q_INVOKABLE void replyToServer(const QString &command, const QString &jsonData);
|
||||
Q_INVOKABLE void notifyServer(const QString &command, const QString &jsonData);
|
||||
|
@ -51,6 +53,8 @@ private:
|
|||
Router *router;
|
||||
QMap<int, ClientPlayer *> players;
|
||||
ClientPlayer *self;
|
||||
qint64 start_connent_timestamp; // 连接时的时间戳 单位毫秒
|
||||
qint64 server_lag = 0; // 与服务器时差,单位毫秒,正数表示自己快了 负数表示慢了
|
||||
|
||||
lua_State *L;
|
||||
QFileSystemWatcher fsWatcher;
|
||||
|
|
|
@ -212,6 +212,7 @@ void Server::setupPlayer(ServerPlayer *player, bool all_info) {
|
|||
arr << player->getId();
|
||||
arr << player->getScreenName();
|
||||
arr << player->getAvatar();
|
||||
arr << QDateTime::currentMSecsSinceEpoch();
|
||||
player->doNotify("Setup", JsonArray2Bytes(arr));
|
||||
|
||||
if (all_info) {
|
||||
|
|
|
@ -18,6 +18,8 @@ extern QmlBackend *Backend;
|
|||
%nodefaultdtor Client;
|
||||
class Client : public QObject {
|
||||
public:
|
||||
void setupServerLag(long long server_time);
|
||||
|
||||
void replyToServer(const QString &command, const QString &json_data);
|
||||
void notifyServer(const QString &command, const QString &json_data);
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ public:
|
|||
QVariant ret;
|
||||
if (high < 0) {
|
||||
if (low < 1) {
|
||||
ret.setValue($self->bounded(0, 100001) / 100000);
|
||||
ret.setValue(qreal($self->bounded(0, 100000001)) / 100000000);
|
||||
} else {
|
||||
ret.setValue($self->bounded(1, low + 1));
|
||||
}
|
||||
|
|
|
@ -289,16 +289,21 @@ QVariant QmlBackend::callLuaFunction(const QString &func_name,
|
|||
if (!ClientInstance) return QVariantMap();
|
||||
|
||||
lua_State *L = ClientInstance->getLuaState();
|
||||
|
||||
lua_getglobal(L, "debug");
|
||||
lua_getfield(L, -1, "traceback");
|
||||
lua_replace(L, -2);
|
||||
|
||||
lua_getglobal(L, func_name.toLatin1().data());
|
||||
|
||||
foreach (QVariant v, params) {
|
||||
pushLuaValue(L, v);
|
||||
}
|
||||
|
||||
int err = lua_pcall(L, params.length(), 1, 0);
|
||||
int err = lua_pcall(L, params.length(), 1, -params.length() - 2);
|
||||
if (err) {
|
||||
qCritical() << lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
lua_pop(L, 2);
|
||||
return QVariant();
|
||||
}
|
||||
auto result = readLuaValue(L);
|
||||
|
@ -649,7 +654,10 @@ QJsonObject QmlBackend::getRequestData() const {
|
|||
auto router = ClientInstance->getRouter();
|
||||
obj["id"] = router->getRequestId();
|
||||
obj["timeout"] = router->getTimeout();
|
||||
obj["timestamp"] = router->getRequestTimestamp();
|
||||
auto timestamp = router->getRequestTimestamp();
|
||||
// 因为timestamp是服务器发来的时间,如果自己比服务器的时钟快的话,那么就得加上这个差值才行
|
||||
timestamp += ClientInstance->getServerLag();
|
||||
obj["timestamp"] = timestamp;
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user