多选、勾选框和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) => {
// jsonData: [ int[] handcards, int[] equips, int[] delayedtricks,
// string reason ]

View File

@ -6,6 +6,7 @@ GeneralsOverview 1.0 GeneralsOverview.qml
Init 1.0 Init.qml
Lobby 1.0 Lobby.qml
MetroButton 1.0 MetroButton.qml
MetroToggleButton 1.0 MetroToggleButton.qml
ModesOverview 1.0 ModesOverview.qml
PackageManage 1.0 PackageManage.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 {
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
width: 70 + 700
@ -116,7 +116,7 @@ GraphicsBox {
width: 120
height: 35
text: Backend.translate("Cancel")
enabled: root.cancelable
visible: root.cancelable
onClicked: root.cardsSelected([])
}

View File

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

View File

@ -715,11 +715,11 @@ function GetCardProhibitReason(cid, method, pattern)
end
end
function PoxiPrompt(poxi_type, data)
function PoxiPrompt(poxi_type, data, extra_data)
local poxi = Fk.poxi_methods[poxi_type]
if not poxi or not poxi.prompt then return "" end
if type(poxi.prompt) == "string" then return Fk:translate(poxi.prompt) end
return poxi.prompt(data)
return poxi.prompt(data, extra_data)
end
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"] = "Luck card",
["#AskForChoice"] = "%1: Please choose",
["#AskForCheck"] = "%1: Please choose",
["#choose-trigger"] = "Please choose the skill to use",
["trigger"] = "Trigger skill",
-- ["Please arrange cards"] = "请拖拽移动卡牌",
@ -169,6 +170,7 @@ Fk:loadTranslationTable({
["AskForGuanxing"] = "Stargazing",
["AskForExchange"] = "Exchaging",
["AskForChoice"] = "Making choice",
["AskForCheck"] = "Making choice",
["AskForKingdom"] = "Choosing kingdom",
["AskForPindian"] = "Point fight",
["AskForMoveCardInBoard"] = "Moving cards",

View File

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

View File

@ -52,6 +52,24 @@ Util.convertSubtypeAndEquipSlot = function(value)
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, ...)
print(string.format(fmt, ...))
end
@ -118,14 +136,22 @@ function table:map(func)
end
-- frequenly used filter & map functions
--- 返回ID
Util.IdMapper = function(e) return e.id end
--- 根据卡牌ID返回卡牌
Util.Id2CardMapper = function(id) return Fk:getCardById(id) end
--- 根据玩家ID返回玩家
Util.Id2PlayerMapper = function(id)
return Fk:currentRoom():getPlayerById(id)
end
--- 返回武将名
Util.NameMapper = function(e) return e.name end
--- 根据武将名返回武将
Util.Name2GeneralMapper = function(e) return Fk.generals[e] end
--- 根据技能名返回技能
Util.Name2SkillMapper = function(e) return Fk.skills[e] end
--- 返回译文
Util.TranslateMapper = function(str) return Fk:translate(str) end
-- 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_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 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
---@param spec ActiveSkillSpec

View File

@ -18,7 +18,8 @@ math.randomseed(os.time())
-- 加载实用类让Lua编写起来更轻松。
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"
-- 加载游戏核心类

View File

@ -1052,7 +1052,7 @@ end
---@param cancelable bool @ 是否可以点取消
---@param extra_data table|nil @ 额外信息,因技能而异了
---@param no_indicate bool @ 是否不显示指示线
---@return boolean, table
---@return boolean, table|nil
function Room:askForUseActiveSkill(player, skill_name, prompt, cancelable, extra_data, no_indicate)
prompt = prompt or ""
cancelable = (cancelable == nil) and true or cancelable
@ -1322,6 +1322,55 @@ function Room:askForChooseCardAndPlayers(player, targets, minNum, maxNum, patter
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抽出来就没有了所以记得放回去。
@ -1609,6 +1658,35 @@ function Room:askForChoice(player, choices, skill_name, prompt, detailed, all_ch
return result
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 skill_name string @ 技能名

View File

@ -92,6 +92,23 @@ local choosePlayersSkill = fk.CreateActiveSkill{
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{
name = "max_cards_skill",
global = true,
@ -222,6 +239,7 @@ AuxSkills = {
discardSkill,
chooseCardsSkill,
choosePlayersSkill,
exChooseSkill,
maxCardsSkill,
choosePlayersToMoveCardInBoardSkill,
uncompulsoryInvalidity,