Changelog: v0.4.23

This commit is contained in:
notify 2024-11-09 19:27:41 +08:00
parent f8b67531c0
commit a09a973064
75 changed files with 2300 additions and 1697 deletions

View File

@ -1,6 +1,6 @@
# ChangeLog # ChangeLog
## v0.4.21 & v0.4.22 ## v0.4.21 & v0.4.22 & v0.4.23
- 修复了确认键亮起时取消键不可用的bug - 修复了确认键亮起时取消键不可用的bug
- lua端的ob属性根本没同步同步一下 - lua端的ob属性根本没同步同步一下

View File

@ -6,7 +6,7 @@
cmake_minimum_required(VERSION 3.22) 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}\") add_definitions(-DFK_VERSION=\"${CMAKE_PROJECT_VERSION}\")
find_package(Qt6 REQUIRED COMPONENTS find_package(Qt6 REQUIRED COMPONENTS

View File

@ -153,6 +153,10 @@ Flickable {
selectByMouse: false selectByMouse: false
wrapMode: TextEdit.WordWrap wrapMode: TextEdit.WordWrap
textFormat: TextEdit.RichText textFormat: TextEdit.RichText
property var savedtext: []
function clearSavedText() {
savedtext = [];
}
onLinkActivated: (link) => { onLinkActivated: (link) => {
if (link === "back") { if (link === "back") {
text = savedtext.pop(); text = savedtext.pop();
@ -180,6 +184,7 @@ Flickable {
screenName.text = ""; screenName.text = "";
playerGameData.text = ""; playerGameData.text = "";
skillDesc.text = ""; skillDesc.text = "";
skillDesc.clearSavedText();
const id = extra_data.photo.playerid; const id = extra_data.photo.playerid;
if (id === 0) return; if (id === 0) return;

View File

@ -5,7 +5,7 @@ import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import Fk import Fk
import Fk.RoomElement import Fk.RoomElement
import "RoomLogic.js" as RoomLogic // import "RoomLogic.js" as RoomLogic
Item { Item {
id: root id: root
@ -390,12 +390,28 @@ Item {
} }
onClicked: { onClicked: {
callbacks["LogEvent"]({ const skill = name;
type: "PlaySkillSound", const general = specific ? detailGeneralCard.name : null;
name: name, let extension;
general: specific ? detailGeneralCard.name : null, // let path;
i: idx, 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: { onPressAndHold: {

View File

@ -341,6 +341,7 @@ Item {
} }
lcall("FinishRequestUI"); lcall("FinishRequestUI");
applyChange({});
} }
} }
}, },
@ -1338,7 +1339,12 @@ Item {
photo.state = pdata.state; photo.state = pdata.state;
photo.selectable = pdata.enabled; photo.selectable = pdata.enabled;
photo.selected = pdata.selected; 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"]; const buttons = uiUpdate["Button"];
if (buttons) { if (buttons) {
okCancel.visible = true; okCancel.visible = true;

View File

@ -768,26 +768,17 @@ callbacks["MoveFocus"] = (data) => {
cancelAllFocus(); cancelAllFocus();
const focuses = data[0]; const focuses = data[0];
const command = data[1]; const command = data[1];
const timeout = data[2] ?? (config.roomTimeout * 1000);
let item, model; let item, model;
for (let i = 0; i < playerNum; i++) { for (let i = 0; i < playerNum; i++) {
model = photoModel.get(i); model = photoModel.get(i);
if (focuses.indexOf(model.id) != -1) { if (focuses.indexOf(model.id) != -1) {
item = photos.itemAt(i); item = photos.itemAt(i);
item.progressBar.duration = timeout;
item.progressBar.visible = true; item.progressBar.visible = true;
item.progressTip = luatr(command) item.progressTip = luatr(command)
+ luatr(" thinking..."); + 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; roomScene.okCancel.visible = true;
} }
callbacks["WaitForNullification"] = () => {
roomScene.state = "notactive";
}
callbacks["SetPlayerMark"] = (data) => { callbacks["SetPlayerMark"] = (data) => {
const player = getPhoto(data[0]); const player = getPhoto(data[0]);
const mark = data[1]; const mark = data[1];
@ -1469,6 +1456,10 @@ callbacks["UpdateMiniGame"] = (data) => {
} }
} }
callbacks["EmptyRequest"] = (data) => {
roomScene.activate();
}
callbacks["UpdateLimitSkill"] = (data) => { callbacks["UpdateLimitSkill"] = (data) => {
const id = data[0]; const id = data[0];
const skill = data[1]; const skill = data[1];

View File

@ -162,6 +162,13 @@ Item {
break; 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;
}
}
} }
} }

View File

@ -627,13 +627,14 @@ Item {
anchors.bottomMargin: -4 anchors.bottomMargin: -4
from: 0.0 from: 0.0
to: 100.0 to: 100.0
property int duration: config.roomTimeout * 1000
visible: false visible: false
NumberAnimation on value { NumberAnimation on value {
running: progressBar.visible running: progressBar.visible
from: 100.0 from: 100.0
to: 0.0 to: 0.0
duration: config.roomTimeout * 1000 duration: progressBar.duration
onFinished: { onFinished: {
progressBar.visible = false; progressBar.visible = false;
@ -665,6 +666,59 @@ Item {
visible: root.state === "candidate" && selectable 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 { InvisibleCardArea {
id: handcardAreaItem id: handcardAreaItem
anchors.centerIn: parent 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 { Rectangle {
color: "#CC2E2C27" color: "#CC2E2C27"
radius: 6 radius: 6

View File

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

View File

@ -213,18 +213,21 @@ fk.client_callback["SetCardFootnote"] = function(data)
ClientInstance:setCardNote(data[1], data[2]); ClientInstance:setCardNote(data[1], data[2]);
end end
local function setup(id, name, avatar) local function setup(id, name, avatar, msec)
local self = fk.Self local self = fk.Self
self:setId(id) self:setId(id)
self:setScreenName(name) self:setScreenName(name)
self:setAvatar(avatar) self:setAvatar(avatar)
Self = ClientPlayer:new(fk.Self) Self = ClientPlayer:new(fk.Self)
if msec then
fk.ClientInstance:setupServerLag(msec)
end
end end
fk.client_callback["Setup"] = function(data) fk.client_callback["Setup"] = function(data)
-- jsonData: [ int id, string screenName, string avatar ] -- jsonData: [ int id, string screenName, string avatar ]
local id, name, avatar = data[1], data[2], data[3] local id, name, avatar, msec = data[1], data[2], data[3], data[4]
setup(id, name, avatar) setup(id, name, avatar, msec)
end end
fk.client_callback["EnterRoom"] = function(_data) fk.client_callback["EnterRoom"] = function(_data)
@ -243,7 +246,7 @@ fk.client_callback["EnterRoom"] = function(_data)
local data = _data[3] local data = _data[3]
ClientInstance.enter_room_data = json.encode(_data); ClientInstance.enter_room_data = json.encode(_data);
ClientInstance.room_settings = data ClientInstance.settings = data
table.insertTableIfNeed( table.insertTableIfNeed(
data.disabledPack, data.disabledPack,
Fk.game_mode_disabled[data.gameMode] Fk.game_mode_disabled[data.gameMode]
@ -857,13 +860,9 @@ fk.client_callback["AskForUseActiveSkill"] = function(data)
local skill = Fk.skills[data[1]] local skill = Fk.skills[data[1]]
local extra_data = data[4] local extra_data = data[4]
skill._extra_data = extra_data skill._extra_data = extra_data
Fk.currentResponseReason = extra_data.skillName Fk.currentResponseReason = extra_data.skillName
local h = Fk.request_handlers["AskForUseActiveSkill"]:new(Self)
h.skill_name = data[1] local h = Fk.request_handlers["AskForUseActiveSkill"]:new(Self, data)
h.prompt = data[2]
h.cancelable = data[3]
h.extra_data = data[4]
h.change = {} h.change = {}
h:setup() h:setup()
h.scene:notifyUI() h.scene:notifyUI()
@ -873,12 +872,7 @@ end
fk.client_callback["AskForUseCard"] = function(data) fk.client_callback["AskForUseCard"] = function(data)
-- jsonData: card, pattern, prompt, cancelable, {} -- jsonData: card, pattern, prompt, cancelable, {}
Fk.currentResponsePattern = data[2] Fk.currentResponsePattern = data[2]
local h = Fk.request_handlers["AskForUseCard"]:new(Self) local h = Fk.request_handlers["AskForUseCard"]:new(Self, data)
-- h.skill_name = data[1] (skill_name是给选中的视为技用的)
h.pattern = data[2]
h.prompt = data[3]
h.cancelable = data[4]
h.extra_data = data[5]
h.change = {} h.change = {}
h:setup() h:setup()
h.scene:notifyUI() h.scene:notifyUI()
@ -888,12 +882,7 @@ end
fk.client_callback["AskForResponseCard"] = function(data) fk.client_callback["AskForResponseCard"] = function(data)
-- jsonData: card, pattern, prompt, cancelable, {} -- jsonData: card, pattern, prompt, cancelable, {}
Fk.currentResponsePattern = data[2] Fk.currentResponsePattern = data[2]
local h = Fk.request_handlers["AskForResponseCard"]:new(Self) local h = Fk.request_handlers["AskForResponseCard"]:new(Self, data)
-- h.skill_name = data[1] (skill_name是给选中的视为技用的)
h.pattern = data[2]
h.prompt = data[3]
h.cancelable = data[4]
h.extra_data = data[5]
h.change = {} h.change = {}
h:setup() h:setup()
h.scene:notifyUI() h.scene:notifyUI()
@ -1100,7 +1089,7 @@ fk.client_callback["GameOver"] = function(jsonData)
c.record[2] = table.concat({ c.record[2] = table.concat({
c.record[2], c.record[2],
Self.player:getScreenName(), Self.player:getScreenName(),
c.room_settings.gameMode, c.settings.gameMode,
Self.general, Self.general,
Self.role, Self.role,
jsonData, jsonData,
@ -1118,7 +1107,7 @@ fk.client_callback["EnterLobby"] = function(jsonData)
c.record[2] = table.concat({ c.record[2] = table.concat({
c.record[2], c.record[2],
Self.player:getScreenName(), Self.player:getScreenName(),
c.room_settings.gameMode, c.settings.gameMode,
Self.general, Self.general,
Self.role, Self.role,
"", "",

View File

@ -335,49 +335,6 @@ function CanUseCardToTarget(card, to_select, selected, extra_data_str)
return ret return ret
end end
---@param card string | integer
---@param to_select integer @ id of the target
---@param selected integer[] @ ids of selected targets
---@param selectable bool
---@param extra_data_str string @ extra data
function GetUseCardTargetTip(card, to_select, selected, selectable, extra_data_str)
local extra_data = extra_data_str == "" and nil or json.decode(extra_data_str)
local c ---@type Card
local selected_cards
if type(card) == "number" then
c = Fk:getCardById(card)
selected_cards = {card}
else
local t = json.decode(card)
return ActiveTargetTip(t.skill, to_select, selected, t.subcards, selectable, extra_data)
end
local ret
local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or Util.DummyTable
for _, skill in ipairs(status_skills) do
ret = ret or {}
if #ret > 4 then
return ret
end
local tip = skill:getTargetTip(Self, to_select, selected, selected_cards, c, selectable, extra_data)
if type(tip) == "string" then
table.insert(ret, { content = ret, type = "normal" })
elseif type(tip) == "table" then
table.insertTable(ret, tip)
end
end
ret = ret or {}
local tip = c.skill:targetTip(to_select, selected, selected_cards, c, selectable, extra_data)
if type(tip) == "string" then
table.insert(ret, { content = ret, type = "normal" })
elseif type(tip) == "table" then
table.insertTable(ret, tip)
end
return ret
end
---@param card string | integer ---@param card string | integer
---@param to_select integer @ id of a card not selected ---@param to_select integer @ id of a card not selected
---@param selected_targets integer[] @ ids of selected players ---@param selected_targets integer[] @ ids of selected players
@ -457,10 +414,25 @@ function GetSkillData(skill_name)
end end
function GetSkillStatus(skill_name) function GetSkillStatus(skill_name)
local player = Self
local skill = Fk.skills[skill_name] local skill = Fk.skills[skill_name]
local locked = not skill:isEffectable(Self) local locked = not skill:isEffectable(player)
if not locked and type(Self:getMark(MarkEnum.InvalidSkills)) == "table" and table.contains(Self:getMark(MarkEnum.InvalidSkills), skill_name) then if not locked then
locked = true 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 end
return { return {
locked = locked, ---@type boolean locked = locked, ---@type boolean
@ -543,46 +515,6 @@ function ActiveTargetFilter(skill_name, to_select, selected, selected_cards, ext
return ret return ret
end end
function ActiveTargetTip(skill_name, to_select, selected, selected_cards, selectable, extra_data)
local skill = Fk.skills[skill_name]
local ret
if skill then
if skill:isInstanceOf(ActiveSkill) then
ret = skill:targetTip(to_select, selected, selected_cards, nil, selectable)
if type(ret) == "string" then
ret = { { content = ret, type = "normal" } }
end
elseif skill:isInstanceOf(ViewAsSkill) then
local card = skill:viewAs(selected_cards)
if card then
local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or Util.DummyTable
for _, skill in ipairs(status_skills) do
ret = ret or {}
if #ret > 4 then
return ret
end
local tip = skill:getTargetTip(Self, to_select, selected, selected_cards, card, selectable, extra_data)
if type(tip) == "string" then
table.insert(ret, { content = ret, type = "normal" })
elseif type(tip) == "table" then
table.insertTable(ret, tip)
end
end
ret = ret or {}
local tip = card.skill:targetTip(to_select, selected, selected_cards, card, selectable, extra_data)
if type(tip) == "string" then
table.insert(ret, { content = ret, type = "normal" })
elseif type(tip) == "table" then
table.insertTable(ret, tip)
end
end
end
end
return ret
end
function ActiveFeasible(skill_name, selected, selected_cards) function ActiveFeasible(skill_name, selected, selected_cards)
local skill = Fk.skills[skill_name] local skill = Fk.skills[skill_name]
local ret = false local ret = false
@ -746,7 +678,7 @@ end
function ResetClientLua() function ResetClientLua()
local _data = ClientInstance.enter_room_data; local _data = ClientInstance.enter_room_data;
local data = ClientInstance.room_settings local data = ClientInstance.settings
Self = ClientPlayer:new(fk.Self) Self = ClientPlayer:new(fk.Self)
ClientInstance = Client:new() -- clear old client data ClientInstance = Client:new() -- clear old client data
ClientInstance.players = {Self} ClientInstance.players = {Self}
@ -754,7 +686,7 @@ function ResetClientLua()
ClientInstance.discard_pile = {} ClientInstance.discard_pile = {}
ClientInstance.enter_room_data = _data; ClientInstance.enter_room_data = _data;
ClientInstance.room_settings = data ClientInstance.settings = data
ClientInstance.disabled_packs = data.disabledPack ClientInstance.disabled_packs = data.disabledPack
ClientInstance.disabled_generals = data.disabledGenerals ClientInstance.disabled_generals = data.disabledGenerals
@ -766,7 +698,7 @@ function ResetAddPlayer(j)
end end
function GetRoomConfig() function GetRoomConfig()
return ClientInstance.room_settings return ClientInstance.settings
end end
function GetPlayerGameData(pid) function GetPlayerGameData(pid)
@ -813,7 +745,7 @@ function SetReplayingShowCards(o)
end end
function CheckSurrenderAvailable(playedTime) function CheckSurrenderAvailable(playedTime)
local curMode = ClientInstance.room_settings.gameMode local curMode = ClientInstance.settings.gameMode
return Fk.game_modes[curMode]:surrenderFunc(playedTime) return Fk.game_modes[curMode]:surrenderFunc(playedTime)
end end
@ -822,17 +754,38 @@ function SaveRecord()
c.client:saveRecord(json.encode(c.record), c.record[2]) c.client:saveRecord(json.encode(c.record), c.record[2])
end end
function GetCardProhibitReason(cid, method, pattern) function GetCardProhibitReason(cid)
local card = Fk:getCardById(cid) local card = Fk:getCardById(cid)
if not card then return "" end 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.skill:canUse(Self, card) then return "" end
if method ~= "play" and not card:matchPattern(pattern) then return "" end if method ~= "play" and not card:matchPattern(pattern) then return "" end
if method == "play" then method = "use" 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 status_skills = Fk:currentRoom().status_skills[ProhibitSkill] or Util.DummyTable
local s local s
for _, skill in ipairs(status_skills) do 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 if fn(skill, Self, card) then
s = skill s = skill
break break
@ -844,14 +797,70 @@ function GetCardProhibitReason(cid, method, pattern)
local skillName = s.name local skillName = s.name
local ret = Fk:translate(skillName) local ret = Fk:translate(skillName)
if ret ~= skillName then if ret ~= skillName then
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 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 else
return ret return ret
end end
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) function CanSortHandcards(pid)
return ClientInstance:getPlayerById(pid):getMark(MarkEnum.SortProhibited) == 0 return ClientInstance:getPlayerById(pid):getMark(MarkEnum.SortProhibited) == 0
end end
@ -940,7 +949,10 @@ function RevertSelection()
h.scene:notifyUI() h.scene:notifyUI()
end end
local requestUIUpdating = false
function UpdateRequestUI(elemType, id, action, data) function UpdateRequestUI(elemType, id, action, data)
if requestUIUpdating then return end
requestUIUpdating = true
local h = ClientInstance.current_request_handler local h = ClientInstance.current_request_handler
h.change = {} h.change = {}
local finish = h:update(elemType, id, action, data) local finish = h:update(elemType, id, action, data)
@ -949,12 +961,14 @@ function UpdateRequestUI(elemType, id, action, data)
else else
h:_finish() h:_finish()
end end
requestUIUpdating = false
end end
function FinishRequestUI() function FinishRequestUI()
local h = ClientInstance.current_request_handler local h = ClientInstance.current_request_handler
if h then if h then
h:_finish() h:_finish()
ClientInstance.current_request_handler = nil
end end
end end

View File

@ -236,7 +236,7 @@ Fk:loadTranslationTable({
["#replaceEquip"] = "Please Choose a Equip Card to be replaced", ["#replaceEquip"] = "Please Choose a Equip Card to be replaced",
["#askForPindian"] = "%arg: please choose a hand card for point fight", ["#askForPindian"] = "%arg: please choose a hand card for point fight",
["#StartPindianReason"] = "%from started point fight (%arg)", ["#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", ["#ShowPindianResult"] = "%from %arg the point fight between %from and %to",
["pindianwin"] = "won", ["pindianwin"] = "won",
["pindiannotwin"] = "lost", ["pindiannotwin"] = "lost",

View File

@ -205,6 +205,7 @@ FreeKill使用的是libgit2的C API与此同时使用Git完成拓展包的下
["Cancel"] = "取消", ["Cancel"] = "取消",
["End"] = "结束", ["End"] = "结束",
-- ["Quit"] = "退出", -- ["Quit"] = "退出",
["All"] = "全部",
["BanGeneral"] = "禁将", ["BanGeneral"] = "禁将",
["ResumeGeneral"] = "解禁", ["ResumeGeneral"] = "解禁",
["Enable"] = "启用", ["Enable"] = "启用",
@ -293,7 +294,7 @@ FreeKill使用的是libgit2的C API与此同时使用Git完成拓展包的下
["#replaceEquip"] = "选择一张装备牌替换之", ["#replaceEquip"] = "选择一张装备牌替换之",
["#askForPindian"] = "%arg请选择一张手牌作为拼点牌", ["#askForPindian"] = "%arg请选择一张手牌作为拼点牌",
["#StartPindianReason"] = "%from 由于 %arg 而发起拼点", ["#StartPindianReason"] = "%from 由于 %arg 而发起拼点",
["#ShowPindianCard"] = "%from 的拼点牌是 %card", ["#ShowPindianCard"] = "%from 的拼点牌是 %arg",
["#ShowPindianResult"] = "%from 在 %from 和 %to 之间的拼点中 %arg", ["#ShowPindianResult"] = "%from 在 %from 和 %to 之间的拼点中 %arg",
["pindianwin"] = "", ["pindianwin"] = "",
["pindiannotwin"] = "没赢", ["pindiannotwin"] = "没赢",

View File

@ -23,7 +23,7 @@ function Traceback()
end end
local msgh = function(err) local msgh = function(err)
fk.qCritical(err .. "\n" .. debug.traceback(nil, 2)) fk.qCritical(tostring(err) .. "\n" .. debug.traceback(nil, 2))
end end
function Pcall(f, ...) function Pcall(f, ...)
@ -36,3 +36,10 @@ end
function p(v) print(inspect(v)) end function p(v) print(inspect(v)) end
function pt(t) for k, v in pairs(t) do print(k, v) end 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

View File

@ -19,10 +19,12 @@
---@field public same_generals table<string, string[]> @ 所有同名武将组合 ---@field public same_generals table<string, string[]> @ 所有同名武将组合
---@field public lords string[] @ 所有主公武将,用于常备主公 ---@field public lords string[] @ 所有主公武将,用于常备主公
---@field public all_card_types table<string, Card> @ 所有的卡牌类型以及一张样板牌 ---@field public all_card_types table<string, Card> @ 所有的卡牌类型以及一张样板牌
---@field public all_card_names string[] @ 有序的所有的卡牌牌名,顺序:基本牌(杀置顶),普通锦囊,延时锦囊,按副类别排序的装备
---@field public cards Card[] @ 所有卡牌 ---@field public cards Card[] @ 所有卡牌
---@field public translations table<string, table<string, string>> @ 翻译表 ---@field public translations table<string, table<string, string>> @ 翻译表
---@field public game_modes table<string, GameMode> @ 所有游戏模式 ---@field public game_modes table<string, GameMode> @ 所有游戏模式
---@field public game_mode_disabled table<string, string[]> @ 游戏模式禁用的包 ---@field public game_mode_disabled table<string, string[]> @ 游戏模式禁用的包
---@field public main_mode_list table<string, string[]> @ 主模式检索表
---@field public currentResponsePattern string @ 要求用牌的种类(如要求用特定花色的桃···) ---@field public currentResponsePattern string @ 要求用牌的种类(如要求用特定花色的桃···)
---@field public currentResponseReason string @ 要求用牌的原因(如濒死,被特定牌指定,使用特定技能···) ---@field public currentResponseReason string @ 要求用牌的原因(如濒死,被特定牌指定,使用特定技能···)
---@field public filtered_cards table<integer, Card> @ 被锁视技影响的卡牌 ---@field public filtered_cards table<integer, Card> @ 被锁视技影响的卡牌
@ -66,10 +68,12 @@ function Engine:initialize()
self.same_generals = {} self.same_generals = {}
self.lords = {} -- lordName[] self.lords = {} -- lordName[]
self.all_card_types = {} self.all_card_types = {}
self.all_card_names = {}
self.cards = {} -- Card[] self.cards = {} -- Card[]
self.translations = {} -- srcText --> translated self.translations = {} -- srcText --> translated
self.game_modes = {} self.game_modes = {}
self.game_mode_disabled = {} self.game_mode_disabled = {}
self.main_mode_list = {}
self.kingdoms = {} self.kingdoms = {}
self.kingdom_map = {} self.kingdom_map = {}
self.damage_nature = { [fk.NormalDamage] = { "normal_damage", false } } self.damage_nature = { [fk.NormalDamage] = { "normal_damage", false } }
@ -82,6 +86,7 @@ function Engine:initialize()
self:loadPackages() self:loadPackages()
self:setLords() self:setLords()
self:loadCardNames()
self:loadDisabled() self:loadDisabled()
self:loadRequestHandlers() self:loadRequestHandlers()
self:addSkills(AuxSkills) self:addSkills(AuxSkills)
@ -265,9 +270,28 @@ function Engine:loadDisabled()
for mode_name, game_mode in pairs(self.game_modes) do for mode_name, game_mode in pairs(self.game_modes) do
local disabled_packages = {} local disabled_packages = {}
for name, pkg in pairs(self.packages) do for name, pkg in pairs(self.packages) do
if table.contains(game_mode.blacklist or Util.DummyTable, name) or --- GameMode对Package筛选
(game_mode.whitelist and not table.contains(game_mode.whitelist, name)) or if type(game_mode.whitelist) == "function" then
table.contains(pkg.game_modes_blacklist or Util.DummyTable, mode_name) or 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 (pkg.game_modes_whitelist and not table.contains(pkg.game_modes_whitelist, mode_name)) then
table.insert(disabled_packages, name) table.insert(disabled_packages, name)
end end
@ -387,6 +411,7 @@ function Engine:setLords()
for _, skill in ipairs(skills) do for _, skill in ipairs(skills) do
if skill.lordSkill then if skill.lordSkill then
table.insert(self.lords, general.name) table.insert(self.lords, general.name)
break
end end
end end
end end
@ -498,6 +523,7 @@ function Engine:addCard(card)
if self.all_card_types[card.name] == nil then if self.all_card_types[card.name] == nil then
self.skills[card.skill.name] = card.skill self.skills[card.skill.name] = card.skill
self.all_card_types[card.name] = card self.all_card_types[card.name] = card
table.insert(self.all_card_names, card.name)
end end
end end
@ -524,6 +550,23 @@ function Engine:cloneCard(name, suit, number)
return ret return ret
end 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中添加一系列游戏模式。 --- 向Engine中添加一系列游戏模式。
---@param game_modes GameMode[] @ 要添加的游戏模式列表 ---@param game_modes GameMode[] @ 要添加的游戏模式列表
function Engine:addGameModes(game_modes) function Engine:addGameModes(game_modes)

View File

@ -1,6 +1,22 @@
-- SPDX-License-Identifier: GPL-3.0-or-later -- 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] -- Switch那个开关组件 [boolean model=nil]
-- RadioButton那个多选一圆圈里面打点组件 [string model={ label, value }[]] -- RadioButton那个多选一圆圈里面打点组件 [string model={ label, value }[]]
-- ComboBox: 那个多选一,下拉一个菜单并选择一个的组件 [string model同上] -- ComboBox: 那个多选一,下拉一个菜单并选择一个的组件 [string model同上]
@ -16,21 +32,6 @@
---@field public model any @ 这种delegate需要的model参见注释 ---@field public model any @ 这种delegate需要的model参见注释
---@field public default? any @ 默认值 cfg的value ---@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 name string @ 游戏模式名
---@param min integer @ 最小玩家数 ---@param min integer @ 最小玩家数
@ -100,14 +101,17 @@ function GameMode:getAdjustedProperty (player)
return list return list
end end
--- 向游戏模式中添加拓展包过滤。
---@param whitelist string[] @ 白名单 -- 执行死亡奖惩
---@param blacklist string[] @ 黑名单 ---@param victim ServerPlayer @ 死亡角色
function GameMode:addPackageFilter(whitelist, blacklist) ---@param killer? ServerPlayer @ 击杀者
assert(type(whitelist) == "table") function GameMode:deathRewardAndPunish (victim, killer)
assert(type(blacklist) == "table") if not killer or killer.dead then return end
table.insertTable(self.whitelist, whitelist) if victim.role == "rebel" then
table.insertTable(self.blacklist, blacklist) killer:drawCards(3, "kill")
elseif victim.role == "loyalist" and killer.role == "lord" then
killer:throwAllCards("he")
end
end end
return GameMode return GameMode

View File

@ -85,21 +85,4 @@ function Package:addGameMode(game_mode)
table.insert(self.game_modes, game_mode) table.insert(self.game_modes, game_mode)
end 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 return Package

View File

@ -1194,6 +1194,25 @@ function Player:isBuddy(other)
return self.id == id or table.contains(self.buddy_list, id) return self.id == id or table.contains(self.buddy_list, id)
end end
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 --- Player是否可看到某card
--- @param cardId integer --- @param cardId integer
---@param move? CardsMoveStruct ---@param move? CardsMoveStruct
@ -1203,11 +1222,15 @@ function Player:cardVisible(cardId, move)
if room.replaying and room.replaying_show then return true end if room.replaying and room.replaying_show then return true end
local falsy = false -- 当难以决定时是否要选择暗置? local falsy = false -- 当难以决定时是否要选择暗置?
local oldarea, oldowner
if move then 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 then return true end
if move.moveVisible == false then falsy = true end if move.moveVisible == false then falsy = true end
-- specialVisible还要控制这个pile对他人是否应该可见但是不在这里生效
if move.specialVisible then return true end if move.specialVisible then return true end
if (type(move.visiblePlayers) == "number" and move.visiblePlayers == self.id) or if (type(move.visiblePlayers) == "number" and move.visiblePlayers == self.id) or
@ -1218,11 +1241,9 @@ function Player:cardVisible(cardId, move)
end end
local area = room:getCardArea(cardId) local area = room:getCardArea(cardId)
local owner = room:getCardOwner(cardId)
local card = Fk:getCardById(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 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 local status_skills = Fk:currentRoom().status_skills[VisibilitySkill] or Util.DummyTable
@ -1233,16 +1254,14 @@ function Player:cardVisible(cardId, move)
end end
end end
if area == Card.DrawPile then return false if defaultCardVisible(self, cardId, area, owner, falsy) then
elseif table.contains(public_areas, area) then return not falsy return true
elseif move and area == Card.PlayerSpecial and not move.specialName:startsWith("$") then elseif oldarea then
return not falsy -- 尽可能让牌可见
elseif table.contains(player_areas, area) then return defaultCardVisible(self, cardId, oldarea, oldowner, falsy)
local to = room:getCardOwner(cardId)
return to == self or self:isBuddy(to)
else
return false
end end
return false
end end
--- Player是否可看到某target的身份 --- Player是否可看到某target的身份
@ -1328,7 +1347,7 @@ function Player:loadJsonObject(o)
for _, id in ipairs(o.player_cards[Player.Judge]) do for _, id in ipairs(o.player_cards[Player.Judge]) do
room:setCardArea(id, Card.PlayerJudge, pid) room:setCardArea(id, Card.PlayerJudge, pid)
end end
for _, ids in ipairs(o.special_cards) do for _, ids in pairs(o.special_cards) do
for _, id in ipairs(ids) do for _, id in ipairs(ids) do
room:setCardArea(id, Card.PlayerSpecial, pid) room:setCardArea(id, Card.PlayerSpecial, pid)
end end

View File

@ -37,7 +37,7 @@
---@field public change { [string]: Item[] } 将会传递给UI的更新数据 ---@field public change { [string]: Item[] } 将会传递给UI的更新数据
local RequestHandler = class("RequestHandler") local RequestHandler = class("RequestHandler")
function RequestHandler:initialize(player) function RequestHandler:initialize(player, data)
self.room = Fk:currentRoom() self.room = Fk:currentRoom()
self.player = player self.player = player
-- finish只在Client执行 用于保证UI执行了某些必须执行的善后 -- finish只在Client执行 用于保证UI执行了某些必须执行的善后

View File

@ -25,11 +25,18 @@ local CardItem = (require 'ui_emu.common').CardItem
---@field public expanded_piles { [string]: integer[] } 用于展开/收起 ---@field public expanded_piles { [string]: integer[] } 用于展开/收起
local ReqActiveSkill = RequestHandler:subclass("ReqActiveSkill") local ReqActiveSkill = RequestHandler:subclass("ReqActiveSkill")
function ReqActiveSkill:initialize(player) function ReqActiveSkill:initialize(player, data)
RequestHandler.initialize(self, player) RequestHandler.initialize(self, player)
self.scene = RoomScene:new(self) self.scene = RoomScene:new(self)
self.expanded_piles = {} 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 end
function ReqActiveSkill:setup(ignoreInteraction) function ReqActiveSkill:setup(ignoreInteraction)
@ -55,6 +62,7 @@ function ReqActiveSkill:setup(ignoreInteraction)
self:updateUnselectedTargets() self:updateUnselectedTargets()
self:updateButtons() self:updateButtons()
self:updatePrompt()
end end
function ReqActiveSkill:finish() function ReqActiveSkill:finish()
@ -73,6 +81,15 @@ function ReqActiveSkill:setSkillPrompt(skill, cid)
end end
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() function ReqActiveSkill:setupInteraction()
local skill = Fk.skills[self.skill_name] local skill = Fk.skills[self.skill_name]
if skill and skill.interaction then if skill and skill.interaction then
@ -80,7 +97,7 @@ function ReqActiveSkill:setupInteraction()
if not interaction then if not interaction then
return return
end end
skill.interaction.data = interaction.default_choice or nil -- FIXME skill.interaction.data = interaction.default or interaction.default_choice or nil -- FIXME
-- 假设只有1个interaction (其实目前就是这样) -- 假设只有1个interaction (其实目前就是这样)
local i = Interaction:new(self.scene, "1", interaction) local i = Interaction:new(self.scene, "1", interaction)
i.skill_name = self.skill_name i.skill_name = self.skill_name
@ -291,11 +308,19 @@ function ReqActiveSkill:doOKButton()
if self.selected_card then if self.selected_card then
reply.special_skill = self.skill_name reply.special_skill = self.skill_name
end end
ClientInstance:notifyUI("ReplyToServer", json.encode(reply)) if ClientInstance then
ClientInstance:notifyUI("ReplyToServer", json.encode(reply))
else
return reply
end
end end
function ReqActiveSkill:doCancelButton() function ReqActiveSkill:doCancelButton()
ClientInstance:notifyUI("ReplyToServer", "__cancel") if ClientInstance then
ClientInstance:notifyUI("ReplyToServer", "__cancel")
else
return "__cancel"
end
end end
-- 对点击卡牌的处理。data中包含selected属性可能是选中或者取消选中分开考虑。 -- 对点击卡牌的处理。data中包含selected属性可能是选中或者取消选中分开考虑。
@ -365,6 +390,7 @@ function ReqActiveSkill:update(elemType, id, action, data)
elseif elemType == "Interaction" then elseif elemType == "Interaction" then
self:updateInteraction(data) self:updateInteraction(data)
end end
self:updatePrompt()
end end
return ReqActiveSkill return ReqActiveSkill

View File

@ -19,7 +19,6 @@ end
function ReqPlayCard:setup() function ReqPlayCard:setup()
ReqUseCard.setup(self) ReqUseCard.setup(self)
self:setPrompt(self.original_prompt)
self.scene:update("Button", "End", { enabled = true }) self.scene:update("Button", "End", { enabled = true })
end end
@ -85,14 +84,22 @@ end
function ReqPlayCard:feasible() function ReqPlayCard:feasible()
local player = self.player local player = self.player
if self.skill_name then
return ReqActiveSkill.feasible(self)
end
local card = self.selected_card
local ret = false 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 if card then
local skill = card.skill ---@type ActiveSkill local skill = card.skill ---@type ActiveSkill
ret = skill:feasible(self.selected_targets, { card.id }, player, card) 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 end
return ret return ret
end end
@ -107,11 +114,11 @@ function ReqPlayCard:selectSpecialUse(data)
if not data or data == "_normal_use" then if not data or data == "_normal_use" then
self.skill_name = nil self.skill_name = nil
self.pendings = 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 else
self.skill_name = data self.skill_name = data
self.pendings = Card:getIdList(self.selected_card) self.pendings = Card:getIdList(self.selected_card)
self:setSkillPrompt(Fk.skills[data], self.pendings) -- self:setSkillPrompt(Fk.skills[data], self.pendings)
end end
self:initiateTargets() self:initiateTargets()
end end
@ -131,7 +138,11 @@ end
function ReqPlayCard:doEndButton() function ReqPlayCard:doEndButton()
self.scene:update("SpecialSkills", "1", { skills = {} }) self.scene:update("SpecialSkills", "1", { skills = {} })
self.scene:notifyUI() self.scene:notifyUI()
ClientInstance:notifyUI("ReplyToServer", "") if ClientInstance then
ClientInstance:notifyUI("ReplyToServer", "")
else
return ""
end
end end
function ReqPlayCard:selectCard(cid, data) function ReqPlayCard:selectCard(cid, data)
@ -146,7 +157,7 @@ function ReqPlayCard:selectCard(cid, data)
self.skill_name = nil self.skill_name = nil
self.selected_card = Fk:getCardById(cid) self.selected_card = Fk:getCardById(cid)
scene:unselectOtherCards(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 = {} local sp_skills = {}
if self.selected_card.special_skills then if self.selected_card.special_skills then
sp_skills = table.simpleClone(self.selected_card.special_skills) sp_skills = table.simpleClone(self.selected_card.special_skills)

View File

@ -17,6 +17,19 @@ local ReqActiveSkill = require 'core.request_type.active_skill'
---@field public original_prompt string 最开始的提示信息;这种涉及技能按钮的需要这样一下 ---@field public original_prompt string 最开始的提示信息;这种涉及技能按钮的需要这样一下
local ReqResponseCard = ReqActiveSkill:subclass("ReqResponseCard") 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() function ReqResponseCard:setup()
if not self.original_prompt then if not self.original_prompt then
self.original_prompt = self.prompt or "" self.original_prompt = self.prompt or ""
@ -25,6 +38,7 @@ function ReqResponseCard:setup()
ReqActiveSkill.setup(self) ReqActiveSkill.setup(self)
self.selected_card = nil self.selected_card = nil
self:updateSkillButtons() self:updateSkillButtons()
self:updatePrompt()
end end
-- FIXME: 关于&牌堆的可使用打出瞎jb写了点 来个懂哥优化一下 -- FIXME: 关于&牌堆的可使用打出瞎jb写了点 来个懂哥优化一下
@ -41,8 +55,12 @@ end
function ReqResponseCard:skillButtonValidity(name) function ReqResponseCard:skillButtonValidity(name)
local player = self.player local player = self.player
local skill = Fk.skills[name] local skill = Fk.skills[name]
return skill:isInstanceOf(ViewAsSkill) and skill:enabledAtResponse(player, true) return
and skill.pattern and Exppattern:Parse(self.pattern):matchExp(skill.pattern) 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 end
function ReqResponseCard:cardValidity(cid) function ReqResponseCard:cardValidity(cid)
@ -86,7 +104,11 @@ function ReqResponseCard:doOKButton()
card = self.selected_card:getEffectiveId(), card = self.selected_card:getEffectiveId(),
targets = self.selected_targets, targets = self.selected_targets,
} }
ClientInstance:notifyUI("ReplyToServer", json.encode(reply)) if ClientInstance then
ClientInstance:notifyUI("ReplyToServer", json.encode(reply))
else
return reply
end
end end
function ReqResponseCard:doCancelButton() function ReqResponseCard:doCancelButton()
@ -109,9 +131,10 @@ function ReqResponseCard:selectSkill(skill, data)
end end
self.skill_name = skill self.skill_name = skill
self.selected_card = nil self.selected_card = nil
self:setSkillPrompt(skill)
ReqActiveSkill.setup(self) ReqActiveSkill.setup(self)
-- self:setSkillPrompt(Fk.skills[skill])
else else
self.skill_name = nil self.skill_name = nil
self.prompt = self.original_prompt self.prompt = self.original_prompt

View File

@ -4,11 +4,27 @@ local ReqResponseCard = require 'core.request_type.response_card'
---@class ReqUseCard: ReqResponseCard ---@class ReqUseCard: ReqResponseCard
local ReqUseCard = ReqResponseCard:subclass("ReqUseCard") 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) function ReqUseCard:skillButtonValidity(name)
local player = self.player local player = self.player
local skill = Fk.skills[name] local skill = Fk.skills[name]
return skill:isInstanceOf(ViewAsSkill) and skill:enabledAtResponse(player, false) return
and skill.pattern and Exppattern:Parse(self.pattern):matchExp(skill.pattern) 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 end
function ReqUseCard:cardValidity(cid) function ReqUseCard:cardValidity(cid)
@ -45,7 +61,7 @@ function ReqUseCard:targetValidity(pid)
-- 若include中全都没选且target不在include中则不可选择 -- 若include中全都没选且target不在include中则不可选择
if table.every(data.include_targets, function(id) if table.every(data.include_targets, function(id)
return not table.contains(self.selected_targets, 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 ret = false
end end
end end
@ -108,7 +124,7 @@ function ReqUseCard:selectTarget(playerid, data)
self.selected_targets = {} self.selected_targets = {}
for _, pid in ipairs(previous_targets) do for _, pid in ipairs(previous_targets) do
local ret 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, skill:targetFilter(pid, self.selected_targets,
{ card.id }, card, data.extra_data) { card.id }, card, data.extra_data)
-- 从头开始写目标 -- 从头开始写目标

View File

@ -89,4 +89,11 @@ function AbstractRoom:loadJsonObject(o)
end end
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 return AbstractRoom

View File

@ -156,4 +156,16 @@ function Skill:getTimes()
return ret return ret
end 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 return Skill

View File

@ -57,15 +57,18 @@ end
-- DO NOT modify this function -- DO NOT modify this function
function TriggerSkill:doCost(event, target, player, data) function TriggerSkill:doCost(event, target, player, data)
local start_time = os.getms() local start_time = os.getms()
local room = player.room
room.current_cost_skill = self
local ret = self:cost(event, target, player, data) local ret = self:cost(event, target, player, data)
local end_time = os.getms() local end_time = os.getms()
local room = player.room local room = player.room
-- 对于那种cost直接返回true的锁定技如果是预亮技那么还是询问一下好 -- 对于那种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 (self.main_skill and self.main_skill or self).visible then
ret = room:askForSkillInvoke(player, self.name) ret = room:askForSkillInvoke(player, self.name)
end end
room.current_cost_skill = nil
local cost_data_bak = self.cost_data local cost_data_bak = self.cost_data
room.logic:trigger(fk.BeforeTriggerSkillUse, player, { skill = self, willUse = ret }) room.logic:trigger(fk.BeforeTriggerSkillUse, player, { skill = self, willUse = ret })

View File

@ -92,4 +92,25 @@ function UsableSkill:withinTimesLimit(player, scope, card, card_name, to)
-- end))) -- end)))
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 return UsableSkill

View File

@ -98,15 +98,15 @@ end
---@param val_func? fun(e: T): integer @ 计算权值的函数对int[]可不写 ---@param val_func? fun(e: T): integer @ 计算权值的函数对int[]可不写
---@param reverse? boolean @ 是否反排?反排的话优先返回权值小的元素 ---@param reverse? boolean @ 是否反排?反排的话优先返回权值小的元素
function fk.sorted_pairs(t, val_func, reverse) function fk.sorted_pairs(t, val_func, reverse)
val_func = val_func or function(e) return e end
local t2 = table.simpleClone(t) -- 克隆一次表,用作迭代器上值 local t2 = table.simpleClone(t) -- 克隆一次表,用作迭代器上值
local t_vals = table.map(t2, val_func or function(e) return e end)
local iter = function() local iter = function()
local max_idx, max, max_val = -1, nil, nil local max_idx, max, max_val = -1, nil, nil
for i, v in ipairs(t2) do for i, v in ipairs(t2) do
if not max then 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 else
local val = val_func(v) local val = t_vals[i]
local checked = val > max_val local checked = val > max_val
if reverse then checked = not checked end if reverse then checked = not checked end
if checked then if checked then
@ -116,6 +116,7 @@ function fk.sorted_pairs(t, val_func, reverse)
end end
if max_idx == -1 then return nil, nil end if max_idx == -1 then return nil, nil end
table.remove(t2, max_idx) table.remove(t2, max_idx)
table.remove(t_vals, max_idx)
return -1, max, max_val return -1, max, max_val
end end
return iter, nil, 1 return iter, nil, 1
@ -729,4 +730,15 @@ function AimGroup:getCancelledTargets(aimGroup)
return aimGroup[AimGroup.Cancelled] return aimGroup[AimGroup.Cancelled]
end 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 } return { TargetGroup, AimGroup, Util }

View File

@ -45,6 +45,16 @@ local function readCommonSpecToSkill(skill, spec)
assert(type(spec.relate_to_place) == "string") assert(type(spec.relate_to_place) == "string")
skill.relate_to_place = spec.relate_to_place skill.relate_to_place = spec.relate_to_place
end 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 end
local function readUsableSpecToSkill(skill, spec) local function readUsableSpecToSkill(skill, spec)
@ -88,6 +98,8 @@ end
---@field public max_round_use_time? integer ---@field public max_round_use_time? integer
---@field public max_game_use_time? integer ---@field public max_game_use_time? integer
---@field public times? integer | fun(self: UsableSkill): integer ---@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 ---@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 mod_target_filter? fun(self: ActiveSkill, to_select: integer, selected: integer[], user: integer, card: Card, distance_limited: boolean): boolean?
---@field public prompt? string|fun(self: ActiveSkill, selected_cards: integer[], selected_targets: integer[]): string ---@field public prompt? string|fun(self: ActiveSkill, selected_cards: integer[], selected_targets: integer[]): string
---@field public interaction any ---@field public interaction any
---@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 ---@param spec ActiveSkillSpec
---@return ActiveSkill ---@return ActiveSkill
@ -635,7 +648,14 @@ function fk.CreateTreasure(spec)
return card return card
end 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 ---@return GameMode
function fk.CreateGameMode(spec) function fk.CreateGameMode(spec)
assert(type(spec.name) == "string") assert(type(spec.name) == "string")
@ -646,6 +666,9 @@ function fk.CreateGameMode(spec)
ret.blacklist = spec.blacklist ret.blacklist = spec.blacklist
ret.rule = spec.rule ret.rule = spec.rule
ret.logic = spec.logic 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 if spec.winner_getter then
assert(type(spec.winner_getter) == "function") assert(type(spec.winner_getter) == "function")
@ -663,6 +686,10 @@ function fk.CreateGameMode(spec)
assert(type(spec.get_adjusted) == "function") assert(type(spec.get_adjusted) == "function")
ret.getAdjustedProperty = spec.get_adjusted ret.getAdjustedProperty = spec.get_adjusted
end end
if spec.reward_punish then
assert(type(spec.reward_punish) == "function")
ret.deathRewardAndPunish = spec.reward_punish
end
return ret return ret
end end

View File

@ -79,17 +79,18 @@ end
Config = loadConf() Config = loadConf()
-- 禁用各种危险的函数尽可能让Lua执行安全的代码。 -- 禁用各种危险的函数尽可能让Lua执行安全的代码。
local _os = { os = {
time = os.time, time = os.time,
date = os.date, date = os.date,
clock = os.clock, clock = os.clock,
difftime = os.difftime, difftime = os.difftime,
getms = os.getms, getms = os.getms,
} }
os = _os io = {
io = nil lines = io.lines
}
package = nil package = nil
load = nil -- load = nil
loadfile = nil loadfile = nil
local _dofile = dofile local _dofile = dofile
dofile = function(f) dofile = function(f)

View File

@ -245,7 +245,9 @@ local function where(info, context_lines)
local filename = info.source:match("@(.*)") local filename = info.source:match("@(.*)")
if filename then if filename then
if UsingNewCore and (filename:startsWith("./lua/") or filename:startsWith("lua/")) then 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 end
pcall(function() for line in io.lines(filename) do table.insert(source, line) end end) pcall(function() for line in io.lines(filename) do table.insert(source, line) end end)
elseif info.source then elseif info.source then

View File

@ -24,7 +24,10 @@ function Object:initialize(...) end
---@return T ---@return T
function Object:new(...)end function Object:new(...)end
---@generic T
---@param self T
---@param name string ---@param name string
---@return T
function Object:subclass(name)end function Object:subclass(name)end
---@param class class|Object ---@param class class|Object

View File

@ -7,39 +7,222 @@
---@field public room Room ---@field public room Room
---@field public player ServerPlayer ---@field public player ServerPlayer
---@field public command string ---@field public command string
---@field public jsonData string ---@field public data any
---@field public cb_table table<string, fun(self: AI, jsonData: string)> ---@field public handler ReqActiveSkill 可能空,但是没打问号(免得一堆警告)
local AI = class("AI") local AI = class("AI")
function AI:initialize(player) function AI:initialize(player)
---@diagnostic disable-next-line
self.room = RoomInstance self.room = RoomInstance
self.player = player 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 end
function AI:readRequestData() function AI:__tostring()
self.command = self.player.ai_data.command return string.format("%s: %s", self.class.name, tostring(self.player))
self.jsonData = self.player.ai_data.jsonData 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 end
function AI:makeReply() function AI:makeReply()
Self = self.player Self = self.player
-- local start = os.getms() local now = os.getms()
local ret = self.cb_table[self.command] and self.cb_table[self.command](self, self.jsonData) or "__cancel" local fn = self["handle" .. self.command]
if ret == "" then ret = "__cancel" end local is_active = self.command == "AskForUseActiveSkill"
-- local to_delay = 500 - (os.getms() - start) / 1000 if is_active then
-- print(to_delay) local skill = Fk.skills[self.data[1]]
-- self.room:delay(to_delay) 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 return ret
end end

View File

@ -2,20 +2,29 @@
AI = require "server.ai.ai" AI = require "server.ai.ai"
TrustAI = require "server.ai.trust_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" SmartAI = require "server.ai.smart_ai"
---[[ 调试中暂且不加载额外的AI。 ---[[ 调试中暂且不加载额外的AI。
-- load ai module from packages -- load ai module from packages
local directories = FileIO.ls("packages") local directories
require "packages.standard.ai" if UsingNewCore then
require "packages.standard_cards.ai" directories = FileIO.ls("..")
require "packages.maneuvering.ai" 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")
table.removeOne(directories, "standard_cards") table.removeOne(directories, "standard_cards")
table.removeOne(directories, "maneuvering") table.removeOne(directories, "maneuvering")
--[[
local _disable_packs = json.decode(fk.GetDisabledPacks()) local _disable_packs = json.decode(fk.GetDisabledPacks())
for _, dir in ipairs(directories) do for _, dir in ipairs(directories) do

421
lua/server/ai/logic.lua Normal file
View 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

View File

@ -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
View 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

View File

@ -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
View 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

View File

@ -1,41 +1,32 @@
-- SPDX-License-Identifier: GPL-3.0-or-later -- SPDX-License-Identifier: GPL-3.0-or-later
--[[ --[[
SmartAI: AI架构的AI体系
AI常用的种种表以及实用函数等AI逻辑的接口
AI的核心在于编程实现对各种交互的回应(room:askForXXX) AI框架
smart_cb表以实现合理的答复
便AIAI判断时常用的函数
1. smart_cb表
2.
3.
-- TODO: 优化底层逻辑防止AI每次操作之前都要json.decode一下。
-- TODO: 更加详细的文档
--]] --]]
---@class SmartAI: AI ---@class SmartAI: TrustAI
---@field private _memory table<string, any> @ AI底层的空间换时间机制 ---@field private _memory table<string, any> @ AI底层的空间换时间机制
---@field public friends ServerPlayer[] @ 队友 ---@field public friends ServerPlayer[] @ 队友
---@field public enemies ServerPlayer[] @ 敌人 ---@field public enemies ServerPlayer[] @ 敌人
local SmartAI = AI:subclass("SmartAI") local SmartAI = TrustAI:subclass("SmartAI") -- 哦,我懒得写出闪之类的,不得不继承一下,饶了我吧
---@type table<string, fun(self: SmartAI, jsonData: string): string> AIParser = require 'lua.server.ai.parser'
local smart_cb = {} 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) function SmartAI:initialize(player)
AI.initialize(self, player) TrustAI.initialize(self, player)
self.cb_table = smart_cb
self.player = player
end end
function SmartAI:makeReply() function SmartAI:makeReply()
self._memory = {} self._memory = setmetatable({}, { __mode = "k" })
return AI.makeReply(self) return TrustAI.makeReply(self)
end end
function SmartAI:__index(k) function SmartAI:__index(k)
@ -56,201 +47,214 @@ function SmartAI:__index(k)
return ret return ret
end 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"状态 -- 对应UI中的"responding"状态和"playing"状态
-- AI代码需要像实际操作UI那样完成以下几个任务 -- AI代码需要像实际操作UI那样完成以下几个任务
-- * 点击技能按钮出牌阶段或者打算使用ViewAsSkill -- * 点击技能按钮完成interaction与子卡选择或者直接点可用手牌
-- * 技能如果带有interaction则选择interaction
-- * 如果需要的话点选手牌
-- * 选择目标 -- * 选择目标
-- * 点确定 -- * 点确定
-- 这些步骤归结起来就是让AI想办法返回如下定义的UseReply
-- 或者返回nil表示点取消
--=================================================== --===================================================
---@class UseReply -- 考虑为triggerSkill设置收益修正函数
---@field card? integer|string @ string情况下是json.encode后
---@field targets? integer[]
---@field special_skill string @ 出牌阶段空闲点使用实体卡特有
---@field interaction_data any @ 因技能而异一般都是nil
---@param card integer|table --@field ask_use_card? fun(skill: ActiveSkill, ai: SmartAI): any
---@param targets? integer[] --@field ask_response? fun(skill: ActiveSkill, ai: SmartAI): any
---@param special_skill? string
---@param interaction_data? any ---@type table<string, SkillAI>
function SmartAI:buildUseReply(card, targets, special_skill, interaction_data) fk.ai_skills = {}
if type(card) == "table" then card = json.encode(card) end
return { ---@param key string
card = card, ---@param spec SkillAISpec
targets = targets or {}, ---@param inherit? string
special_skill = special_skill, function SmartAI.static:setSkillAI(key, spec, inherit)
interaction_data = interaction_data, 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 end
-- AskForUseActiveSkill: 询问发动主动技/视为技 --- 将spec中的键值保存到这个技能的ai中
-- * 此处 UseReply.card 必定由 json.encode 而来 ---@param key string
-- * 且原型为 { skill = skillName, subcards = integer[] } ---@param spec SkillAISpec 表
---------------------------------------------------------- ---@param inherit? string 可以直接复用某个技能已有的函数 自然spec中更加优先
---@diagnostic disable-next-line
---@type table<string, fun(self: SmartAI, prompt: string, cancelable?: boolean, data: any): UseReply?> function SmartAI:setSkillAI(key, spec, inherit)
fk.ai_active_skill = {} error("This is a static method. Please use SmartAI:setSkillAI(...)")
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)
end end
-- AskForUseCard: 询问使用卡牌 SmartAI:setSkillAI("__card_skill", {
-- 判断函数一样返回UseReply此时卡牌可能是integer或者string choose_targets = function(self, ai)
-- 为string的话肯定是由ViewAsSkill转化而来 local targets = ai:getEnabledTargets()
-- 真的要考虑ViewAsSkill吗害怕 local logic = AIGameLogic:new(ai)
--------------------------------------------------------- local val_func = function(p)
logic.benefit = 0
-- 使用牌相关——同时见于主动使用和响应式使用。 logic:useCard({
--- 键是prompt的第一项或者牌名优先prompt其次name实在不行trueName。 from = ai.player.id,
---@type table<string, fun(self: SmartAI, pattern: string, prompt: string, cancelable?: boolean, extra_data?: UseExtraData): UseReply?> tos = { { p.id } },
fk.ai_use_card = setmetatable({}, { card = ai:getSelectedCard(),
__index = function(_, k) })
-- FIXME: 感觉不妥 verbose("目前状况下,对%s的预测收益为%d", tostring(p), logic.benefit)
local c = Fk.all_card_types[k] return logic.benefit
if not c then return nil end end
if c.type == Card.TypeEquip then for _, p, val in fk.sorted_pairs(targets, val_func) do
return function(self, pattern, prompt, cancelable, extra_data) if val > 0 then
local slashes = self:getCards(k, "use", extra_data) ai:selectTarget(p, true)
if #slashes == 0 then return nil end return ai:doOKButton(), val
else
return self:buildUseReply(slashes[1].id) break
end end
end 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) function SmartAI.static:setCardSkillAI(key, spec)
if cancelable then return nil end SmartAI:setSkillAI(key, spec, "__card_skill")
local cards = self:getCards(pattern, "use", exdata)
if #cards == 0 then return nil end
-- TODO: 目标
return self:buildUseReply(cards[1].id)
end end
--- 请求使用先试图使用prompt再试图使用card_name最后交给随机AI -- 等价于SmartAI:setCardSkillAI(key, spec, "__card_skill")
smart_cb["AskForUseCard"] = function(self, jsonData) ---@param key string
local data = json.decode(jsonData) ---@param spec SkillAISpec 表
local card_name, pattern, prompt, cancelable, extra_data = table.unpack(data) function SmartAI:setCardSkillAI(key, spec)
error("This is a static method. Please use SmartAI:setCardSkillAI(...)")
end
local prompt_prefix = prompt:split(":")[1] ---@type table<string, TriggerSkillAI>
local key fk.ai_trigger_skills = {}
if fk.ai_use_card[prompt_prefix] then
key = prompt_prefix ---@param spec TriggerSkillAISpec
elseif fk.ai_use_card[card_name] then function SmartAI.static:setTriggerSkillAI(key, spec)
key = card_name if not fk.ai_trigger_skills[key] then
else fk.ai_trigger_skills[key] = TriggerSkillAI:new(key)
local tmp = card_name:split("__")
key = tmp[#tmp]
end end
local ret = self:callFromTable(fk.ai_use_card, defauld_use_card, key, local ai = fk.ai_trigger_skills[key]
self, pattern, prompt, cancelable, extra_data) if spec.correct_func then
ai.getCorrect = spec.correct_func
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]
end 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 end
-- PlayCard: 出牌阶段空闲时间点使用牌/技能 --- 将spec中的键值保存到这个技能的ai中
-- 老规矩得丢一个UseReply回来但是自由度就高得多了 ---@param key string
-- 需要完成的任务:从众多亮着的卡、技能中选一个 ---@param spec TriggerSkillAISpec
-- 考虑要不要用?用的话就用,否则选下个 ---@diagnostic disable-next-line
-- 至于如何使用可以复用askFor中的函数 function SmartAI:setTriggerSkillAI(key, spec)
----------------------------------------------- error("This is a static method. Please use SmartAI:setTriggerSkillAI(...)")
smart_cb["PlayCard"] = function(self) end
local extra_use_data = { playing = true }
local cards = self:getCards(".", "use", extra_use_data)
local card_names = {} ---@param cid_or_skill integer|string
for _, cd in ipairs(cards) do function SmartAI:getBasicBenefit(cid_or_skill)
-- TODO: 视为技 end
-- 视为技对应的function一般会返回一张印出来的卡又要纳入新的考虑范围了
-- 不过这种根据牌名判断的逻辑而言 可能需要调用多次视为技函数了 local function hasKey(t1, t2, key)
-- 要用好空间换时间 if (t1 and t1[key]) or (t2 and t2[key]) then return true end
table.insertIfNeed(card_names, cd.name) 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 end
-- TODO: 主动技 if not fn then return end
return fn(...)
end
-- 第二步:考虑使用其中之一 function SmartAI:handleAskForUseActiveSkill()
local value_func = function(str) return #str end local name = self.handler.skill_name
for _, name in fk.sorted_pairs(card_names, value_func) do local current_skill = self:currentSkill()
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)
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
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 "" return ""
end end
@ -265,24 +269,33 @@ end
-- 函数返回true或者false即可。 -- 函数返回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) function SmartAI:handleAskForSkillInvoke(data)
local data = json.decode(jsonData)
local skillName, prompt = data[1], data[2] 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 if type(ask) == "function" then
return ask(self, prompt) and "1" or "" return ask(skill, self) and "1" or ""
elseif type(ask) == "boolean" then elseif type(ask) == "boolean" then
return ask and "1" or "" return ask and "1" or ""
elseif Fk.skills[skillName].frequency == Skill.Frequent then elseif Fk.skills[skillName].frequency == Skill.Frequent then
return "1" return "1"
else else
return RandomAI.cb_table["AskForSkillInvoke"](self, jsonData) return math.random() < 0.5 and "1" or ""
end end
end end
--]]
-- 敌友判断相关。 -- 敌友判断相关。
-- 目前才开始,做个明身份打牌的就行了。 -- 目前才开始,做个明身份打牌的就行了。
@ -309,38 +322,7 @@ end
-- sorted_pairs 见 core/util.lua -- 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 return SmartAI

View 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

View File

@ -1,15 +1,47 @@
-- SPDX-License-Identifier: GPL-3.0-or-later -- SPDX-License-Identifier: GPL-3.0-or-later
-- Trust AI -- Trust AI
-- 需要打出牌时,有的话就打出
-- 需要使用闪、对自己使用无懈、酒、桃时,只要有就使用
-- 除此之外什么都不做
---@class TrustAI: AI ---@class TrustAI: AI
local TrustAI = AI:subclass("TrustAI") local TrustAI = AI:subclass("TrustAI")
local trust_cb = {}
function TrustAI:initialize(player) function TrustAI:initialize(player)
AI.initialize(self, 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 end
return TrustAI return TrustAI

View File

@ -48,6 +48,7 @@ fk.FinishJudge = 27
fk.RoundStart = 28 fk.RoundStart = 28
fk.RoundEnd = 29 fk.RoundEnd = 29
fk.AfterRoundEnd = 85
fk.BeforeTurnOver = 79 fk.BeforeTurnOver = 79
fk.TurnedOver = 30 fk.TurnedOver = 30
@ -126,7 +127,7 @@ fk.AfterSkillEffect = 82
-- 83 = PreTurnStart -- 83 = PreTurnStart
-- 84 = AfterTurnEnd -- 84 = AfterTurnEnd
-- 85 = xxx -- 85 = AfterRoundEnd
-- 86 = AfterPhaseEnd -- 86 = AfterPhaseEnd
fk.AreaAborted = 87 fk.AreaAborted = 87

View File

@ -80,11 +80,11 @@ function DrawInitial:main()
return return
end end
room:notifyMoveFocus(room.alive_players, "AskForLuckCard") local request = Request:new(room.alive_players, "AskForSkillInvoke")
local request = Request:new("AskForSkillInvoke", room.alive_players)
for _, p in ipairs(room.alive_players) do for _, p in ipairs(room.alive_players) do
request:setData(p, { "AskForLuckCard", "#AskForLuckCard:::" .. room.settings.luckTime }) request:setData(p, { "AskForLuckCard", "#AskForLuckCard:::" .. room.settings.luckTime })
end end
request.focus_text = "AskForLuckCard"
request.luck_data = luck_data request.luck_data = luck_data
request.accept_cancel = true request.accept_cancel = true
request:ask() request:ask()
@ -101,28 +101,22 @@ local Round = GameEvent:subclass("GameEvent.Round")
function Round:action() function Round:action()
local room = self.room local room = self.room
local p local currentPlayer
local nextTurnOwner
local skipRoundPlus = false while true do
repeat GameEvent.Turn:create(room.current):exec()
nextTurnOwner = nil
skipRoundPlus = false
p = room.current
GameEvent.Turn:create(p):exec()
if room.game_finished then break end 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) room.logic:trigger(fk.EventTurnChanging, room.current, changingData, true)
skipRoundPlus = changingData.skipRoundPlus local nextTurnOwner = changingData.to
local nextAlive = room.current:getNextAlive(true, nil, true) if room.current.seat > nextTurnOwner.seat and not changingData.skipRoundPlus then
if nextAlive ~= changingData.to and not changingData.to.dead then break
room.current = changingData.to
nextTurnOwner = changingData.to
else else
room.current = nextAlive room.current = nextTurnOwner
end end
until p.seat >= (nextTurnOwner or p:getNextAlive(true, nil, true)).seat and not skipRoundPlus end
end end
function Round:main() function Round:main()
@ -153,7 +147,8 @@ function Round:main()
logic:trigger(fk.RoundStart, room.current) logic:trigger(fk.RoundStart, room.current)
self:action() self:action()
logic:trigger(fk.RoundEnd, p) logic:trigger(fk.RoundEnd, room.current)
logic:trigger(fk.AfterRoundEnd, room.current)
end end
function Round:clear() function Round:clear()
@ -188,7 +183,10 @@ local Turn = GameEvent:subclass("GameEvent.Turn")
function Turn:prepare() function Turn:prepare()
local room = self.room local room = self.room
local logic = room.logic local logic = room.logic
local player = room.current local player = 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 if player.rest > 0 and player.rest < 999 then
room:setPlayerRest(player, player.rest - 1) room:setPlayerRest(player, player.rest - 1)
@ -208,21 +206,24 @@ function Turn:prepare()
return true return true
end end
return logic:trigger(fk.BeforeTurnStart, player) return logic:trigger(fk.BeforeTurnStart, player, data)
end end
function Turn:main() function Turn:main()
local room = self.room local room = self.room
room.current.phase = Player.PhaseNone local player = self.data[1]---@type ServerPlayer
room.logic:trigger(fk.TurnStart, room.current) local data = self.data[2]---@type TurnStruct
room.current.phase = Player.NotActive player.phase = Player.PhaseNone
room.current:play() room.logic:trigger(fk.TurnStart, player, data)
player.phase = Player.NotActive
player:play(data.phase_table)
end end
function Turn:clear() function Turn:clear()
local room = self.room 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 local logic = room.logic
if self.interrupted then if self.interrupted then
if current.phase ~= Player.NotActive then if current.phase ~= Player.NotActive then
@ -239,8 +240,8 @@ function Turn:clear()
end end
current.phase = Player.PhaseNone current.phase = Player.PhaseNone
logic:trigger(fk.TurnEnd, current, nil, self.interrupted) logic:trigger(fk.TurnEnd, current, data, self.interrupted)
logic:trigger(fk.AfterTurnEnd, current, nil, self.interrupted) logic:trigger(fk.AfterTurnEnd, current, data, self.interrupted)
current.phase = Player.NotActive current.phase = Player.NotActive
room:setTag("endTurn", false) room:setTag("endTurn", false)
@ -275,7 +276,7 @@ function Phase:main()
local room = self.room local room = self.room
local logic = room.logic 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 not logic:trigger(fk.EventPhaseStart, player) then
if player.phase ~= Player.NotActive then if player.phase ~= Player.NotActive then
logic:trigger(fk.EventPhaseProceeding, player) logic:trigger(fk.EventPhaseProceeding, player)
@ -322,7 +323,7 @@ function Phase:main()
} }
room.logic:trigger(fk.DrawNCards, player, data) room.logic:trigger(fk.DrawNCards, player, data)
if not player._phase_end then if not player._phase_end then
room:drawCards(player, data.n, "game_rule") room:drawCards(player, data.n, "phase_draw")
end end
room.logic:trigger(fk.AfterDrawNCards, player, data) room.logic:trigger(fk.AfterDrawNCards, player, data)
end, end,
@ -332,8 +333,8 @@ function Phase:main()
while not player.dead do while not player.dead do
if player._phase_end then break end if player._phase_end then break end
logic:trigger(fk.StartPlayCard, player, nil, true) logic:trigger(fk.StartPlayCard, player, nil, true)
room:notifyMoveFocus(player, "PlayCard")
local result = room:doRequest(player, "PlayCard", player.id) local result = Request:new(player, "PlayCard"):getResult(player)
if result == "" then break end if result == "" then break end
local useResult = room:handleUseCardReply(player, result) local useResult = room:handleUseCardReply(player, result)
@ -354,7 +355,7 @@ function Phase:main()
) - player:getMaxCards() ) - player:getMaxCards()
room:broadcastProperty(player, "MaxCards") room:broadcastProperty(player, "MaxCards")
if discardNum > 0 then if discardNum > 0 then
room:askForDiscard(player, discardNum, discardNum, false, "game_rule", false) room:askForDiscard(player, discardNum, discardNum, false, "phase_discard", false)
end end
end, end,
[Player.Finish] = function() [Player.Finish] = function()

View File

@ -100,29 +100,17 @@ end
---@param exchange? boolean @ 是否要替换原有判定牌(即类似鬼道那样) ---@param exchange? boolean @ 是否要替换原有判定牌(即类似鬼道那样)
function JudgeEventWrappers:retrial(card, player, judge, skillName, exchange) function JudgeEventWrappers:retrial(card, player, judge, skillName, exchange)
if not card then return end 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 move1 = {} ---@type CardsMoveInfo
local resp = {} ---@type CardResponseEvent move1.ids = { card:getEffectiveId() }
resp.from = player.id move1.from = self.owner_map[card:getEffectiveId()]
resp.card = card move1.toArea = Card.Processing
resp.skipDrop = true move1.moveReason = fk.ReasonJustMove
self:responseCard(resp) move1.skillName = skillName
else self:moveCards(move1)
local move1 = {} ---@type CardsMoveInfo
move1.ids = { card:getEffectiveId() }
move1.from = player.id
move1.toArea = Card.Processing
move1.moveReason = fk.ReasonJustMove
move1.skillName = skillName
self:moveCards(move1)
end
local oldJudge = judge.card local oldJudge = judge.card
judge.card = card judge.card = card
local rebyre = judge.retrial_by_response
judge.retrial_by_response = player
self:sendLog{ self:sendLog{
type = "#ChangedJudge", type = "#ChangedJudge",
@ -134,16 +122,18 @@ function JudgeEventWrappers:retrial(card, player, judge, skillName, exchange)
Fk:filterCard(judge.card.id, judge.who, judge) 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 local move2 = {} ---@type CardsMoveInfo
move2.ids = { oldJudge:getEffectiveId() } move2.ids = { oldJudge:getEffectiveId() }
move2.toArea = exchange and Card.PlayerHand or Card.DiscardPile move2.toArea = exchange and Card.PlayerHand or Card.DiscardPile
move2.moveReason = exchange and fk.ReasonJustMove or fk.ReasonJudge move2.moveReason = exchange and fk.ReasonJustMove or fk.ReasonJudge
move2.to = exchange and player.id or nil move2.to = exchange and player.id or nil
move2.skillName = skillName move2.skillName = skillName
self:moveCards(move2)
end
self:moveCards(move2)
end end
return { Judge, JudgeEventWrappers } return { Judge, JudgeEventWrappers }

View File

@ -397,16 +397,17 @@ end
---@param cards integer|integer[] @ 移动的牌 ---@param cards integer|integer[] @ 移动的牌
---@param skillName? string @ 技能名 ---@param skillName? string @ 技能名
---@param convert? boolean @ 是否可以替换装备(默认可以) ---@param convert? boolean @ 是否可以替换装备(默认可以)
---@param proposer? ServerPlayer @ 操作者 ---@param proposer? ServerPlayer | integer @ 操作者
function MoveEventWrappers:moveCardIntoEquip(target, cards, skillName, convert, proposer) function MoveEventWrappers:moveCardIntoEquip(target, cards, skillName, convert, proposer)
convert = (convert == nil) and true or convert convert = (convert == nil) and true or convert
skillName = skillName or "" skillName = skillName or ""
cards = type(cards) == "table" and cards or {cards} 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 = {} local moves = {}
for _, cardId in ipairs(cards) do for _, cardId in ipairs(cards) do
local card = Fk:getCardById(cardId) local card = Fk:getCardById(cardId)
local fromId = self.owner_map[cardId] local fromId = self.owner_map[cardId]
local proposerId = proposer and proposer.id or nil
if target:canMoveCardIntoEquip(cardId, convert) then if target:canMoveCardIntoEquip(cardId, convert) then
if target:hasEmptyEquipSlot(card.sub_type) 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}) table.insert(moves,{ids = {cardId}, from = fromId, to = target.id, toArea = Card.PlayerEquip, moveReason = fk.ReasonPut,skillName = skillName,proposer = proposerId})

View File

@ -42,17 +42,19 @@ function Pindian:main()
table.insert(targets, pindianData.from) table.insert(targets, pindianData.from)
pindianData.from.request_data = json.encode(data) pindianData.from.request_data = json.encode(data)
else else
local _pindianCard = pindianData.fromCard if not pindianData._fromCard then
local pindianCard = _pindianCard:clone(_pindianCard.suit, _pindianCard.number) local _pindianCard = pindianData.fromCard
pindianCard:addSubcard(_pindianCard.id) 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, { table.insert(moveInfos, {
ids = { _pindianCard.id }, ids = { pindianData._fromCard.id },
from = room.owner_map[_pindianCard.id], from = room.owner_map[pindianData._fromCard.id],
fromArea = room:getCardArea(_pindianCard.id), fromArea = room:getCardArea(pindianData._fromCard.id),
toArea = Card.Processing, toArea = Card.Processing,
moveReason = fk.ReasonPut, moveReason = fk.ReasonPut,
skillName = pindianData.reason, skillName = pindianData.reason,
@ -61,17 +63,19 @@ function Pindian:main()
end end
for _, to in ipairs(pindianData.tos) do for _, to in ipairs(pindianData.tos) do
if pindianData.results[to.id] and pindianData.results[to.id].toCard then if pindianData.results[to.id] and pindianData.results[to.id].toCard then
local _pindianCard = pindianData.results[to.id].toCard if not pindianData.results[to.id]._toCard then
local pindianCard = _pindianCard:clone(_pindianCard.suit, _pindianCard.number) local _pindianCard = pindianData.results[to.id].toCard
pindianCard:addSubcard(_pindianCard.id) 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, { table.insert(moveInfos, {
ids = { _pindianCard.id }, ids = { pindianData.results[to.id]._toCard.id },
from = room.owner_map[_pindianCard.id], from = room.owner_map[pindianData.results[to.id]._toCard.id],
fromArea = room:getCardArea(_pindianCard.id), fromArea = room:getCardArea(pindianData.results[to.id]._toCard.id),
toArea = Card.Processing, toArea = Card.Processing,
moveReason = fk.ReasonPut, moveReason = fk.ReasonPut,
skillName = pindianData.reason, skillName = pindianData.reason,
@ -79,48 +83,51 @@ function Pindian:main()
}) })
else else
table.insert(targets, to) table.insert(targets, to)
to.request_data = json.encode(data)
end end
end end
room:notifyMoveFocus(targets, "AskForPindian") if #targets ~= 0 then
room:doBroadcastRequest("AskForUseActiveSkill", targets) 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 for _, p in ipairs(targets) do
local _pindianCard local _pindianCard
if p.reply_ready then local result = req:getResult(p)
local replyCard = json.decode(p.client_reply).card if result ~= "" then
_pindianCard = Fk:getCardById(json.decode(replyCard).subcards[1]) local replyCard = result.card
else _pindianCard = Fk:getCardById(json.decode(replyCard).subcards[1])
_pindianCard = Fk:getCardById(p:getCardIds(Player.Hand)[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 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 end
room:moveCards(table.unpack(moveInfos)) room:moveCards(table.unpack(moveInfos))

View File

@ -110,7 +110,7 @@ function SkillEventWrappers:handleAddLoseSkills(player, skill_names, source_skil
if #skill_names == 0 then return end if #skill_names == 0 then return end
local losts = {} ---@type boolean[] local losts = {} ---@type boolean[]
local triggers = {} ---@type Skill[] local triggers = {} ---@type Skill[]
local lost_piles = {} ---@type integer[] -- local lost_piles = {} ---@type integer[]
for _, skill in ipairs(skill_names) do for _, skill in ipairs(skill_names) do
if string.sub(skill, 1, 1) == "-" then if string.sub(skill, 1, 1) == "-" then
local actual_skill = string.sub(skill, 2, #skill) 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(losts, true)
table.insert(triggers, s) table.insert(triggers, s)
if s.derived_piles then -- if s.derived_piles then
for _, pile_name in ipairs(s.derived_piles) do -- for _, pile_name in ipairs(s.derived_piles) do
table.insertTableIfNeed(lost_piles, player:getPile(pile_name)) -- table.insertTableIfNeed(lost_piles, player:getPile(pile_name))
end -- end
end -- end
end end
end end
else else
@ -169,19 +169,26 @@ function SkillEventWrappers:handleAddLoseSkills(player, skill_names, source_skil
if (not no_trigger) and #triggers > 0 then if (not no_trigger) and #triggers > 0 then
for i = 1, #triggers do for i = 1, #triggers do
local event = losts[i] and fk.EventLoseSkill or fk.EventAcquireSkill if losts[i] then
self.logic:trigger(event, player, triggers[i]) 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
end end
if #lost_piles > 0 then -- if #lost_piles > 0 then
self:moveCards({ -- self:moveCards({
ids = lost_piles, -- ids = lost_piles,
from = player.id, -- from = player.id,
toArea = Card.DiscardPile, -- toArea = Card.DiscardPile,
moveReason = fk.ReasonPutIntoDiscardPile, -- moveReason = fk.ReasonPutIntoDiscardPile,
}) -- })
end -- end
end end
return { SkillEffect, SkillEventWrappers } return { SkillEffect, SkillEventWrappers }

View File

@ -858,6 +858,9 @@ function UseCardEventWrappers:handleCardEffect(event, cardEffectEvent)
extra_data = { useEventId = parentUseEvent.id, effectTo = cardEffectEvent.to } extra_data = { useEventId = parentUseEvent.id, effectTo = cardEffectEvent.to }
end end
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) local use = self:askForNullification(players, nil, nil, prompt, true, extra_data, cardEffectEvent)
if use then if use then
use.toCard = cardEffectEvent.card use.toCard = cardEffectEvent.card

View File

@ -181,7 +181,7 @@ local function bin_search(events, from, to, n, func)
end end
-- 从某个区间中找出类型符合且符合func函数检测的至多n个事件。 -- 从某个区间中找出类型符合且符合func函数检测的至多n个事件。
---@param eventType integer @ 要查找的事件类型 ---@param eventType GameEvent @ 要查找的事件类型
---@param n integer @ 最多找多少个 ---@param n integer @ 最多找多少个
---@param func fun(e: GameEvent): boolean? @ 过滤用的函数 ---@param func fun(e: GameEvent): boolean? @ 过滤用的函数
---@param endEvent? GameEvent @ 区间终止点,默认为本事件结束 ---@param endEvent? GameEvent @ 区间终止点,默认为本事件结束

View File

@ -132,29 +132,18 @@ function GameLogic:chooseGenerals()
local nonlord = room:getOtherPlayers(lord, true) local nonlord = room:getOtherPlayers(lord, true)
local generals = table.random(room.general_pile, #nonlord * generalNum) local generals = table.random(room.general_pile, #nonlord * generalNum)
local req = Request:new(nonlord, "AskForGeneral")
for i, p in ipairs(nonlord) do for i, p in ipairs(nonlord) do
local arg = table.slice(generals, (i - 1) * generalNum + 1, i * generalNum + 1) local arg = table.slice(generals, (i - 1) * generalNum + 1, i * generalNum + 1)
p.request_data = json.encode{ arg, n } req:setData(p, { arg, n })
p.default_reply = table.random(arg, n) req:setDefaultReply(p, table.random(arg, n))
end end
room:notifyMoveFocus(nonlord, "AskForGeneral")
room:doBroadcastRequest("AskForGeneral", nonlord)
for _, p in ipairs(nonlord) do for _, p in ipairs(nonlord) do
local general, deputy local result = req:getResult(p)
if p.general == "" and p.reply_ready then local general, deputy = result[1], result[2]
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)
room:prepareGeneral(p, general, deputy) room:prepareGeneral(p, general, deputy)
p.default_reply = ""
end end
room:askForChooseKingdom(nonlord) room:askForChooseKingdom(nonlord)
@ -233,7 +222,9 @@ function GameLogic:attachSkillToPlayers()
return return
end 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 end
for _, p in ipairs(room.alive_players) do for _, p in ipairs(room.alive_players) do
local skills = Fk.generals[p.general].skills local skills = Fk.generals[p.general].skills
@ -278,6 +269,8 @@ function GameLogic:action()
while true do while true do
execGameEvent(GameEvent.Round) execGameEvent(GameEvent.Round)
if room.game_finished then break end 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
end end
@ -570,9 +563,10 @@ function GameLogic:getMostRecentEvent(eventType)
end end
--- 如果当前事件刚好是技能生效事件,就返回那个技能名,否则返回空串。 --- 如果当前事件刚好是技能生效事件,就返回那个技能名,否则返回空串。
---@return string|nil
function GameLogic:getCurrentSkillName() function GameLogic:getCurrentSkillName()
local skillEvent = self:getCurrentEvent() local skillEvent = self:getCurrentEvent()
local ret = "" local ret = nil
if skillEvent.event == GameEvent.SkillEffect then if skillEvent.event == GameEvent.SkillEffect then
local _, _, _skill = table.unpack(skillEvent.data) local _, _, _skill = table.unpack(skillEvent.data)
local skill = _skill.main_skill and _skill.main_skill or _skill local skill = _skill.main_skill and _skill.main_skill or _skill
@ -605,7 +599,7 @@ function GameLogic:getEventsOfScope(eventType, n, func, scope)
end end
-- 在指定历史范围中找符合条件的事件(逆序) -- 在指定历史范围中找符合条件的事件(逆序)
---@param eventType integer @ 要查找的事件类型 ---@param eventType GameEvent @ 要查找的事件类型
---@param func fun(e: GameEvent): boolean @ 过滤用的函数 ---@param func fun(e: GameEvent): boolean @ 过滤用的函数
---@param n integer @ 最多找多少个 ---@param n integer @ 最多找多少个
---@param end_id integer @ 查询历史范围从最后的事件开始逆序查找直到id为end_id的事件不含 ---@param end_id integer @ 查询历史范围从最后的事件开始逆序查找直到id为end_id的事件不含

View File

@ -29,7 +29,7 @@ MarkEnum.BypassTimesLimitTo = "BypassTimesLimitTo"
MarkEnum.BypassDistancesLimitTo = "BypassDistancesLimitTo" MarkEnum.BypassDistancesLimitTo = "BypassDistancesLimitTo"
---非锁定技失效 ---非锁定技失效
MarkEnum.UncompulsoryInvalidity = "UncompulsoryInvalidity" MarkEnum.UncompulsoryInvalidity = "UncompulsoryInvalidity"
---失效技能表 ---失效技能表(用``Room:addTableMark``和``Room:removeTableMark``控制)
MarkEnum.InvalidSkills = "InvalidSkills" MarkEnum.InvalidSkills = "InvalidSkills"
---不可明置值为表m - 主将, d - 副将) ---不可明置值为表m - 主将, d - 副将)
MarkEnum.RevealProhibited = "RevealProhibited" MarkEnum.RevealProhibited = "RevealProhibited"

View File

@ -1,19 +1,23 @@
---@class Request : Object ---@class Request : Object
---@field public room Room ---@field public room Room
---@field public players ServerPlayer[] ---@field public players ServerPlayer[]
---@field public n integer @ n个人做出回复后询问结束 ---@field public n integer @ 产生n个winner后询问直接结束
---@field public accept_cancel? boolean @ 是否将取消也算作是收到 ---@field public accept_cancel? boolean @ 是否将取消也算作是收到肯定答
---@field public ai_start_time integer? @ 只剩AI思考开始的时间微秒delay专用 ---@field public ai_start_time integer? @ 只剩AI思考开始的时间微秒delay专用
---@field public timeout? integer @ 本次耗时(秒),默认为房间内配置的出手时间 ---@field public timeout? integer @ 本次耗时(秒),默认为房间内配置的出手时间
---@field public command string @ command自然就是command ---@field public command string @ command自然就是command
---@field public data table<integer, any> @ 每个player对应的询问数据 ---@field public data table<integer, any> @ 每个player对应的询问数据
---@field public default_reply table<integer, any> @ 玩家id - 默认回复内容 ---@field public default_reply table<integer, any> @ 玩家id - 默认回复内容 默认空串
---@field public send_json boolean? @ 是否需要对data使用json.encode默认true ---@field public send_encode boolean? @ 是否需要对data使用json.encode默认true
---@field public receive_json boolean? @ 是否需要对reply使用json.decode默认true ---@field public receive_decode boolean? @ 是否需要对reply使用json.decode默认true
---@field private send_success table<fk.ServerPlayer, boolean> @ 数据是否发送成功不成功的后面全部视为AI ---@field private send_success table<fk.ServerPlayer, boolean> @ 数据是否发送成功不成功的后面全部视为AI
---@field public result table<integer, any> @ 玩家id - 回复内容 nil表示完全未回复 ---@field public result table<integer, any> @ 玩家id - 回复内容 nil表示完全未回复
---@field public winners ServerPlayer[] @ 按肯定回复先后顺序排序 由于有概率所有人烧条 可能会空
---@field public luck_data any? @ 是否是询问手气卡 TODO: 有需求的话把这个通用化一点 ---@field public luck_data any? @ 是否是询问手气卡 TODO: 有需求的话把这个通用化一点
---@field private pending_requests table<fk.ServerPlayer, integer[]> @ 一控多时暂存的请求 ---@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") local Request = class("Request")
-- TODO: 懒得思考了 -- TODO: 懒得思考了
@ -23,9 +27,10 @@ local Request = class("Request")
-- 若还能再用一次那就重新发Request并继续等 -- 若还能再用一次那就重新发Request并继续等
---@param command string ---@param command string
---@param players ServerPlayer[] ---@param players ServerPlayer|ServerPlayer[]
---@param n? integer ---@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) assert(#players > 0)
self.command = command self.command = command
self.players = players self.players = players
@ -36,29 +41,43 @@ function Request:initialize(command, players, n)
self.room = room self.room = room
self.data = {} self.data = {}
self.default_reply = {} self.default_reply = {}
for _, p in ipairs(players) do self.default_reply[p.id] = "__cancel" end
self.timestamp = math.ceil(os.getms() / 1000) self.timestamp = math.ceil(os.getms() / 1000)
self.timeout = room.timeout self.timeout = room.timeout
self.send_json = true self.send_encode = true
self.receive_json = true -- 除了几个特殊字符串之外都decode self.receive_decode = true -- 除了几个特殊字符串之外都decode
self.pending_requests = setmetatable({}, { __mode = "k" }) self.pending_requests = setmetatable({}, { __mode = "k" })
self.send_success = setmetatable({}, { __mode = "k" }) self.send_success = setmetatable({}, { __mode = "k" })
self.result = {} self.result = {}
self.winners = {}
end end
function Request:__tostring() function Request:__tostring()
return "<Request>" return string.format("<Request '%s'>", self.command)
end end
---@param player ServerPlayer
---@param data any
function Request:setData(player, data) function Request:setData(player, data)
self.data[player.id] = data self.data[player.id] = data
end end
---@param player ServerPlayer
---@param data any @ 注意不要json.encode
function Request:setDefaultReply(player, data) function Request:setDefaultReply(player, data)
self.default_reply[player.id] = data self.default_reply[player.id] = data
end 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 -- 将相应请求数据发给player
-- 不能向thinking中的玩家发送这种情况下暂存起来等待收到答复后 -- 不能向thinking中的玩家发送这种情况下暂存起来等待收到答复后
---@param player ServerPlayer ---@param player ServerPlayer
@ -86,7 +105,7 @@ function Request:_sendPacket(player)
-- 发送请求数据并将控制者标记为烧条中 -- 发送请求数据并将控制者标记为烧条中
local jsonData = self.data[player.id] 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++代码中 -- FIXME: 这里确认数据是否发送的环节一定要写在C++代码中
self.send_success[controller] = controller:getState() == fk.Player_Online self.send_success[controller] = controller:getState() == fk.Player_Online
controller:doRequest(self.command, jsonData, self.timeout, self.timestamp) controller:doRequest(self.command, jsonData, self.timeout, self.timestamp)
@ -142,34 +161,21 @@ function Request:_checkReply(player, use_ai)
if use_ai then if use_ai then
player.ai.command = self.command player.ai.command = self.command
-- FIXME: 后面进行SmartAI的时候准备爆破此处 -- FIXME: 后面进行SmartAI的时候准备爆破此处
-- player.ai.data = self.data[player.id] player.ai.data = self.data[player.id]
player.ai.jsonData = self.data[player.id] reply = Pcall(player.ai.makeReply, player.ai)
if player.ai:isInstanceOf(RandomAI) then
reply = "__cancel"
else
reply = player.ai:makeReply()
end
else else
-- 还没轮到AI呢所以需要标记为未答复 -- 还没轮到AI呢所以需要标记为未答复
reply = "__notready" reply = "__notready"
end end
end end
if reply == '' then reply = '__cancel' end
return reply return reply
end 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() function Request:ask()
if self._asked then return end
local room = self.room local room = self.room
-- 0. 设置计时器,防止因无人回复一直等下去 -- 0. 设置计时器,防止因无人回复一直等下去
room.room:setRequestTimer(self.timeout * 1000 + 500) room.room:setRequestTimer(self.timeout * 1000 + 500)
@ -183,6 +189,10 @@ function Request:ask()
p.serverplayer:setThinking(false) p.serverplayer:setThinking(false)
end end
-- 发送focus
room:notifyMoveFocus(self.focus_players or self.players, self.focus_text or self.command,
math.floor(self.timeout * 1000))
-- 1. 向所有人发送询问请求 -- 1. 向所有人发送询问请求
for _, p in ipairs(players) do for _, p in ipairs(players) do
self:_sendPacket(p) self:_sendPacket(p)
@ -191,10 +201,9 @@ function Request:ask()
-- 2. 进入循环等待结束条件为已有n个回复或者超时或者有人点了 -- 2. 进入循环等待结束条件为已有n个回复或者超时或者有人点了
-- 若很多人都取消了导致最多回复数达不到n了那么也结束 -- 若很多人都取消了导致最多回复数达不到n了那么也结束
local replied_players = 0 local replied_players = 0
local ready_players = 0
while true do while true do
local changed = false local changed = false
-- 判断1若投降则直接结束全部询问,若超时则踢掉所有人类玩家(这样AI还可计算 -- 若投降则直接结束全部询问,若超时则踢掉所有人类玩家(AI还可计算
if room.hasSurrendered then break end if room.hasSurrendered then break end
local elapsed = os.time() - currentTime local elapsed = os.time() - currentTime
if self.timeout - elapsed <= 0 or resume_reason == "request_timer" then if self.timeout - elapsed <= 0 or resume_reason == "request_timer" then
@ -205,22 +214,23 @@ function Request:ask()
end end
end end
-- 若players中只剩人机那么允许人机进行计算
if table.every(players, function(p) if table.every(players, function(p)
return p.serverplayer:getState() ~= fk.Player_Online or not return p.serverplayer:getState() ~= fk.Player_Online or not
self.send_success[p.serverplayer] self.send_success[p.serverplayer]
end) then end) then
self.ai_start_time = os.getms() self.ai_start_time = os.getms()
end end
-- 判断2收到足够多回复了
local use_ai = self.ai_start_time ~= nil local use_ai = self.ai_start_time ~= nil
-- 轮询所有参与回答的玩家,如果作出了答复,那么就把他从名单移除;
-- 然后如果作出的是“肯定”答复那么添加到winner里面
for i = #players, 1, -1 do for i = #players, 1, -1 do
local player = players[i] local player = players[i]
local reply = self:_checkReply(player, use_ai) local reply = self:_checkReply(player, use_ai)
if reply ~= "__notready" then 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) reply = json.decode(reply)
end end
self.result[player.id] = reply self.result[player.id] = reply
@ -229,35 +239,39 @@ function Request:ask()
changed = true changed = true
if reply ~= "__cancel" or self.accept_cancel then if reply ~= "__cancel" or self.accept_cancel then
ready_players = ready_players + 1 table.insert(self.winners, player)
if ready_players >= self.n then if #self.winners >= self.n then
for _, p in ipairs(self.players) do -- winner数量已经足够剩下的人不用算了
for _, p in ipairs(players) do
-- 避免触发后续的烧条检测 -- 避免触发后续的烧条检测
if self.result[p.id] == nil then if self.result[p.id] == nil then
self.result[p.id] = "__failed_in_race" self.result[p.id] = "__failed_in_race"
end end
end end
players = {} -- 清空参与者名单
break -- 注意外面还有一层循环 break -- 注意外面还有一层循环
end end
end end
end end
end end
if #players + ready_players < self.n then break end if #players == 0 then break end
if ready_players >= self.n then break end if #self.winners >= self.n then break end
-- 防止万一如果AI算完后还是有机器人notready的话也别等了 -- 防止万一如果AI算完后还是有机器人notready的话也别等了
-- 不然就永远别想被唤醒了 -- 不然就永远别想被唤醒了
if self.ai_start_time then break end if self.ai_start_time then break end
-- 需要等待呢,等待被唤醒吧 -- 需要等待呢,等待被唤醒吧,唤醒后继续下一次轮询检测
if not changed then if not changed then
resume_reason = coroutine.yield("__handleRequest") resume_reason = coroutine.yield("__handleRequest")
end end
end end
room.room:destroyRequestTimer() room.room:destroyRequestTimer()
self:finish() self:_finish()
self._asked = true
end end
local function surrenderCheck(room) local function surrenderCheck(room)
@ -283,15 +297,19 @@ local function surrenderCheck(room)
end end
-- 善后工作主要是result规范化、投降检测等 -- 善后工作主要是result规范化、投降检测等
function Request:finish() function Request:_finish()
local room = self.room local room = self.room
surrenderCheck(room) surrenderCheck(room)
-- FIXME: 这里QML中有个bug这个命令应该是用来暗掉玩家面板的
-- room:doBroadcastNotify("CancelRequest", "")
for _, p in ipairs(self.players) do for _, p in ipairs(self.players) do
p.serverplayer:setThinking(false) 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 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 p._timewaste_count = p._timewaste_count + 1
if p._timewaste_count >= 3 and p.serverplayer:getState() == fk.Player_Online then if p._timewaste_count >= 3 and p.serverplayer:getState() == fk.Player_Online then
p._timewaste_count = 0 p._timewaste_count = 0
@ -301,10 +319,7 @@ function Request:finish()
p._timewaste_count = 0 p._timewaste_count = 0
end end
if self.result[p.id] == "__cancel" then if self.result[p.id] == "__cancel" then
self.result[p.id] = "" self.result[p.id] = (not self.accept_cancel) and self.default_reply[p.id] or ""
end
if self.result[p.id] == "__failed_in_race" then
self.result[p.id] = nil
end end
end end
room.last_request = self room.last_request = self
@ -312,7 +327,7 @@ function Request:finish()
for _, isHuman in pairs(self.send_success) do for _, isHuman in pairs(self.send_success) do
if not self.ai_start_time then break end if not self.ai_start_time then break end
if not isHuman then 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) room:delay(to_delay)
break break
end end

View File

@ -15,18 +15,20 @@
---@field public game_finished boolean @ 游戏是否已经结束 ---@field public game_finished boolean @ 游戏是否已经结束
---@field public tag table<string, any> @ Tag清单其实跟Player的标记是差不多的东西 ---@field public tag table<string, any> @ Tag清单其实跟Player的标记是差不多的东西
---@field public general_pile string[] @ 武将牌堆,这是可用武将名的数组 ---@field public general_pile string[] @ 武将牌堆,这是可用武将名的数组
---@field public disabled_packs string[] @ 未开启的扩展包名(是小包名,不是大包名)
---@field public logic GameLogic @ 这个房间使用的游戏逻辑,可能根据游戏模式而变动 ---@field public logic GameLogic @ 这个房间使用的游戏逻辑,可能根据游戏模式而变动
---@field public request_queue table<userdata, table> ---@field public request_queue table<userdata, table>
---@field public request_self table<integer, integer> ---@field public request_self table<integer, integer>
---@field public last_request Request @ 上一次完成的request ---@field public last_request Request @ 上一次完成的request
---@field public skill_costs table<string, any> @ 存放skill.cost_data用 ---@field public skill_costs table<string, any> @ 存放skill.cost_data用
---@field public card_marks table<integer, any> @ 存放card.mark之用 ---@field public card_marks table<integer, any> @ 存放card.mark之用
---@field public current_cost_skill TriggerSkill? @ AI用
local Room = AbstractRoom:subclass("Room") local Room = AbstractRoom:subclass("Room")
-- load classes used by the game -- load classes used by the game
Request = require "server.network" Request = require "server.network"
GameEvent = require "server.gameevent" GameEvent = require "server.gameevent"
local GameEventWrappers = require "lua/server/events" GameEventWrappers = require "lua/server/events"
Room:include(GameEventWrappers) Room:include(GameEventWrappers)
GameLogic = require "server.gamelogic" GameLogic = require "server.gamelogic"
ServerPlayer = require "server.serverplayer" ServerPlayer = require "server.serverplayer"
@ -508,10 +510,11 @@ function Room:setDeputyGeneral(player, general)
self:notifyProperty(player, player, "deputyGeneral") self:notifyProperty(player, player, "deputyGeneral")
end end
--- 为角色设置武将,并从武将池中抽出,若有隐匿技变为隐匿将。注意此时不会进行选择势力,请随后自行处理
---@param player ServerPlayer ---@param player ServerPlayer
---@param general string ---@param general string @ 主将名
---@param deputy string ---@param deputy? string @ 副将名
---@param broadcast boolean|nil ---@param broadcast? boolean @ 是否公示,默认否
function Room:prepareGeneral(player, general, deputy, broadcast) function Room:prepareGeneral(player, general, deputy, broadcast)
self:findGeneral(general) self:findGeneral(general)
self:findGeneral(deputy) self:findGeneral(deputy)
@ -597,9 +600,9 @@ end
function Room:doRequest(player, command, jsonData, wait) function Room:doRequest(player, command, jsonData, wait)
-- fk.qCritical("Room:doRequest is deprecated!") -- fk.qCritical("Room:doRequest is deprecated!")
if wait == true then error("wait can't be true") end if wait == true then error("wait can't be true") end
local request = Request:new(command, {player}) local request = Request:new(player, command)
request.send_json = false -- 因为参数已经json.encode过了该死的兼容性 request.send_encode = false -- 因为参数已经json.encode过了该死的兼容性
request.receive_json = false request.receive_decode = false
request.accept_cancel = true request.accept_cancel = true
request:setData(player, jsonData) request:setData(player, jsonData)
request:ask() request:ask()
@ -613,9 +616,9 @@ end
function Room:doBroadcastRequest(command, players, jsonData) function Room:doBroadcastRequest(command, players, jsonData)
-- fk.qCritical("Room:doBroadcastRequest is deprecated!") -- fk.qCritical("Room:doBroadcastRequest is deprecated!")
players = players or self.players players = players or self.players
local request = Request:new(command, players) local request = Request:new(players, command)
request.send_json = false -- 因为参数已经json.encode过了 request.send_encode = false -- 因为参数已经json.encode过了
request.receive_json = false request.receive_decode = false
request.accept_cancel = true request.accept_cancel = true
for _, p in ipairs(players) do for _, p in ipairs(players) do
request:setData(p, jsonData or p.request_data) request:setData(p, jsonData or p.request_data)
@ -635,14 +638,14 @@ end
function Room:doRaceRequest(command, players, jsonData) function Room:doRaceRequest(command, players, jsonData)
-- fk.qCritical("Room:doRaceRequest is deprecated!") -- fk.qCritical("Room:doRaceRequest is deprecated!")
players = players or self.players players = players or self.players
local request = Request:new(command, players, 1) local request = Request:new(players, command, 1)
request.send_json = false -- 因为参数已经json.encode过了 request.send_encode = false -- 因为参数已经json.encode过了
request.receive_json = false request.receive_decode = false
for _, p in ipairs(players) do for _, p in ipairs(players) do
request:setData(p, jsonData or p.request_data) request:setData(p, jsonData or p.request_data)
end end
request:ask() request:ask()
return request:getWinners()[1] return request.winners[1]
end end
--- 延迟一段时间。 --- 延迟一段时间。
@ -656,6 +659,15 @@ function Room:delay(ms)
coroutine.yield("__handleRequest", ms) coroutine.yield("__handleRequest", ms)
end 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 players? ServerPlayer[] @ 要被告知的玩家列表,默认为全员
---@param card_moves CardsMoveStruct[] @ 要告知的移牌信息列表 ---@param card_moves CardsMoveStruct[] @ 要告知的移牌信息列表
@ -691,7 +703,8 @@ end
--- 形象点说,就是在那些玩家下面显示一个“弃牌 思考中...”之类的烧条提示。 --- 形象点说,就是在那些玩家下面显示一个“弃牌 思考中...”之类的烧条提示。
---@param players ServerPlayer | ServerPlayer[] @ 要获得焦点的一名或者多名角色 ---@param players ServerPlayer | ServerPlayer[] @ 要获得焦点的一名或者多名角色
---@param command string @ 烧条的提示文字 ---@param command string @ 烧条的提示文字
function Room:notifyMoveFocus(players, command) ---@param timeout integer? @ focus的烧条时长
function Room:notifyMoveFocus(players, command, timeout)
if (players.class) then if (players.class) then
players = {players} players = {players}
end end
@ -712,7 +725,8 @@ function Room:notifyMoveFocus(players, command)
self:doBroadcastNotify("MoveFocus", json.encode{ self:doBroadcastNotify("MoveFocus", json.encode{
ids, ids,
command command,
timeout
}) })
end end
@ -896,18 +910,20 @@ function Room:askForUseActiveSkill(player, skill_name, prompt, cancelable, extra
end end
local command = "AskForUseActiveSkill" 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} local data = {skill_name, prompt, cancelable, extra_data}
Fk.currentResponseReason = extra_data.skillName 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 Fk.currentResponseReason = nil
if result == "" then if result == "" then
return false return false
end end
data = json.decode(result) data = result
local card = data.card local card = data.card
local targets = data.targets local targets = data.targets
local card_data = json.decode(card) local card_data = json.decode(card)
@ -969,7 +985,7 @@ function Room:askForDiscard(player, minNum, maxNum, includeEquip, skillName, can
return false return false
end end
end end
if skillName == "game_rule" then if skillName == "phase_discard" then
status_skills = Fk:currentRoom().status_skills[MaxCardsSkill] or Util.DummyTable status_skills = Fk:currentRoom().status_skills[MaxCardsSkill] or Util.DummyTable
for _, skill in ipairs(status_skills) do for _, skill in ipairs(status_skills) do
if skill:excludeFrom(player, card) then if skill:excludeFrom(player, card) then
@ -1213,7 +1229,7 @@ end
---@param minNum? integer @ 最少交出的卡牌数默认0 ---@param minNum? integer @ 最少交出的卡牌数默认0
---@param maxNum? integer @ 最多交出的卡牌数,默认所有牌 ---@param maxNum? integer @ 最多交出的卡牌数,默认所有牌
---@param prompt? string @ 询问提示信息 ---@param prompt? string @ 询问提示信息
---@param expand_pile? string @ 可选私人牌堆名称,如要分配你武将牌上的牌请填写 ---@param expand_pile? string|integer[] @ 可选私人牌堆名称,如要分配你武将牌上的牌请填写
---@param skipMove? boolean @ 是否跳过移动。默认不跳过 ---@param skipMove? boolean @ 是否跳过移动。默认不跳过
---@param single_max? integer|table @ 限制每人能获得的最大牌数。输入整数或(以角色id为键以整数为值)的表 ---@param single_max? integer|table @ 限制每人能获得的最大牌数。输入整数或(以角色id为键以整数为值)的表
---@return table<integer, integer[]> @ 返回一个表键为角色id转字符串值为分配给其的牌id数组 ---@return table<integer, integer[]> @ 返回一个表键为角色id转字符串值为分配给其的牌id数组
@ -1346,7 +1362,7 @@ function Room:returnToGeneralPile(g, position)
end end
--- 抽特定名字的武将(抽了就没了) --- 抽特定名字的武将(抽了就没了)
---@param name string @ 武将name如找不到则查找truename再找不到则返回nil ---@param name string? @ 武将name如找不到则查找truename再找不到则返回nil
---@return string? @ 抽出的武将名 ---@return string? @ 抽出的武将名
function Room:findGeneral(name) function Room:findGeneral(name)
if not Fk.generals[name] then return nil end if not Fk.generals[name] then return nil end
@ -1384,25 +1400,18 @@ end
---@return string|string[] @ 选择的武将 ---@return string|string[] @ 选择的武将
function Room:askForGeneral(player, generals, n, noConvert) function Room:askForGeneral(player, generals, n, noConvert)
local command = "AskForGeneral" local command = "AskForGeneral"
self:notifyMoveFocus(player, command)
n = n or 1 n = n or 1
if #generals == n then return n == 1 and generals[1] or generals end if #generals == n then return n == 1 and generals[1] or generals end
local defaultChoice = table.random(generals, n) local defaultChoice = table.random(generals, n)
if (player.serverplayer:getState() == fk.Player_Online) then local req = Request:new(player, command)
local result = self:doRequest(player, command, json.encode{ generals, n, noConvert }) local data = { generals, n, noConvert }
local choices req:setData(player, data)
if result == "" then req:setDefaultReply(player, defaultChoice)
choices = defaultChoice local choices = req:getResult(player)
else if #choices == 1 then return choices[1] end
choices = json.decode(result) return choices
end
if #choices == 1 then return choices[1] end
return choices
end
return n == 1 and defaultChoice[1] or defaultChoice
end end
--- 询问玩家若为神将、双势力需选择一个势力。 --- 询问玩家若为神将、双势力需选择一个势力。
@ -1414,7 +1423,9 @@ function Room:askForChooseKingdom(players)
end) end)
if #specialKingdomPlayers > 0 then 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 for _, p in ipairs(specialKingdomPlayers) do
local allKingdoms = {} local allKingdoms = {}
local curGeneral = Fk.generals[p.general] local curGeneral = Fk.generals[p.general]
@ -1424,25 +1435,13 @@ function Room:askForChooseKingdom(players)
allKingdoms = Fk:getKingdomMap(p.kingdom) allKingdoms = Fk:getKingdomMap(p.kingdom)
end end
if #allKingdoms > 0 then if #allKingdoms > 0 then
choiceMap[p.id] = allKingdoms req:setData(p, { allKingdoms, allKingdoms, "AskForKingdom", "#ChooseInitialKingdom" })
req:setDefaultReply(p, allKingdoms[1])
local data = json.encode({ allKingdoms, allKingdoms, "AskForKingdom", "#ChooseInitialKingdom" })
p.request_data = data
end end
end end
self:notifyMoveFocus(players, "AskForKingdom")
self:doBroadcastRequest("AskForChoice", specialKingdomPlayers)
for _, p in ipairs(specialKingdomPlayers) do for _, p in ipairs(specialKingdomPlayers) do
local kingdomChosen p.kingdom = req:getResult(p)
if p.reply_ready then
kingdomChosen = p.client_reply
else
kingdomChosen = choiceMap[p.id][1]
end
p.kingdom = kingdomChosen
self:notifyProperty(p, p, "kingdom") self:notifyProperty(p, p, "kingdom")
end end
end end
@ -1458,9 +1457,10 @@ end
function Room:askForCardChosen(chooser, target, flag, reason, prompt) function Room:askForCardChosen(chooser, target, flag, reason, prompt)
local command = "AskForCardChosen" local command = "AskForCardChosen"
prompt = prompt or "" prompt = prompt or ""
self:notifyMoveFocus(chooser, command)
local data = {target.id, flag, reason, prompt} 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 if result == "" then
local areas = {} local areas = {}
@ -1478,8 +1478,6 @@ function Room:askForCardChosen(chooser, target, flag, reason, prompt)
end end
if #handcards == 0 then return end if #handcards == 0 then return end
result = handcards[math.random(1, #handcards)] result = handcards[math.random(1, #handcards)]
else
result = tonumber(result)
end end
if result == -1 then if result == -1 then
@ -1507,18 +1505,20 @@ function Room:askForPoxi(player, poxi_type, data, extra_data, cancelable)
if not poxi then return {} end if not poxi then return {} end
local command = "AskForPoxi" local command = "AskForPoxi"
self:notifyMoveFocus(player, poxi_type) local req = Request:new(player, command)
local result = self:doRequest(player, command, json.encode { req.focus_text = poxi_type
req:setData(player, {
type = poxi_type, type = poxi_type,
data = data, data = data,
extra_data = extra_data, extra_data = extra_data,
cancelable = (cancelable == nil) and true or cancelable cancelable = (cancelable == nil) and true or cancelable
}) })
local result = req:getResult(player)
if result == "" then if result == "" then
return poxi.default_choice(data, extra_data) return poxi.default_choice(data, extra_data)
else else
return poxi.post_select(json.decode(result), data, extra_data) return poxi.post_select(result, data, extra_data)
end end
end end
@ -1535,7 +1535,7 @@ end
---@return integer[] @ 选择的id ---@return integer[] @ 选择的id
function Room:askForCardsChosen(chooser, target, min, max, flag, reason, prompt) function Room:askForCardsChosen(chooser, target, min, max, flag, reason, prompt)
if min == 1 and max == 1 then if min == 1 and max == 1 then
return { self:askForCardChosen(chooser, target, flag, reason) } return { self:askForCardChosen(chooser, target, flag, reason, prompt) }
end end
local cards local cards
@ -1607,10 +1607,15 @@ function Room:askForChoice(player, choices, skill_name, prompt, detailed, all_ch
local command = "AskForChoice" local command = "AskForChoice"
prompt = prompt or "" prompt = prompt or ""
all_choices = all_choices or choices 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 choices, all_choices, skill_name, prompt, detailed
}) })
local result = req:getResult(player)
if result == "" then if result == "" then
if table.contains(choices, "Cancel") then if table.contains(choices, "Cancel") then
result = "Cancel" 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 if #choices <= minNum and not all_choices and not cancelable then return choices end
assert(minNum <= maxNum) assert(minNum <= maxNum)
assert(not all_choices or table.every(choices, function(c) return table.contains(all_choices, c) end)) assert(not all_choices or table.every(choices, function(c) return table.contains(all_choices, c) end))
local command = "AskForChoices" local command = "AskForChoices"
skill_name = skill_name or "" skill_name = skill_name or ""
prompt = prompt or "" prompt = prompt or ""
all_choices = all_choices or choices all_choices = all_choices or choices
detailed = detailed or false 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 choices, all_choices, {minNum, maxNum}, cancelable, skill_name, prompt, detailed
}) })
local result = req:getResult(player)
if result == "" then if result == "" then
if cancelable then if cancelable then
return {} return {}
@ -1653,7 +1662,7 @@ function Room:askForChoices(player, choices, minNum, maxNum, skill_name, prompt,
return table.random(choices, math.min(minNum, #choices)) return table.random(choices, math.min(minNum, #choices))
end end
end end
return json.decode(result) return result
end end
--- 询问玩家是否发动技能。 --- 询问玩家是否发动技能。
@ -1664,11 +1673,11 @@ end
---@return boolean ---@return boolean
function Room:askForSkillInvoke(player, skill_name, data, prompt) function Room:askForSkillInvoke(player, skill_name, data, prompt)
local command = "AskForSkillInvoke" local command = "AskForSkillInvoke"
self:notifyMoveFocus(player, skill_name) local req = Request:new(player, command)
local invoked = false req.focus_text = skill_name
local result = self:doRequest(player, command, json.encode{ skill_name, prompt }) req.receive_decode = false -- 这个返回的都是"1" 不用decode
if result ~= "" then invoked = true end req:setData(player, { skill_name, prompt })
return invoked return req:getResult(player) ~= ""
end end
-- 获取使用牌的合法额外目标(【借刀杀人】等带副目标的卡牌除外) -- 获取使用牌的合法额外目标(【借刀杀人】等带副目标的卡牌除外)
@ -1795,7 +1804,9 @@ function Room:askForArrangeCards(player, skillname, cardMap, prompt, free_arrang
poxi_type = poxi_type or "", poxi_type = poxi_type or "",
cancelable = ((pattern ~= "." or poxi_type ~= "") and (default_choice == nil)) 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, -- local result = player.room:askForCustomDialog(player, skillname,
-- "RoomElement/ArrangeCardsBox.qml", { -- "RoomElement/ArrangeCardsBox.qml", {
-- cardMap, prompt, box_size, max_limit, min_limit, free_arrange or false, areaNames, -- 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 end
return cardMap return cardMap
end end
return json.decode(result) return result
end end
-- TODO: guanxing type -- TODO: guanxing type
@ -1867,7 +1878,6 @@ function Room:askForGuanxing(player, cards, top_limit, bottom_limit, customNotif
areaNames = { "Top", "Bottom" } areaNames = { "Top", "Bottom" }
end end
local command = "AskForGuanxing" local command = "AskForGuanxing"
self:notifyMoveFocus(player, customNotify or command)
local max_top = top_limit[2] local max_top = top_limit[2]
local card_map = {} local card_map = {}
if max_top > 0 then if max_top > 0 then
@ -1888,10 +1898,13 @@ function Room:askForGuanxing(player, cards, top_limit, bottom_limit, customNotif
bottom_area_name = areaNames[2], 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 local top, bottom
if result ~= "" then if result ~= "" then
local d = json.decode(result) local d = result
if top_limit[2] == 0 then if top_limit[2] == 0 then
top = Util.DummyTable top = Util.DummyTable
bottom = d[1] bottom = d[1]
@ -1942,15 +1955,17 @@ function Room:askForExchange(player, piles, piles_name, customNotify)
elseif x < 0 then elseif x < 0 then
piles_name = table.slice(piles_name, 1, #piles + 1) piles_name = table.slice(piles_name, 1, #piles + 1)
end end
self:notifyMoveFocus(player, customNotify or command)
local data = { local data = {
piles = piles, piles = piles,
piles_name = piles_name, 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 if result ~= "" then
local d = json.decode(result) return result
return d
else else
return piles return piles
end end
@ -1960,7 +1975,7 @@ end
---@param data string ---@param data string
---@return CardUseStruct ---@return CardUseStruct
function Room:handleUseCardReply(player, data) function Room:handleUseCardReply(player, data)
data = json.decode(data) -- data = json.decode(data)
local card = data.card local card = data.card
local targets = data.targets local targets = data.targets
if type(card) == "string" then if type(card) == "string" then
@ -2062,7 +2077,6 @@ function Room:askForUseCard(player, card_name, pattern, prompt, cancelable, extr
end end
local command = "AskForUseCard" local command = "AskForUseCard"
self:notifyMoveFocus(player, card_name)
cancelable = (cancelable == nil) and true or cancelable cancelable = (cancelable == nil) and true or cancelable
extra_data = extra_data or Util.DummyTable extra_data = extra_data or Util.DummyTable
prompt = prompt or "" prompt = prompt or ""
@ -2088,7 +2102,12 @@ function Room:askForUseCard(player, card_name, pattern, prompt, cancelable, extr
Fk.currentResponsePattern = pattern Fk.currentResponsePattern = pattern
self.logic:trigger(fk.HandleAskForPlayCard, nil, askForUseCardData, true) 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 askForUseCardData.afterRequest = true
self.logic:trigger(fk.HandleAskForPlayCard, nil, askForUseCardData, true) self.logic:trigger(fk.HandleAskForPlayCard, nil, askForUseCardData, true)
Fk.currentResponsePattern = nil Fk.currentResponsePattern = nil
@ -2124,7 +2143,6 @@ function Room:askForResponse(player, card_name, pattern, prompt, cancelable, ext
end end
local command = "AskForResponseCard" local command = "AskForResponseCard"
self:notifyMoveFocus(player, card_name)
cancelable = (cancelable == nil) and true or cancelable cancelable = (cancelable == nil) and true or cancelable
extra_data = extra_data or Util.DummyTable extra_data = extra_data or Util.DummyTable
pattern = pattern or card_name pattern = pattern or card_name
@ -2152,7 +2170,12 @@ function Room:askForResponse(player, card_name, pattern, prompt, cancelable, ext
Fk.currentResponsePattern = pattern Fk.currentResponsePattern = pattern
eventData.isResponse = true eventData.isResponse = true
self.logic:trigger(fk.HandleAskForPlayCard, nil, eventData, 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 eventData.afterRequest = true
self.logic:trigger(fk.HandleAskForPlayCard, nil, eventData, true) self.logic:trigger(fk.HandleAskForPlayCard, nil, eventData, true)
Fk.currentResponsePattern = nil Fk.currentResponsePattern = nil
@ -2206,8 +2229,6 @@ function Room:askForNullification(players, card_name, pattern, prompt, cancelabl
repeat repeat
useResult = nil useResult = nil
self:notifyMoveFocus(self.alive_players, card_name)
self:doBroadcastNotify("WaitForNullification", "")
local data = {card_name, pattern, prompt, cancelable, extra_data, disabledSkillNames} 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, eventData = effectData,
} }
self.logic:trigger(fk.HandleAskForPlayCard, nil, eventData, true) 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 eventData.afterRequest = true
self.logic:trigger(fk.HandleAskForPlayCard, nil, eventData, true) self.logic:trigger(fk.HandleAskForPlayCard, nil, eventData, true)
if winner then if winner then
local result = winner.client_reply local result = req:getResult(winner)
useResult = self:handleUseCardReply(winner, result) useResult = self:handleUseCardReply(winner, result)
if type(useResult) == "string" and useResult ~= "" then if type(useResult) == "string" and useResult ~= "" then
@ -2259,13 +2287,17 @@ function Room:askForAG(player, id_list, cancelable, reason)
end end
local command = "AskForAG" local command = "AskForAG"
self:notifyMoveFocus(player, reason or command)
local data = { id_list, cancelable, reason } 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 if ret == "" and not cancelable then
ret = table.random(id_list) ret = table.random(id_list)
end end
return tonumber(ret) return ret
end end
--- 给player发一条消息在他的窗口中用一系列卡牌填充一个AG。 --- 给player发一条消息在他的窗口中用一系列卡牌填充一个AG。
@ -2356,22 +2388,22 @@ function Room:askForMiniGame(players, focus, game_type, data_table)
local command = "MiniGame" local command = "MiniGame"
local game = Fk.mini_games[game_type] local game = Fk.mini_games[game_type]
if #players == 0 or not game then return end 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 for _, p in ipairs(players) do
local data = data_table[p.id] local data = data_table[p.id]
p.mini_game_data = { type = game_type, data = data } p.mini_game_data = { type = game_type, data = data }
p.request_data = json.encode(p.mini_game_data) req:setData(p, p.mini_game_data)
p.default_reply = game.default_choice and json.encode(game.default_choice(p, data)) or "" req:setDefaultReply(p, game.default_choice and json.encode(game.default_choice(p, data)))
end end
self:notifyMoveFocus(players, focus) req:ask()
self:doBroadcastRequest(command, players)
for _, p in ipairs(players) do for _, p in ipairs(players) do
p.mini_game_data = nil p.mini_game_data = nil
if not p.reply_ready then
p.client_reply = p.default_reply
p.reply_ready = true
end
end end
end end
@ -2386,11 +2418,14 @@ end
---@return string ---@return string
function Room:askForCustomDialog(player, focustxt, qmlPath, extra_data) function Room:askForCustomDialog(player, focustxt, qmlPath, extra_data)
local command = "CustomDialog" local command = "CustomDialog"
self:notifyMoveFocus(player, focustxt) local req = Request:new(player, command)
return self:doRequest(player, command, json.encode{ req.focus_text = focustxt
req.receive_decode = false -- 没法知道要不要decode所以我写false (json.decode该杀啊)
req:setData(player, {
path = qmlPath, path = qmlPath,
data = extra_data, data = extra_data,
}) })
return req:getResult(player)
end end
--- 询问移动场上的一张牌 --- 询问移动场上的一张牌
@ -2475,14 +2510,13 @@ function Room:askForMoveCardInBoard(player, targetOne, targetTwo, skillName, fla
playerIds = { targetOne.id, targetTwo.id } playerIds = { targetOne.id, targetTwo.id }
} }
local command = "AskForMoveCardInBoard" local command = "AskForMoveCardInBoard"
self:notifyMoveFocus(player, command) local req = Request:new(player, command)
local result = self:doRequest(player, command, json.encode(data)) req:setData(player, data)
local result = req:getResult(player)
if result == "" then if result == "" then
local randomIndex = math.random(1, #cards) local randomIndex = math.random(1, #cards)
result = { cardId = cards[randomIndex], pos = cardsPosition[randomIndex] } result = { cardId = cards[randomIndex], pos = cardsPosition[randomIndex] }
else
result = json.decode(result)
end end
local from, to local from, to
@ -2942,10 +2976,5 @@ function Room:removeTableMark(sth, mark, value)
end end
end end
function Room:__index(k)
if k == "room_settings" then
return self.settings
end
end
return Room return Room

View File

@ -1,6 +1,6 @@
-- SPDX-License-Identifier: GPL-3.0-or-later -- SPDX-License-Identifier: GPL-3.0-or-later
local Room = require "server.room" Room = require "server.room"
-- 所有当前正在运行的房间(即游戏尚未结束的房间) -- 所有当前正在运行的房间(即游戏尚未结束的房间)
---@type table<integer, Room> ---@type table<integer, Room>

View File

@ -30,13 +30,6 @@ function ServerPlayer:initialize(_self)
self.id = _self:getId() self.id = _self:getId()
self.room = nil 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.phases = {}
self.skipped_phases = {} self.skipped_phases = {}
self.phase_state = {} self.phase_state = {}
@ -47,7 +40,7 @@ function ServerPlayer:initialize(_self)
self._prelighted_skills = {} self._prelighted_skills = {}
self._timewaste_count = 0 self._timewaste_count = 0
self.ai = RandomAI:new(self) self.ai = SmartAI:new(self)
end end
---@param command string ---@param command string
@ -284,6 +277,9 @@ end
function ServerPlayer:play(phase_table) function ServerPlayer:play(phase_table)
phase_table = phase_table or {} phase_table = phase_table or {}
if #phase_table > 0 then 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 if not table.contains(phase_table, Player.NotActive) then
table.insert(phase_table, Player.NotActive) table.insert(phase_table, Player.NotActive)
end end
@ -384,17 +380,19 @@ function ServerPlayer:endCurrentPhase()
end end
--- 获得一个额外回合 --- 获得一个额外回合
---@param delay? boolean ---@param delay? boolean @ 是否延迟到当前回合结束再开启额外回合,默认是
---@param skillName? string ---@param skillName? string @ 额外回合原因
function ServerPlayer:gainAnExtraTurn(delay, skillName) ---@param turnData? TurnStruct @ 额外回合的信息
function ServerPlayer:gainAnExtraTurn(delay, skillName, turnData)
local room = self.room local room = self.room
delay = (delay == nil) and true or delay 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 if delay then
local logic = room.logic local turn = room.logic:getCurrentEvent():findParent(GameEvent.Turn, true)
local turn = logic:getCurrentEvent():findParent(GameEvent.Turn, true)
if turn then if turn then
turn:prependExitFunc(function() self:gainAnExtraTurn(false, skillName) end) turn:prependExitFunc(function() self:gainAnExtraTurn(false, skillName, turnData) end)
return return
end end
end end
@ -407,13 +405,15 @@ function ServerPlayer:gainAnExtraTurn(delay, skillName)
local current = room.current local current = room.current
room.current = self room.current = self
self.tag["_extra_turn_count"] = self.tag["_extra_turn_count"] or {} room:addTableMark(self, "_extra_turn_count", skillName)
local ex_tag = self.tag["_extra_turn_count"]
table.insert(ex_tag, 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 room.current = current
end end
@ -421,17 +421,14 @@ end
--- 当前是否处于额外的回合。 --- 当前是否处于额外的回合。
--- @return boolean --- @return boolean
function ServerPlayer:insideExtraTurn() function ServerPlayer:insideExtraTurn()
return self.tag["_extra_turn_count"] and #self.tag["_extra_turn_count"] > 0 return self:getCurrentExtraTurnReason() ~= "game_rule"
end end
--- 当前额外回合的技能原因。 --- 当前额外回合的技能原因。非额外回合则为game_rule
---@return string ---@return string
function ServerPlayer:getCurrentExtraTurnReason() function ServerPlayer:getCurrentExtraTurnReason()
local ex_tag = self.tag["_extra_turn_count"] local mark = self:getTableMark("_extra_turn_count")
if (not ex_tag) or #ex_tag == 0 then return mark[#mark] or "game_rule"
return "game_rule"
end
return ex_tag[#ex_tag]
end end
--- 角色摸牌。 --- 角色摸牌。
@ -461,6 +458,7 @@ function ServerPlayer:bury()
self:throwAllCards() self:throwAllCards()
self:throwAllMarks() self:throwAllMarks()
self:clearPiles() self:clearPiles()
self:onAllSkillLose()
self:reset() self:reset()
end end
@ -482,6 +480,12 @@ function ServerPlayer:throwAllCards(flag)
self.room:throwCard(cardIds, "", self) self.room:throwCard(cardIds, "", self)
end end
function ServerPlayer:onAllSkillLose()
for _, skill in ipairs(self:getAllSkills()) do
skill:onLose(self, true)
end
end
function ServerPlayer:throwAllMarks() function ServerPlayer:throwAllMarks()
for name, _ in pairs(self.mark) do for name, _ in pairs(self.mark) do
self.room:setPlayerMark(self, name, 0) self.room:setPlayerMark(self, name, 0)

View File

@ -246,6 +246,11 @@ fk.IceDamage = 4
---@field public skillName string @ 技能名 ---@field public skillName string @ 技能名
---@field public fromPlace "top"|"bottom" @ 摸牌的位置 ---@field public fromPlace "top"|"bottom" @ 摸牌的位置
---@--- TurnStruct 回合事件的数据
---@class TurnStruct
---@field public reason string? @ 当前额外回合的原因不为额外回合则为game_rule
---@field public phase_table? Phase[] @ 此回合将进行的阶段,填空则为正常流程
--- 移动理由 --- 移动理由
---@alias CardMoveReason integer ---@alias CardMoveReason integer
fk.ReasonJustMove = 1 fk.ReasonJustMove = 1

View File

@ -4,6 +4,11 @@ local SelectableItem = base.SelectableItem
---@class CardItem: SelectableItem ---@class CardItem: SelectableItem
local CardItem = SelectableItem:subclass("CardItem") 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 ---@class Photo: SelectableItem
---@field public state string ---@field public state string
local Photo = SelectableItem:subclass("Photo") local Photo = SelectableItem:subclass("Photo")

View File

@ -1,180 +1,10 @@
--[[ local slash = fk.ai_skills["slash_skill"]
fk.ai_card.thunder__slash = fk.ai_card.slash local just_use = fk.ai_skills["__just_use"]
fk.ai_use_play.thunder__slash = fk.ai_use_play.slash local use_to_enemy = fk.ai_skills["__use_to_enemy"]
fk.ai_card.fire__slash = fk.ai_card.slash SmartAI:setSkillAI("thunder__slash_skill", slash)
fk.ai_use_play.fire__slash = fk.ai_use_play.slash SmartAI:setSkillAI("fire__slash_skill", slash)
fk.ai_card.analeptic = { SmartAI:setSkillAI("analeptic_skill", just_use)
intention = 60, -- 身份值 SmartAI:setSkillAI("iron_chain_skill", just_use)
value = 5, -- 卡牌价值 SmartAI:setSkillAI("fire_attack_skill", use_to_enemy)
priority = 3 -- 使用优先值 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
--]]

View File

@ -1,55 +1,11 @@
-- aux_skill的AI文件。aux_skill的重量级程度无需多说。 SmartAI:setSkillAI("discard_skill", {
-- 这个文件说是第二个smart_ai.lua也不为过。 choose_targets = function(_, ai)
return ai:doOKButton()
end,
})
-- discard_skill: 弃牌相关AI SmartAI:setSkillAI("choose_cards_skill", {
----------------------------- choose_targets = function(_, ai)
return ai:doOKButton()
--- 弃牌相关判定函数的表。键为技能名,值为原型如下的函数。 end,
---@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

View File

@ -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: hujia
-- TODO: guicai 关于如何界定判定的好坏 需要向AI中单独说明 -- TODO: guicai 关于如何界定判定的好坏 需要向AI中单独说明
SmartAI:setSkillAI("fankui", enemy_damage_invoke)
fk.ai_skill_invoke["fankui"] = function(self) SmartAI:setSkillAI("ganglie", {
local room = self.room skill_invoke = function(skill, ai)
local logic = room.logic local room = ai.room
local logic = room.logic
-- 询问反馈时处于on_cost环节当前事件必是damage且有from local event = logic:getCurrentEvent()
local event = logic:getCurrentEvent() local dmg = event.data[1]
local dmg = event.data[1] return ai:isEnemy(dmg.from)
return self:isEnemy(dmg.from) end,
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) SmartAI:setSkillAI("tiandu", true_invoke)
return false SmartAI:setSkillAI("yiji", {
end skill_invoke = true,
-- ask_active = function
})
fk.ai_skill_invoke["tiandu"] = true SmartAI:setSkillAI("luoshen", true_invoke)
SmartAI:setSkillAI("qingguo", active_random_select_card)
-- TODO: yiji
fk.ai_skill_invoke["luoshen"] = true
-- TODO: qingguo
-- 蜀国 -- 蜀国
-- TODO: rende SmartAI:setSkillAI("rende", active_random_select_card)
SmartAI:setSkillAI("rende", use_to_friend)
-- TODO: jijiang -- TODO: jijiang
-- TODO: wusheng SmartAI:setSkillAI("wusheng", active_random_select_card)
-- TODO: guanxing -- TODO: guanxing
-- TODO: longdan -- TODO: longdan
SmartAI:setSkillAI("longdan", active_random_select_card)
fk.ai_skill_invoke["tieqi"] = function(self) SmartAI:setSkillAI("tieqi", {
local room = self.room skill_invoke = function(skill, ai)
local logic = room.logic local room = ai.room
local logic = room.logic
-- 询问反馈时处于on_cost环节当前事件必是damage且有from -- 询问反馈时处于on_cost环节当前事件必是damage且有from
local event = logic:getCurrentEvent() local event = logic:getCurrentEvent()
local use = event.data[1] ---@type CardUseStruct local use = event.data[1] ---@type CardUseStruct
return table.find(use.tos, function(t) return table.find(use.tos, function(t)
return self:isEnemy(room:getPlayerById(t[1])) return ai:isEnemy(room:getPlayerById(t[1]))
end) end)
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 -- 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 SmartAI:setSkillAI("fanjian", use_to_enemy)
-- TODO: guose
SmartAI:setSkillAI("guose", active_random_select_card)
-- TODO: liuli -- TODO: liuli
fk.ai_skill_invoke["lianying"] = true SmartAI:setSkillAI("lianying", true_invoke)
fk.ai_skill_invoke["xiaoji"] = true
-- 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: jijiu
-- TODO: wushuang SmartAI:setSkillAI("qingnang", active_random_select_card)
-- TODO: lijian -- TODO: lijian
fk.ai_skill_invoke["biyue"] = true SmartAI:setSkillAI("biyue", true_invoke)
--]=]

View File

@ -27,7 +27,7 @@ local discardSkill = fk.CreateActiveSkill{
return false return false
end end
end end
if Fk.currentResponseReason == "game_rule" then if Fk.currentResponseReason == "phase_discard" then
status_skills = Fk:currentRoom().status_skills[MaxCardsSkill] or Util.DummyTable status_skills = Fk:currentRoom().status_skills[MaxCardsSkill] or Util.DummyTable
for _, skill in ipairs(status_skills) do for _, skill in ipairs(status_skills) do
if skill:excludeFrom(Self, card) then if skill:excludeFrom(Self, card) then

View File

@ -1,14 +1,5 @@
-- SPDX-License-Identifier: GPL-3.0-or-later -- 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{ GameRule = fk.CreateTriggerSkill{
name = "game_rule", name = "game_rule",
@ -90,14 +81,11 @@ GameRule = fk.CreateTriggerSkill{
end, end,
[fk.BuryVictim] = function() [fk.BuryVictim] = function()
player:bury() 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 return false
end end
local damage = data.damage local damage = data.damage
if damage and damage.from then Fk.game_modes[room.settings.gameMode]:deathRewardAndPunish(player, damage and damage.from)
local killer = damage.from
rewardAndPunish(killer, player);
end
end, end,
default = function() default = function()
print("game_rule: Event=" .. event) print("game_rule: Event=" .. event)

View File

@ -222,7 +222,7 @@ Fk:loadTranslationTable({
["revealMain"] = "Reveal main character %arg", ["revealMain"] = "Reveal main character %arg",
["revealDeputy"] = "Reveal deputy character %arg", ["revealDeputy"] = "Reveal deputy character %arg",
["game_rule"] = "Discard", ["game_rule"] = "GameRule",
}, "en_US") }, "en_US")
-- init -- init

View File

@ -298,6 +298,8 @@ Fk:loadTranslationTable{
["lijian"] = "离间", ["lijian"] = "离间",
[":lijian"] = "出牌阶段限一次,你可以弃置一张牌并选择两名其他男性角色,后选择的角色视为对先选择的角色使用了一张不能被【无懈可击】的【决斗】。", [":lijian"] = "出牌阶段限一次,你可以弃置一张牌并选择两名其他男性角色,后选择的角色视为对先选择的角色使用了一张不能被【无懈可击】的【决斗】。",
["#lijian-active"] = "发动 离间,弃置一张手牌并选择两名其他男性角色,后选择的角色视为对先选择的角色使用了一张不能被【无懈可击】的【决斗】", ["#lijian-active"] = "发动 离间,弃置一张手牌并选择两名其他男性角色,后选择的角色视为对先选择的角色使用了一张不能被【无懈可击】的【决斗】",
["lijian_tip_1"] = "先出杀",
["lijian_tip_2"] = "后出杀",
["$biyue1"] = "失礼了~", ["$biyue1"] = "失礼了~",
["$biyue2"] = "羡慕吧~", ["$biyue2"] = "羡慕吧~",
["biyue"] = "闭月", ["biyue"] = "闭月",
@ -529,13 +531,23 @@ Fk:loadTranslationTable{
["ex__choose_skill"] = "选择", ["ex__choose_skill"] = "选择",
["distribution_select_skill"] = "分配", ["distribution_select_skill"] = "分配",
["choose_players_to_move_card_in_board"] = "选择角色", ["choose_players_to_move_card_in_board"] = "选择角色",
["AskForUseActiveSkill"] = "使用技能",
["AskForSkillInvoke"] = "发动技能",
["AskForUseCard"] = "使用",
["AskForResponseCard"] = "打出",
["AskForDiscard"] = "弃牌",
["AskForCardChosen"] = "选牌",
["AskForCardsChosen"] = "选牌",
["AskForPindian"] = "拼点",
["reveal_skill&"] = "亮将", ["reveal_skill&"] = "亮将",
["#reveal_skill&"] = "选择一个武将亮将(点击左侧选择框展开)", ["#reveal_skill&"] = "选择一个武将亮将(点击左侧选择框展开)",
[":reveal_skill&"] = "出牌阶段,你可明置一张有锁定技的武将。", [":reveal_skill&"] = "出牌阶段,你可明置一张有锁定技的武将。",
["revealMain"] = "明置主将 %arg", ["revealMain"] = "明置主将 %arg",
["revealDeputy"] = "明置副将 %arg", ["revealDeputy"] = "明置副将 %arg",
["game_rule"] = "弃牌阶段", ["game_rule"] = "游戏规则",
["replace_equip"] = "替换装备", ["replace_equip"] = "替换装备",
["#EquipmentChoice"] = "%arg", ["#EquipmentChoice"] = "%arg",
["#GameRuleReplaceEquipment"] = "请选择要置入的区域", ["#GameRuleReplaceEquipment"] = "请选择要置入的区域",

View File

@ -1097,6 +1097,14 @@ local lijian = fk.CreateActiveSkill{
} }
room:useCard(new_use) room:useCard(new_use)
end, 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{ local biyue = fk.CreateTriggerSkill{
name = "biyue", name = "biyue",
@ -1215,30 +1223,20 @@ local role_getlogic = function()
end end
local nonlord = room:getOtherPlayers(lord, true) local nonlord = room:getOtherPlayers(lord, true)
local req = Request:new(nonlord, "AskForGeneral")
local generals = table.random(room.general_pile, #nonlord * generalNum) local generals = table.random(room.general_pile, #nonlord * generalNum)
for i, p in ipairs(nonlord) do for i, p in ipairs(nonlord) do
local arg = table.slice(generals, (i - 1) * generalNum + 1, i * generalNum + 1) local arg = table.slice(generals, (i - 1) * generalNum + 1, i * generalNum + 1)
p.request_data = json.encode{ arg, n } req:setData(p, { arg, n })
p.default_reply = table.random(arg, n) req:setDefaultReply(p, table.random(arg, n))
end end
room:notifyMoveFocus(nonlord, "AskForGeneral")
room:doBroadcastRequest("AskForGeneral", nonlord)
for _, p in ipairs(nonlord) do for _, p in ipairs(nonlord) do
local general, deputy local result = req:getResult(p)
if p.general == "" and p.reply_ready then local general, deputy = result[1], result[2]
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(general)
room:findGeneral(deputy) room:findGeneral(deputy)
room:prepareGeneral(p, general, deputy) room:prepareGeneral(p, general, deputy)
p.default_reply = ""
end end
room:askForChooseKingdom(nonlord) room:askForChooseKingdom(nonlord)
@ -1252,6 +1250,7 @@ local role_mode = fk.CreateGameMode{
minPlayer = 2, minPlayer = 2,
maxPlayer = 8, maxPlayer = 8,
logic = role_getlogic, logic = role_getlogic,
main_mode = "role_mode",
is_counted = function(self, room) is_counted = function(self, room)
return #room.players >= 5 return #room.players >= 5
end, end,

View File

@ -1,87 +1,71 @@
-- TODO: 合法性的方便函数 SmartAI:setCardSkillAI("slash_skill", {
-- TODO: 关于如何选择多个目标 estimated_benefit = 120,
-- TODO: 关于装备牌
-- 基本牌:杀,闪,桃 on_effect = function(self, logic, effect)
self.skill:onEffect(logic, effect)
end,
})
---@param from ServerPlayer SmartAI:setTriggerSkillAI("#nioh_shield_skill", {
---@param to ServerPlayer correct_func = function(self, logic, event, target, player, data)
---@param card Card return self.skill:triggerable(event, target, player, data)
local function tgtValidator(from, to, card) end,
return not from:prohibitUse(card) and })
not from:isProhibited(to, card) and
true -- feasible
end
local function justUse(self, card_name, extra_data) --[=====[
local slashes = self:getCards(card_name, "use", extra_data) local just_use = {
if #slashes == 0 then return nil end name = "__just_use",
will_use = Util.TrueFunc,
choose_targets = function(skill, ai, card)
return ai:doOKButton()
end,
}
return self:buildUseReply(slashes[1].id) local use_to_friend = {
end 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 local use_to_enemy = {
---@param card_name string name = "__use_to_enemy",
local function useToEnemy(self, card_name, extra_data) will_use = Util.TrueFunc,
local slashes = self:getCards(card_name, "use", extra_data) choose_targets = function(skill, ai, card)
if #slashes == 0 then return nil end 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: 目标合法性 SmartAI:setSkillAI("__just_use", just_use)
local targets = {} SmartAI:setSkillAI("__use_to_enemy", use_to_enemy)
if self.enemies[1] then SmartAI:setSkillAI("__use_to_friend", use_to_friend)
table.insert(targets, self.enemies[1].id) SmartAI:setSkillAI("slash_skill", use_to_enemy)
else SmartAI:setSkillAI("dismantlement_skill", use_to_enemy)
return nil SmartAI:setSkillAI("snatch_skill", use_to_enemy)
end SmartAI:setSkillAI("duel_skill", use_to_enemy)
SmartAI:setSkillAI("indulgence_skill", use_to_enemy)
return self:buildUseReply(slashes[1].id, targets) SmartAI:setSkillAI("jink_skill", just_use)
end SmartAI:setSkillAI("peach_skill", just_use)
SmartAI:setSkillAI("ex_nihilo_skill", just_use)
fk.ai_use_card["slash"] = function(self, pattern, prompt, cancelable, extra_data) SmartAI:setSkillAI("savage_assault_skill", just_use)
return useToEnemy(self, "slash", extra_data) SmartAI:setSkillAI("archery_attack_skill", just_use)
end SmartAI:setSkillAI("god_salvation_skill", just_use)
SmartAI:setSkillAI("amazing_grace_skill", just_use)
fk.ai_use_card["jink"] = function(self, pattern, prompt, cancelable, extra_data) SmartAI:setSkillAI("lightning_skill", just_use)
return justUse(self, "jink", extra_data) SmartAI:setSkillAI("default_equip_skill", just_use)
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

View File

@ -26,10 +26,12 @@ Fk:loadTranslationTable{
["basic_char"] = "", ["basic_char"] = "",
["trick_char"] = "", ["trick_char"] = "",
["equip_char"] = "", ["equip_char"] = "",
["non_basic_char"] = "非基",
["basic"] = "基本牌", ["basic"] = "基本牌",
["trick"] = "锦囊牌", ["trick"] = "锦囊牌",
["equip"] = "装备牌", ["equip"] = "装备牌",
["non_basic"] = "非基本牌",
["weapon"] = "武器牌", ["weapon"] = "武器牌",
["armor"] = "防具牌", ["armor"] = "防具牌",
["defensive_horse"] = "防御坐骑牌", ["defensive_horse"] = "防御坐骑牌",

View File

@ -462,7 +462,6 @@ extension:addCards({
local nullificationSkill = fk.CreateActiveSkill{ local nullificationSkill = fk.CreateActiveSkill{
name = "nullification_skill", name = "nullification_skill",
can_use = Util.FalseFunc, can_use = Util.FalseFunc,
on_use = function() RoomInstance:delay(1200) end,
on_effect = function(self, room, effect) on_effect = function(self, room, effect)
if effect.responseToEvent then if effect.responseToEvent then
effect.responseToEvent.isCancellOut = true effect.responseToEvent.isCancellOut = true
@ -713,7 +712,7 @@ local lightningSkill = fk.CreateActiveSkill{
} }
room:moveCards{ room:moveCards{
ids = Card:getIdList(effect.card), ids = room:getSubcardsByRule(effect.card, { Card.Processing }),
toArea = Card.DiscardPile, toArea = Card.DiscardPile,
moveReason = fk.ReasonUse moveReason = fk.ReasonUse
} }

View File

@ -46,9 +46,19 @@ Client::~Client() {
} }
void Client::connectToHost(const QString &server, ushort port) { void Client::connectToHost(const QString &server, ushort port) {
start_connent_timestamp = QDateTime::currentMSecsSinceEpoch();
router->getSocket()->connectToHost(server, port); 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) { void Client::replyToServer(const QString &command, const QString &jsonData) {
int type = Router::TYPE_REPLY | Router::SRC_CLIENT | Router::DEST_SERVER; int type = Router::TYPE_REPLY | Router::SRC_CLIENT | Router::DEST_SERVER;
router->reply(type, command, jsonData); router->reply(type, command, jsonData);

View File

@ -17,6 +17,8 @@ public:
~Client(); ~Client();
void connectToHost(const QString &server, ushort port); 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 replyToServer(const QString &command, const QString &jsonData);
Q_INVOKABLE void notifyServer(const QString &command, const QString &jsonData); Q_INVOKABLE void notifyServer(const QString &command, const QString &jsonData);
@ -51,6 +53,8 @@ private:
Router *router; Router *router;
QMap<int, ClientPlayer *> players; QMap<int, ClientPlayer *> players;
ClientPlayer *self; ClientPlayer *self;
qint64 start_connent_timestamp; // 连接时的时间戳 单位毫秒
qint64 server_lag = 0; // 与服务器时差,单位毫秒,正数表示自己快了 负数表示慢了
lua_State *L; lua_State *L;
QFileSystemWatcher fsWatcher; QFileSystemWatcher fsWatcher;

View File

@ -212,6 +212,7 @@ void Server::setupPlayer(ServerPlayer *player, bool all_info) {
arr << player->getId(); arr << player->getId();
arr << player->getScreenName(); arr << player->getScreenName();
arr << player->getAvatar(); arr << player->getAvatar();
arr << QDateTime::currentMSecsSinceEpoch();
player->doNotify("Setup", JsonArray2Bytes(arr)); player->doNotify("Setup", JsonArray2Bytes(arr));
if (all_info) { if (all_info) {

View File

@ -18,6 +18,8 @@ extern QmlBackend *Backend;
%nodefaultdtor Client; %nodefaultdtor Client;
class Client : public QObject { class Client : public QObject {
public: public:
void setupServerLag(long long server_time);
void replyToServer(const QString &command, const QString &json_data); void replyToServer(const QString &command, const QString &json_data);
void notifyServer(const QString &command, const QString &json_data); void notifyServer(const QString &command, const QString &json_data);

View File

@ -63,7 +63,7 @@ public:
QVariant ret; QVariant ret;
if (high < 0) { if (high < 0) {
if (low < 1) { if (low < 1) {
ret.setValue($self->bounded(0, 100001) / 100000); ret.setValue(qreal($self->bounded(0, 100000001)) / 100000000);
} else { } else {
ret.setValue($self->bounded(1, low + 1)); ret.setValue($self->bounded(1, low + 1));
} }

View File

@ -289,16 +289,21 @@ QVariant QmlBackend::callLuaFunction(const QString &func_name,
if (!ClientInstance) return QVariantMap(); if (!ClientInstance) return QVariantMap();
lua_State *L = ClientInstance->getLuaState(); 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()); lua_getglobal(L, func_name.toLatin1().data());
foreach (QVariant v, params) { foreach (QVariant v, params) {
pushLuaValue(L, v); 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) { if (err) {
qCritical() << lua_tostring(L, -1); qCritical() << lua_tostring(L, -1);
lua_pop(L, 1); lua_pop(L, 2);
return QVariant(); return QVariant();
} }
auto result = readLuaValue(L); auto result = readLuaValue(L);
@ -649,7 +654,10 @@ QJsonObject QmlBackend::getRequestData() const {
auto router = ClientInstance->getRouter(); auto router = ClientInstance->getRouter();
obj["id"] = router->getRequestId(); obj["id"] = router->getRequestId();
obj["timeout"] = router->getTimeout(); obj["timeout"] = router->getTimeout();
obj["timestamp"] = router->getRequestTimestamp(); auto timestamp = router->getRequestTimestamp();
// 因为timestamp是服务器发来的时间如果自己比服务器的时钟快的话那么就得加上这个差值才行
timestamp += ClientInstance->getServerLag();
obj["timestamp"] = timestamp;
return obj; return obj;
} }