多选、勾选框和bugfix (#284)

- 修复了prompt看不见extra_data的bug
- 添加askForChooseBoth用以选择多牌多角色的情况
- 拆分Util以方便开发插件识别
- 大招不再显示武将卡面信息
- 添加勾选框askForCheck,用以提供多选项多选

---------

Co-authored-by: notify <notify-ctrl@qq.com>
This commit is contained in:
YoumuKon 2023-11-07 12:57:00 +08:00 committed by GitHub
parent 379ea06970
commit 513fcf36d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 462 additions and 7 deletions

View File

@ -0,0 +1,87 @@
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
Item {
property bool enabled: true
property bool triggered: false
property alias text: title.text
property alias textColor: title.color
property alias textFont: title.font
property alias backgroundColor: bg.color
property alias border: bg.border
property alias iconSource: icon.source
property int padding: 5
signal clicked
id: button
width: icon.width + title.implicitWidth + padding * 2
height: Math.max(icon.height, title.implicitHeight) + padding * 2
Rectangle {
id: bg
anchors.fill: parent
color: "black"
border.width: 2
border.color: "white"
opacity: 0.8
}
states: [
State {
name: "hovered_checked"; when: hover.hovered && triggered
PropertyChanges { target: bg; color: "gold" }
PropertyChanges { target: title; color: "black" }
},
State {
name: "hovered"; when: hover.hovered
PropertyChanges { target: bg; color: "white" }
PropertyChanges { target: title; color: "black" }
},
State {
name: "checked"; when: triggered
PropertyChanges { target: border; color: "gold" }
PropertyChanges { target: title; color: "gold" }
},
State {
name: "disabled"; when: !enabled
PropertyChanges { target: button; opacity: 0.2 }
}
]
TapHandler {
id: mouse
onTapped: if (parent.enabled) {
triggered = !triggered;
parent.clicked();
}
}
HoverHandler {
id: hover
cursorShape: Qt.PointingHandCursor
}
Row {
x: padding
y: padding
anchors.centerIn: parent
spacing: 5
Image {
id: icon
anchors.verticalCenter: parent.verticalCenter
fillMode: Image.PreserveAspectFit
}
Text {
id: title
font.pixelSize: 18
// font.family: "WenQuanYi Micro Hei"
anchors.verticalCenter: parent.verticalCenter
text: ""
color: "white"
}
}
}

View File

@ -1008,6 +1008,48 @@ callbacks["AskForChoice"] = (jsonData) => {
}); });
} }
callbacks["AskForCheck"] = (jsonData) => {
// jsonData: [ string[] choices, string skill ]
// TODO: multiple choices, e.g. benxi_ol
const data = JSON.parse(jsonData);
const choices = data[0];
const all_choices = data[1];
const min_num = data[2][0];
const max_num = data[2][1];
const cancelable = data[3];
const skill_name = data[4];
const prompt = data[5];
const detailed = data[6];
if (prompt === "") {
roomScene.promptText = Backend.translate("#AskForCheck")
.arg(Backend.translate(skill_name));
} else {
roomScene.setPrompt(processPrompt(prompt), true);
}
roomScene.state = "replying";
let qmlSrc;
if (!detailed) {
qmlSrc = "../RoomElement/CheckBox.qml";
} else {
qmlSrc = "../RoomElement/DetailedCheckBox.qml";
}
roomScene.popupBox.sourceComponent = Qt.createComponent(qmlSrc);
const box = roomScene.popupBox.item;
box.options = choices;
box.skill_name = skill_name;
box.all_options = all_choices;
box.min_num = min_num;
box.max_num = max_num;
box.cancelable = cancelable;
box.accepted.connect(() => {
const ret = [];
box.result.forEach(id => {
ret.push(all_choices[id]);
});
replyToServer(JSON.stringify(ret));
});
}
callbacks["AskForCardChosen"] = (jsonData) => { callbacks["AskForCardChosen"] = (jsonData) => {
// jsonData: [ int[] handcards, int[] equips, int[] delayedtricks, // jsonData: [ int[] handcards, int[] equips, int[] delayedtricks,
// string reason ] // string reason ]

View File

@ -6,6 +6,7 @@ GeneralsOverview 1.0 GeneralsOverview.qml
Init 1.0 Init.qml Init 1.0 Init.qml
Lobby 1.0 Lobby.qml Lobby 1.0 Lobby.qml
MetroButton 1.0 MetroButton.qml MetroButton 1.0 MetroButton.qml
MetroToggleButton 1.0 MetroToggleButton.qml
ModesOverview 1.0 ModesOverview.qml ModesOverview 1.0 ModesOverview.qml
PackageManage 1.0 PackageManage.qml PackageManage 1.0 PackageManage.qml
Room 1.0 Room.qml Room 1.0 Room.qml

View File

@ -0,0 +1,90 @@
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Layouts
import Fk.Pages
GraphicsBox {
property var options: []
property var all_options: []
property bool cancelable: false
property int min_num: 0
property int max_num: 0
property string skill_name: ""
property var result: []
id: root
title.text: Backend.translate("$Choice").arg(Backend.translate(skill_name))
width: Math.max(140, body.width + 20)
height: buttons.height + body.height + title.height + 20
function processPrompt(prompt) {
const data = prompt.split(":");
let raw = Backend.translate(data[0]);
const src = parseInt(data[1]);
const dest = parseInt(data[2]);
if (raw.match("%src")) raw = raw.replace(/%src/g, Backend.translate(getPhoto(src).general));
if (raw.match("%dest")) raw = raw.replace(/%dest/g, Backend.translate(getPhoto(dest).general));
if (raw.match("%arg2")) raw = raw.replace(/%arg2/g, Backend.translate(data[4]));
if (raw.match("%arg")) raw = raw.replace(/%arg/g, Backend.translate(data[3]));
return raw;
}
GridLayout {
id: body
// x: 10
anchors.horizontalCenter: parent.horizontalCenter
y: title.height + 5
flow: GridLayout.TopToBottom
rows: 8
columnSpacing: 10
Repeater {
model: all_options
MetroToggleButton {
// Layout.fillWidth: true
text: processPrompt(modelData)
enabled: options.indexOf(modelData) !== -1 && (root.result.length < max_num || triggered)
onClicked: {
if (triggered) {
root.result.push(index);
} else {
root.result.splice(root.result.indexOf(index), 1);
}
root.result = root.result;
}
}
}
}
Row {
id: buttons
anchors.margins: 8
anchors.top: body.bottom
anchors.horizontalCenter: root.horizontalCenter
spacing: 32
MetroButton {
Layout.fillWidth: true
text: processPrompt("OK")
enabled: root.result.length >= min_num
onClicked: {
root.close();
}
}
MetroButton {
Layout.fillWidth: true
text: processPrompt("Cancel")
visible: cancelable
onClicked: {
root.result = [];
root.close();
}
}
}
}

View File

@ -0,0 +1,107 @@
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Fk.Pages
GraphicsBox {
property var options: []
property var all_options: []
property bool cancelable: false
property int min_num: 0
property int max_num: 0
property string skill_name: ""
property var result: []
id: root
title.text: Backend.translate("$Choice").arg(Backend.translate(skill_name))
width: Math.max(140, body.width + 20)
height: buttons.height + body.height + title.height + 20
ListView {
id: body
x: 10
y: title.height + 5
width: Math.min(700, 220 * model.length)
height: 300
orientation: ListView.Horizontal
clip: true
spacing: 20
model: all_options
delegate: Item {
width: 200
height: 290
MetroToggleButton {
id: choicetitle
width: parent.width
text: Backend.translate(modelData)
enabled: options.indexOf(modelData) !== -1 && (root.result.length < max_num || triggered)
textFont.pixelSize: 24
anchors.top: choiceDetail.bottom
anchors.topMargin: 8
onClicked: {
if (triggered) {
root.result.push(index);
} else {
root.result.splice(root.result.indexOf(index), 1);
}
root.result = root.result;
}
}
Flickable {
id: choiceDetail
x: 4
height: parent.height - choicetitle.height
contentHeight: detail.height
width: parent.width
clip: true
Text {
id: detail
width: parent.width
text: Backend.translate(":" + modelData)
color: "white"
wrapMode: Text.WordWrap
font.pixelSize: 16
textFormat: TextEdit.RichText
}
}
}
}
Row {
id: buttons
anchors.margins: 8
anchors.bottom: root.bottom
anchors.horizontalCenter: root.horizontalCenter
spacing: 32
MetroButton {
width: 120
height: 35
text: Backend.translate("OK")
enabled: root.result.length >= min_num
onClicked: {
root.close();
}
}
MetroButton {
width: 120
height: 35
text: Backend.translate("Cancel")
visible: root.cancelable
onClicked: {
result = [];
root.close();
}
}
}
}

View File

@ -7,7 +7,7 @@ import Fk.Pages
GraphicsBox { GraphicsBox {
id: root id: root
title.text: Backend.callLuaFunction("PoxiPrompt", [poxi_type, card_data]) title.text: Backend.callLuaFunction("PoxiPrompt", [poxi_type, card_data, extra_data])
// TODO: Adjust the UI design in case there are more than 7 cards // TODO: Adjust the UI design in case there are more than 7 cards
width: 70 + 700 width: 70 + 700
@ -116,7 +116,7 @@ GraphicsBox {
width: 120 width: 120
height: 35 height: 35
text: Backend.translate("Cancel") text: Backend.translate("Cancel")
enabled: root.cancelable visible: root.cancelable
onClicked: root.cardsSelected([]) onClicked: root.cardsSelected([])
} }

View File

@ -74,6 +74,7 @@ Item {
x: root.width + 140 x: root.width + 140
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
opacity: 0 opacity: 0
detailed: false
} }
Text { Text {

View File

@ -715,11 +715,11 @@ function GetCardProhibitReason(cid, method, pattern)
end end
end end
function PoxiPrompt(poxi_type, data) function PoxiPrompt(poxi_type, data, extra_data)
local poxi = Fk.poxi_methods[poxi_type] local poxi = Fk.poxi_methods[poxi_type]
if not poxi or not poxi.prompt then return "" end if not poxi or not poxi.prompt then return "" end
if type(poxi.prompt) == "string" then return Fk:translate(poxi.prompt) end if type(poxi.prompt) == "string" then return Fk:translate(poxi.prompt) end
return poxi.prompt(data) return poxi.prompt(data, extra_data)
end end
function PoxiFilter(poxi_type, to_select, selected, data, extra_data) function PoxiFilter(poxi_type, to_select, selected, data, extra_data)

View File

@ -159,6 +159,7 @@ Fk:loadTranslationTable({
["#AskForLuckCard"] = "Do you want to use luck card (%1 times left)?", ["#AskForLuckCard"] = "Do you want to use luck card (%1 times left)?",
["AskForLuckCard"] = "Luck card", ["AskForLuckCard"] = "Luck card",
["#AskForChoice"] = "%1: Please choose", ["#AskForChoice"] = "%1: Please choose",
["#AskForCheck"] = "%1: Please choose",
["#choose-trigger"] = "Please choose the skill to use", ["#choose-trigger"] = "Please choose the skill to use",
["trigger"] = "Trigger skill", ["trigger"] = "Trigger skill",
-- ["Please arrange cards"] = "请拖拽移动卡牌", -- ["Please arrange cards"] = "请拖拽移动卡牌",
@ -169,6 +170,7 @@ Fk:loadTranslationTable({
["AskForGuanxing"] = "Stargazing", ["AskForGuanxing"] = "Stargazing",
["AskForExchange"] = "Exchaging", ["AskForExchange"] = "Exchaging",
["AskForChoice"] = "Making choice", ["AskForChoice"] = "Making choice",
["AskForCheck"] = "Making choice",
["AskForKingdom"] = "Choosing kingdom", ["AskForKingdom"] = "Choosing kingdom",
["AskForPindian"] = "Point fight", ["AskForPindian"] = "Point fight",
["AskForMoveCardInBoard"] = "Moving cards", ["AskForMoveCardInBoard"] = "Moving cards",

View File

@ -205,6 +205,7 @@ FreeKill使用的是libgit2的C API与此同时使用Git完成拓展包的下
["#AskForLuckCard"] = "你想使用手气卡吗?还可以使用 %1 次,剩余手气卡∞张", ["#AskForLuckCard"] = "你想使用手气卡吗?还可以使用 %1 次,剩余手气卡∞张",
["AskForLuckCard"] = "手气卡", ["AskForLuckCard"] = "手气卡",
["#AskForChoice"] = "%1请选择", ["#AskForChoice"] = "%1请选择",
["#AskForCheck"] = "%1请选择",
["#choose-trigger"] = "请选择一项技能发动", ["#choose-trigger"] = "请选择一项技能发动",
["trigger"] = "选择技能", ["trigger"] = "选择技能",
["Please arrange cards"] = "请拖拽移动卡牌", ["Please arrange cards"] = "请拖拽移动卡牌",
@ -215,6 +216,7 @@ FreeKill使用的是libgit2的C API与此同时使用Git完成拓展包的下
["AskForGuanxing"] = "观星", ["AskForGuanxing"] = "观星",
["AskForExchange"] = "换牌", ["AskForExchange"] = "换牌",
["AskForChoice"] = "选择", ["AskForChoice"] = "选择",
["AskForCheck"] = "选择",
["AskForKingdom"] = "选择势力", ["AskForKingdom"] = "选择势力",
["AskForPindian"] = "拼点", ["AskForPindian"] = "拼点",
["AskForMoveCardInBoard"] = "移动卡牌", ["AskForMoveCardInBoard"] = "移动卡牌",

View File

@ -52,6 +52,24 @@ Util.convertSubtypeAndEquipSlot = function(value)
end end
end end
--- 根据花色文字描述(如 黑桃、红桃、梅花、方块或者符号如♠♥♣♦带颜色返回花色ID。
---@param symbol string @ 描述/符号(原文,确保没被翻译过)
---@return Suit @ 花色ID
Util.getSuitFromString = function(symbol)
assert(type(symbol) == "string")
if symbol:find("spade") then
return Card.Spade
elseif symbol:find("heart") then
return Card.Heart
elseif symbol:find("club") then
return Card.Club
elseif symbol:find("diamond") then
return Card.Diamond
else
return Card.NoSuit
end
end
function printf(fmt, ...) function printf(fmt, ...)
print(string.format(fmt, ...)) print(string.format(fmt, ...))
end end
@ -118,14 +136,22 @@ function table:map(func)
end end
-- frequenly used filter & map functions -- frequenly used filter & map functions
--- 返回ID
Util.IdMapper = function(e) return e.id end Util.IdMapper = function(e) return e.id end
--- 根据卡牌ID返回卡牌
Util.Id2CardMapper = function(id) return Fk:getCardById(id) end Util.Id2CardMapper = function(id) return Fk:getCardById(id) end
--- 根据玩家ID返回玩家
Util.Id2PlayerMapper = function(id) Util.Id2PlayerMapper = function(id)
return Fk:currentRoom():getPlayerById(id) return Fk:currentRoom():getPlayerById(id)
end end
--- 返回武将名
Util.NameMapper = function(e) return e.name end Util.NameMapper = function(e) return e.name end
--- 根据武将名返回武将
Util.Name2GeneralMapper = function(e) return Fk.generals[e] end Util.Name2GeneralMapper = function(e) return Fk.generals[e] end
--- 根据技能名返回技能
Util.Name2SkillMapper = function(e) return Fk.skills[e] end Util.Name2SkillMapper = function(e) return Fk.skills[e] end
--- 返回译文
Util.TranslateMapper = function(str) return Fk:translate(str) end Util.TranslateMapper = function(str) return Fk:translate(str) end
-- for card preset -- for card preset

View File

@ -179,7 +179,7 @@ end
---@field public on_effect nil|fun(self: ActiveSkill, room: Room, cardEffectEvent: CardEffectEvent): bool ---@field public on_effect nil|fun(self: ActiveSkill, room: Room, cardEffectEvent: CardEffectEvent): bool
---@field public on_nullified nil|fun(self: ActiveSkill, room: Room, cardEffectEvent: CardEffectEvent): bool ---@field public on_nullified nil|fun(self: ActiveSkill, room: Room, cardEffectEvent: CardEffectEvent): bool
---@field public mod_target_filter nil|fun(self: ActiveSkill, to_select: integer, selected: integer[], user: integer, card: Card, distance_limited: boolean): bool ---@field public mod_target_filter nil|fun(self: ActiveSkill, to_select: integer, selected: integer[], user: integer, card: Card, distance_limited: boolean): bool
---@field public prompt nil|string|fun(self: ActiveSkill, selected: integer[], selected_cards: integer[]): string ---@field public prompt nil|string|fun(self: ActiveSkill, selected_cards: integer[], selected_targets: integer[]): string
---@field public interaction any ---@field public interaction any
---@param spec ActiveSkillSpec ---@param spec ActiveSkillSpec

View File

@ -18,7 +18,8 @@ math.randomseed(os.time())
-- 加载实用类让Lua编写起来更轻松。 -- 加载实用类让Lua编写起来更轻松。
local Utils = require "core.util" local Utils = require "core.util"
TargetGroup, AimGroup, Util = table.unpack(Utils) -- TargetGroup, AimGroup, Util = table.unpack(Utils)
TargetGroup, AimGroup, Util = Utils[1], Utils[2], Utils[3]
dofile "lua/core/debug.lua" dofile "lua/core/debug.lua"
-- 加载游戏核心类 -- 加载游戏核心类

View File

@ -1052,7 +1052,7 @@ end
---@param cancelable bool @ 是否可以点取消 ---@param cancelable bool @ 是否可以点取消
---@param extra_data table|nil @ 额外信息,因技能而异了 ---@param extra_data table|nil @ 额外信息,因技能而异了
---@param no_indicate bool @ 是否不显示指示线 ---@param no_indicate bool @ 是否不显示指示线
---@return boolean, table ---@return boolean, table|nil
function Room:askForUseActiveSkill(player, skill_name, prompt, cancelable, extra_data, no_indicate) function Room:askForUseActiveSkill(player, skill_name, prompt, cancelable, extra_data, no_indicate)
prompt = prompt or "" prompt = prompt or ""
cancelable = (cancelable == nil) and true or cancelable cancelable = (cancelable == nil) and true or cancelable
@ -1322,6 +1322,55 @@ function Room:askForChooseCardAndPlayers(player, targets, minNum, maxNum, patter
end end
end end
--- 询问玩家选择X张牌和Y名角色。
---
--- 返回两个值第一个是选择的目标列表第二个是选择的那张牌的id
---@param player ServerPlayer @ 要询问的玩家
---@param minCardNum integer @ 选卡牌最小值
---@param maxCardNum integer @ 选卡牌最大值
---@param targets integer[] @ 选择目标的id范围
---@param minTargetNum integer @ 选目标最小值
---@param maxTargetNum integer @ 选目标最大值
---@param pattern string|nil @ 选牌规则
---@param prompt string|nil @ 提示信息
---@param cancelable bool @ 能否点取消
---@param no_indicate bool @ 是否不显示指示线
---@return integer[], integer[]
function Room:askForChooseBoth(player, minCardNum, maxCardNum, targets, minTargetNum, maxTargetNum, pattern, prompt, skillName, cancelable, no_indicate)
if minCardNum < 1 or minTargetNum < 1 then
return table.unpack({}, {})
end
cancelable = (cancelable == nil) and true or cancelable
no_indicate = no_indicate or false
pattern = pattern or "."
local pcards = table.filter(player:getCardIds({ Player.Hand, Player.Equip }), function(id)
local c = Fk:getCardById(id)
return c:matchPattern(pattern)
end)
if #pcards < minCardNum and not cancelable then return table.unpack({}, {}) end
local data = {
targets = targets,
max_target_num = maxTargetNum,
min_target_num = minTargetNum,
max_card_num = maxCardNum,
min_card_num = minCardNum,
pattern = pattern,
skillName = skillName,
}
local _, ret = self:askForUseActiveSkill(player, "ex__choose_skill", prompt or "", cancelable, data, no_indicate)
if ret then
return ret.targets, ret.cards
else
if cancelable then
return table.unpack({}, {})
else
return table.random(targets, minTargetNum), table.random(pcards, minCardNum)
end
end
end
--- 抽个武将 --- 抽个武将
--- ---
--- 同getNCards抽出来就没有了所以记得放回去。 --- 同getNCards抽出来就没有了所以记得放回去。
@ -1609,6 +1658,35 @@ function Room:askForChoice(player, choices, skill_name, prompt, detailed, all_ch
return result return result
end end
--- 询问一名玩家从众多选项中勾选任意项。
---@param player ServerPlayer @ 要询问的玩家
---@param choices string[] @ 可选选项列表
---@param minNum number @ 最少选择项数
---@param maxNum number @ 最多选择项数
---@param skill_name string|nil @ 技能名
---@param prompt string|nil @ 提示信息
---@param cancelable bool|nil @ 是否可取消
---@param detailed bool @ 选项详细描述
---@param all_choices string[]|nil @ 所有选项(不可选变灰)
---@return string[] @ 选择的选项
function Room:askForCheck(player, choices, minNum, maxNum, skill_name, prompt, cancelable, detailed, all_choices)
cancelable = (cancelable == nil) and true or cancelable
if #choices <= minNum and not all_choices then return choices end
assert(minNum <= maxNum)
assert(not all_choices or table.every(choices, function(c) return table.contains(all_choices, c) end))
local command = "AskForCheck"
skill_name = skill_name or ""
prompt = prompt or ""
all_choices = all_choices or choices
detailed = detailed or false
self:notifyMoveFocus(player, skill_name)
local result = self:doRequest(player, command, json.encode{
choices, all_choices, {minNum, maxNum}, cancelable, skill_name, prompt, detailed
})
if result == "" then return {} end
return json.decode(result)
end
--- 询问玩家是否发动技能。 --- 询问玩家是否发动技能。
---@param player ServerPlayer @ 要询问的玩家 ---@param player ServerPlayer @ 要询问的玩家
---@param skill_name string @ 技能名 ---@param skill_name string @ 技能名

View File

@ -92,6 +92,23 @@ local choosePlayersSkill = fk.CreateActiveSkill{
max_target_num = function(self) return self.num end, max_target_num = function(self) return self.num end,
} }
local exChooseSkill = fk.CreateActiveSkill{
name = "ex__choose_skill",
card_filter = function(self, to_select, selected)
return self.pattern ~= "" and Exppattern:Parse(self.pattern):match(Fk:getCardById(to_select)) and #selected < self.max_card_num
end,
target_filter = function(self, to_select, selected, cards)
if self.pattern ~= "" and #cards < self.min_card_num then return end
if #selected < self.max_target_num then
return table.contains(self.targets, to_select)
end
end,
min_target_num = function(self) return self.min_target_num end,
max_target_num = function(self) return self.max_target_num end,
min_card_num = function(self) return self.min_card_num end,
max_card_num = function(self) return self.max_card_num end,
}
local maxCardsSkill = fk.CreateMaxCardsSkill{ local maxCardsSkill = fk.CreateMaxCardsSkill{
name = "max_cards_skill", name = "max_cards_skill",
global = true, global = true,
@ -222,6 +239,7 @@ AuxSkills = {
discardSkill, discardSkill,
chooseCardsSkill, chooseCardsSkill,
choosePlayersSkill, choosePlayersSkill,
exChooseSkill,
maxCardsSkill, maxCardsSkill,
choosePlayersToMoveCardInBoardSkill, choosePlayersToMoveCardInBoardSkill,
uncompulsoryInvalidity, uncompulsoryInvalidity,