UI modify (#158)

- 实现移动场上一张牌;
- 实现用作记录牌名并可查看的mark;
- 将askForChoice和interaction的文本解析方式改为prompt;
- 新增属性将牌移至牌堆指定索引位置;
- 修改时机“游戏开始时”至正确位置;
- 优化衍生牌逻辑;
- 新增“卡牌展示后”时机。
This commit is contained in:
Ho-spair 2023-05-20 16:00:03 +08:00 committed by GitHub
parent c222728a98
commit 1c6304f0f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 460 additions and 78 deletions

View File

@ -34,7 +34,7 @@ Rectangle {
Layout.preferredHeight: 120 Layout.preferredHeight: 120
cellHeight: 48 cellHeight: 48
cellWidth: 48 cellWidth: 48
model: 49 model: 50
visible: false visible: false
delegate: ItemDelegate { delegate: ItemDelegate {
Image { Image {

View File

@ -49,7 +49,7 @@ Item {
TapHandler { TapHandler {
onTapped: { onTapped: {
lobby_dialog.sourceComponent = Qt.createComponent("Fk.LobbyElement", "EditProfile"); lobby_dialog.sourceComponent = Qt.createComponent("EditProfile.qml");
lobby_drawer.open(); lobby_drawer.open();
} }
} }

View File

@ -154,7 +154,7 @@ Item {
icon.name: "media-playback-start" icon.name: "media-playback-start"
text: Backend.translate("Create Room") text: Backend.translate("Create Room")
onClicked: { onClicked: {
lobby_dialog.sourceComponent = Qt.createComponent("Fk.LobbyElement", "CreateRoom"); lobby_dialog.sourceComponent = Qt.createComponent("../LobbyElement/CreateRoom.qml");
lobby_drawer.open(); lobby_drawer.open();
config.observing = false; config.observing = false;
} }

View File

@ -487,14 +487,14 @@ Item {
Backend.callLuaFunction("SetInteractionDataOfSkill", [skill_name, "null"]); Backend.callLuaFunction("SetInteractionDataOfSkill", [skill_name, "null"]);
switch (data.type) { switch (data.type) {
case "combo": case "combo":
skillInteraction.sourceComponent = Qt.createComponent("Fk.SkillInteraction", "SkillCombo"); skillInteraction.sourceComponent = Qt.createComponent("../SkillInteraction/SkillCombo.qml");
skillInteraction.item.skill = skill_name; skillInteraction.item.skill = skill_name;
skillInteraction.item.default_choice = data["default"]; skillInteraction.item.default_choice = data["default"];
skillInteraction.item.choices = data.choices; skillInteraction.item.choices = data.choices;
// skillInteraction.item.clicked(); // skillInteraction.item.clicked();
break; break;
case "spin": case "spin":
skillInteraction.sourceComponent = Qt.createComponent("Fk.SkillInteraction", "SkillSpin"); skillInteraction.sourceComponent = Qt.createComponent("../SkillInteraction/SkillSpin.qml");
skillInteraction.item.skill = skill_name; skillInteraction.item.skill = skill_name;
skillInteraction.item.from = data.from; skillInteraction.item.from = data.from;
skillInteraction.item.to = data.to; skillInteraction.item.to = data.to;
@ -816,7 +816,7 @@ Item {
} }
function startCheat(type, data) { function startCheat(type, data) {
cheatLoader.sourceComponent = Qt.createComponent("Fk.Cheat", type); cheatLoader.sourceComponent = Qt.createComponent(`../Cheat/${type}.qml`);
cheatLoader.item.extra_data = data; cheatLoader.item.extra_data = data;
cheatDrawer.open(); cheatDrawer.open();
} }

View File

@ -228,7 +228,7 @@ function setEmotion(id, emotion, isCardId) {
// TODO: set picture emotion // TODO: set picture emotion
return; return;
} }
let component = Qt.createComponent("Fk.RoomElement", "PixmapAnimation"); let component = Qt.createComponent("../RoomElement/PixmapAnimation.qml");
if (component.status !== Component.Ready) if (component.status !== Component.Ready)
return; return;
@ -277,7 +277,7 @@ function changeHp(id, delta, losthp) {
} }
function doIndicate(from, tos) { function doIndicate(from, tos) {
let component = Qt.createComponent("Fk.RoomElement", "IndicatorLine"); let component = Qt.createComponent("../RoomElement/IndicatorLine.qml");
if (component.status !== Component.Ready) if (component.status !== Component.Ready)
return; return;
@ -594,7 +594,7 @@ callbacks["AskForGeneral"] = function(jsonData) {
let heg = data[2]; let heg = data[2];
roomScene.promptText = Backend.translate("#AskForGeneral"); roomScene.promptText = Backend.translate("#AskForGeneral");
roomScene.state = "replying"; roomScene.state = "replying";
roomScene.popupBox.sourceComponent = Qt.createComponent("Fk.RoomElement", "ChooseGeneralBox"); roomScene.popupBox.sourceComponent = Qt.createComponent("../RoomElement/ChooseGeneralBox.qml");
let box = roomScene.popupBox.item; let box = roomScene.popupBox.item;
box.accepted.connect(() => { box.accepted.connect(() => {
replyToServer(JSON.stringify(box.choices)); replyToServer(JSON.stringify(box.choices));
@ -627,7 +627,7 @@ callbacks["AskForGuanxing"] = function(jsonData) {
let min_bottom_cards = data.min_bottom_cards; let min_bottom_cards = data.min_bottom_cards;
let max_bottom_cards = data.max_bottom_cards; let max_bottom_cards = data.max_bottom_cards;
roomScene.state = "replying"; roomScene.state = "replying";
roomScene.popupBox.sourceComponent = Qt.createComponent("Fk.RoomElement", "GuanxingBox"); roomScene.popupBox.sourceComponent = Qt.createComponent("../RoomElement/GuanxingBox.qml");
data.cards.forEach(id => { data.cards.forEach(id => {
let d = Backend.callLuaFunction("GetCardData", [id]); let d = Backend.callLuaFunction("GetCardData", [id]);
cards.push(JSON.parse(d)); cards.push(JSON.parse(d));
@ -663,7 +663,7 @@ callbacks["AskForChoice"] = function(jsonData) {
roomScene.promptText = processPrompt(prompt); roomScene.promptText = processPrompt(prompt);
} }
roomScene.state = "replying"; roomScene.state = "replying";
roomScene.popupBox.sourceComponent = Qt.createComponent("Fk.RoomElement", "ChoiceBox"); roomScene.popupBox.sourceComponent = Qt.createComponent("../RoomElement/ChoiceBox.qml");
let box = roomScene.popupBox.item; let box = roomScene.popupBox.item;
box.options = choices; box.options = choices;
box.skill_name = skill_name; box.skill_name = skill_name;
@ -702,7 +702,7 @@ callbacks["AskForCardChosen"] = function(jsonData) {
roomScene.promptText = Backend.translate("#AskForChooseCard") roomScene.promptText = Backend.translate("#AskForChooseCard")
.arg(Backend.translate(reason)); .arg(Backend.translate(reason));
roomScene.state = "replying"; roomScene.state = "replying";
roomScene.popupBox.sourceComponent = Qt.createComponent("Fk.RoomElement", "PlayerCardBox"); roomScene.popupBox.sourceComponent = Qt.createComponent("../RoomElement/PlayerCardBox.qml");
let box = roomScene.popupBox.item; let box = roomScene.popupBox.item;
box.addHandcards(handcards); box.addHandcards(handcards);
box.addEquips(equips); box.addEquips(equips);
@ -745,7 +745,7 @@ callbacks["AskForCardsChosen"] = function(jsonData) {
roomScene.promptText = Backend.translate("#AskForChooseCard") roomScene.promptText = Backend.translate("#AskForChooseCard")
.arg(Backend.translate(reason)); .arg(Backend.translate(reason));
roomScene.state = "replying"; roomScene.state = "replying";
roomScene.popupBox.sourceComponent = Qt.createComponent("Fk.RoomElement", "PlayerCardBox"); roomScene.popupBox.sourceComponent = Qt.createComponent("../RoomElement/PlayerCardBox.qml");
let box = roomScene.popupBox.item; let box = roomScene.popupBox.item;
box.multiChoose = true; box.multiChoose = true;
box.min = min; box.min = min;
@ -759,6 +759,33 @@ callbacks["AskForCardsChosen"] = function(jsonData) {
}); });
} }
callbacks["AskForMoveCardInBoard"] = function(jsonData) {
const data = JSON.parse(jsonData);
const { cards, cardsPosition, generalNames } = data;
roomScene.state = "replying";
roomScene.popupBox.sourceComponent = Qt.createComponent("../RoomElement/MoveCardInBoardBox.qml");
const boxCards = [];
cards.forEach(id => {
let d = Backend.callLuaFunction("GetCardData", [id]);
boxCards.push(JSON.parse(d));
});
const box = roomScene.popupBox.item;
box.cards = boxCards;
box.cardsPosition = cardsPosition;
box.generalNames = generalNames.map(name => {
const namesSplited = name.split('/');
return namesSplited.length > 1 ? namesSplited.map(nameSplited => Backend.translate(nameSplited)).join('/') : Backend.translate(name)
});
box.arrangeCards();
box.accepted.connect(() => {
replyToServer(JSON.stringify(box.getResult()));
});
}
callbacks["MoveCards"] = function(jsonData) { callbacks["MoveCards"] = function(jsonData) {
// jsonData: merged moves // jsonData: merged moves
let moves = JSON.parse(jsonData); let moves = JSON.parse(jsonData);
@ -938,7 +965,7 @@ callbacks["Animate"] = function(jsonData) {
} }
case "InvokeSkill": { case "InvokeSkill": {
let id = data.player; let id = data.player;
let component = Qt.createComponent("Fk.RoomElement", "SkillInvokeAnimation"); let component = Qt.createComponent("../RoomElement/SkillInvokeAnimation.qml");
if (component.status !== Component.Ready) if (component.status !== Component.Ready)
return; return;
@ -1002,7 +1029,7 @@ callbacks["LogEvent"] = function(jsonData) {
callbacks["GameOver"] = function(jsonData) { callbacks["GameOver"] = function(jsonData) {
roomScene.state = "notactive"; roomScene.state = "notactive";
roomScene.popupBox.sourceComponent = Qt.createComponent("Fk.RoomElement", "GameOverBox"); roomScene.popupBox.sourceComponent = Qt.createComponent("../RoomElement/GameOverBox.qml");
let box = roomScene.popupBox.item; let box = roomScene.popupBox.item;
box.winner = jsonData; box.winner = jsonData;
roomScene.isStarted = false; roomScene.isStarted = false;
@ -1011,7 +1038,7 @@ callbacks["GameOver"] = function(jsonData) {
callbacks["FillAG"] = (j) => { callbacks["FillAG"] = (j) => {
let data = JSON.parse(j); let data = JSON.parse(j);
let ids = data[0]; let ids = data[0];
roomScene.manualBox.sourceComponent = Qt.createComponent("Fk.RoomElement", "AG"); roomScene.manualBox.sourceComponent = Qt.createComponent("../RoomElement/AG.qml");
roomScene.manualBox.item.addIds(ids); roomScene.manualBox.item.addIds(ids);
} }

View File

@ -33,7 +33,7 @@ Item {
width: childrenRect.width width: childrenRect.width
height: 22 height: 22
Text { Text {
text: Backend.translate(mark_name) + ' ' + mark_extra text: Backend.translate(mark_name) + ' ' + (special_value !== '' ? special_value : mark_extra)
font.family: fontLibian.name font.family: fontLibian.name
font.pixelSize: 22 font.pixelSize: 22
font.letterSpacing: -0.6 font.letterSpacing: -0.6
@ -53,16 +53,20 @@ Item {
TapHandler { TapHandler {
enabled: root.parent.state != "candidate" || !root.parent.selectable enabled: root.parent.state != "candidate" || !root.parent.selectable
onTapped: { onTapped: {
let data = JSON.parse(Backend.callLuaFunction("GetPile", [root.parent.playerid, mark_name])); const params = { name: mark_name };
data = data.filter((e) => e !== -1); if (mark_name.startsWith('@$')) {
if (data.length === 0) params.cardNames = mark_extra.split(',');
return; } else {
let data = JSON.parse(Backend.callLuaFunction("GetPile", [root.parent.playerid, mark_name]));
data = data.filter((e) => e !== -1);
if (data.length === 0)
return;
params.ids = data;
}
// Just for using room's right drawer // Just for using room's right drawer
roomScene.startCheat("ViewPile", { roomScene.startCheat("../RoomElement/ViewPile", params);
name: mark_name,
ids: data
});
} }
} }
} }
@ -83,11 +87,18 @@ Item {
} }
} }
data = (data instanceof Array ? data.map((markText) => Backend.translate(markText)).join(' ') : Backend.translate(data)); let special_value = '';
if (mark.startsWith('@$')) {
special_value = data.length;
data = data.join(',');
} else {
data = data instanceof Array ? data.map((markText) => Backend.translate(markText)).join(' ') : Backend.translate(data);
}
if (modelItem) if (modelItem)
modelItem.mark_extra = data; modelItem.mark_extra = data;
else else
markList.append({ mark_name: mark, mark_extra: data }); markList.append({ mark_name: mark, mark_extra: data, special_value });
arrangeMarks(); arrangeMarks();
} }

View File

@ -85,7 +85,7 @@ Item {
Image { Image {
id: cardItem id: cardItem
source: known ? SkinBank.getCardPicture(cid) source: known ? SkinBank.getCardPicture(cid || name)
: (SkinBank.CARD_DIR + "card-back") : (SkinBank.CARD_DIR + "card-back")
anchors.fill: parent anchors.fill: parent
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop

View File

@ -14,6 +14,18 @@ GraphicsBox {
width: Math.max(140, body.width + 20) width: Math.max(140, body.width + 20)
height: body.height + title.height + 20 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", Backend.translate(getPhoto(src).general));
if (raw.match("%dest")) raw = raw.replace("%dest", Backend.translate(getPhoto(dest).general));
if (raw.match("%arg")) raw = raw.replace("%arg", Backend.translate(data[3]));
if (raw.match("%arg2")) raw = raw.replace("%arg2", Backend.translate(data[4]));
return raw;
}
GridLayout { GridLayout {
id: body id: body
x: 10 x: 10
@ -27,7 +39,7 @@ GraphicsBox {
MetroButton { MetroButton {
Layout.fillWidth: true Layout.fillWidth: true
text: Backend.translate(modelData) text: processPrompt(modelData)
onClicked: { onClicked: {
result = index; result = index;

View File

@ -73,7 +73,7 @@ RowLayout {
if (expanded_pile_names.indexOf(pile) !== -1) if (expanded_pile_names.indexOf(pile) !== -1)
return; return;
let component = Qt.createComponent("Fk.RoomElement", "CardItem"); let component = Qt.createComponent("../RoomElement/CardItem.qml");
let parentPos = roomScene.mapFromItem(self, 0, 0); let parentPos = roomScene.mapFromItem(self, 0, 0);
expanded_piles[pile] = []; expanded_piles[pile] = [];

View File

@ -50,7 +50,7 @@ Item {
function remove(outputs) function remove(outputs)
{ {
let component = Qt.createComponent("Fk.RoomElement", "CardItem"); let component = Qt.createComponent("CardItem.qml");
if (component.status !== Component.Ready) if (component.status !== Component.Ready)
return []; return [];

View File

@ -0,0 +1,148 @@
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Layouts
import Fk.Pages
GraphicsBox {
id: root
property var cards: []
property var cardsPosition: []
property var generalNames: []
property var result
property int padding: 25
title.text: Backend.translate("Please click to move card")
width: body.width + padding * 2
height: title.height + body.height + padding * 2
ColumnLayout {
id: body
x: padding
y: parent.height - padding - height
spacing: 20
Repeater {
id: areaRepeater
model: generalNames
Row {
spacing: 5
Rectangle {
anchors.verticalCenter: parent.verticalCenter
color: "#6B5D42"
width: 20
height: 100
radius: 5
Text {
anchors.fill: parent
width: 20
height: 100
text: modelData
color: "white"
font.family: fontLibian.name
font.pixelSize: 18
style: Text.Outline
wrapMode: Text.WrapAnywhere
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
}
Repeater {
id: cardRepeater
model: cards
Rectangle {
color: "#4A4139"
width: 93
height: 130
opacity: 0.5
Text {
horizontalAlignment: Text.AlignHCenter
anchors.centerIn: parent
text: Backend.translate(JSON.parse(Backend.callLuaFunction("GetCardData", [modelData.cid])).subtype)
color: "#90765F"
font.family: fontLibian.name
font.pixelSize: 16
width: parent.width * 0.8
wrapMode: Text.WordWrap
}
}
}
property alias cardRepeater: cardRepeater
}
}
MetroButton {
Layout.alignment: Qt.AlignHCenter
id: buttonConfirm
text: Backend.translate("OK")
width: 120
height: 35
enabled: false
onClicked: close();
}
}
Repeater {
id: cardItem
model: cards
CardItem {
x: index
y: -1
cid: modelData.cid
name: modelData.name
suit: modelData.suit
number: modelData.number
selectable: !result || result.item === this
onClicked: {
if (!selectable) return;
if ((result || {}).item === this) {
result = undefined;
} else {
result = { item: this };
}
updatePosition(this);
}
}
}
function arrangeCards() {
for (let i = 0; i < cards.length; i++) {
const curCard = cardItem.itemAt(i);
curCard.origX = i * 98 + 50;
curCard.origY = cardsPosition[i] * 150 + body.y;
curCard.goBack();
}
}
function updatePosition(item) {
for (let i = 0; i < 2; i++) {
const index = cards.findIndex(data => item.cid === data.cid);
result && (result.pos = cardsPosition[index]);
const cardPos = cardsPosition[index] === 0 ? (result ? 1 : 0) : (result ? 0 : 1);
const curArea = areaRepeater.itemAt(cardPos);
const curBox = curArea.cardRepeater.itemAt(index);
const curPos = mapFromItem(curArea, curBox.x, curBox.y);
item.origX = curPos.x;
item.origY = curPos.y;
item.goBack(true);
buttonConfirm.enabled = !!result;
}
}
function getResult() {
return result ? { cardId: result.item.cid, pos: result.pos } : '';
}
}

View File

@ -39,27 +39,41 @@ Item {
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
topPadding: 5
id: skill id: skill
font.family: fontLi2.name font.family: fontLi2.name
font.pixelSize: Math.max(26 - text.length, 18) font.pixelSize: Math.max(26 - text.length, 18)
visible: false visible: false
font.bold: true
} }
Glow { Glow {
id: glowItem id: glowItem
source: skill source: skill
anchors.fill: skill anchors.fill: skill
radius: 6 color: "black"
//samples: 8 spread: 0.3
color: "grey" radius: 5
} }
LinearGradient { LinearGradient {
anchors.fill: skill anchors.fill: skill
source: skill source: skill
gradient: Gradient { gradient: Gradient {
GradientStop { position: 0; color: "#FFE07C" } GradientStop {
GradientStop { position: 1; color: "#B79A5F" } position: 0
color: "#FEF7C2"
}
GradientStop {
position: 0.5
color: "#D2AD4A"
}
GradientStop {
position: 1
color: "#BE9878"
}
} }
} }

View File

@ -71,7 +71,7 @@ Item {
columns: 4 columns: 4
Repeater { Repeater {
model: extra_data.ids model: extra_data.ids || extra_data.cardNames
CardItem { CardItem {
id: cardItem id: cardItem
@ -79,7 +79,16 @@ Item {
height: cardItem.width * 1.4 height: cardItem.width * 1.4
autoBack: false autoBack: false
Component.onCompleted: { Component.onCompleted: {
let data = JSON.parse(Backend.callLuaFunction("GetCardData", [modelData])); let data = {}
if (extra_data.ids) {
data = JSON.parse(Backend.callLuaFunction("GetCardData", [modelData]));
} else {
data.cid = 0;
data.name = modelData;
data.suit = '';
data.number = 0;
data.color = '';
}
setData(data); setData(data);
} }
} }

View File

@ -9,7 +9,20 @@ MetroButton {
property var choices: [] property var choices: []
property string default_choice property string default_choice
property string answer: default_choice property string answer: default_choice
text: Backend.translate(answer)
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", Backend.translate(getPhoto(src).general));
if (raw.match("%dest")) raw = raw.replace("%dest", Backend.translate(getPhoto(dest).general));
if (raw.match("%arg")) raw = raw.replace("%arg", Backend.translate(data[3]));
if (raw.match("%arg2")) raw = raw.replace("%arg2", Backend.translate(data[4]));
return raw;
}
text: processPrompt(answer)
onAnswerChanged: { onAnswerChanged: {
if (!answer) return; if (!answer) return;
@ -21,7 +34,7 @@ MetroButton {
} }
onClicked: { onClicked: {
roomScene.popupBox.sourceComponent = Qt.createComponent("Fk.RoomElement", "ChoiceBox"); roomScene.popupBox.sourceComponent = Qt.createComponent("../RoomElement/ChoiceBox.qml");
let box = roomScene.popupBox.item; let box = roomScene.popupBox.item;
box.options = choices; box.options = choices;
box.accepted.connect(() => { box.accepted.connect(() => {

View File

@ -29,10 +29,18 @@ function getGeneralPicture(name) {
return GENERAL_DIR + "0.jpg"; return GENERAL_DIR + "0.jpg";
} }
function getCardPicture(cid) { function getCardPicture(cidOrName) {
let data = JSON.parse(Backend.callLuaFunction("GetCardData", [cid])); let extension = "";
let extension = data.extension; let name = "unknown";
let name = data.name; if (typeof cidOrName === 'string') {
name = cidOrName;
extension = Backend.callLuaFunction("GetCardExtensionByName", [cidOrName]);
} else {
const data = JSON.parse(Backend.callLuaFunction("GetCardData", [cid]));
extension = data.extension;
name = data.name;
}
let path = AppPath + "/packages/" + extension + "/image/card/" + name + ".png"; let path = AppPath + "/packages/" + extension + "/image/card/" + name + ".png";
if (Backend.exists(path)) { if (Backend.exists(path)) {
return path; return path;

View File

@ -98,6 +98,14 @@ function GetCardData(id)
return json.encode(ret) return json.encode(ret)
end end
function GetCardExtensionByName(cardName)
local card = table.find(Fk.cards, function(card)
return card.name == cardName
end)
return card and card.package.extensionName or ""
end
function GetAllGeneralPack() function GetAllGeneralPack()
local ret = {} local ret = {}
for _, name in ipairs(Fk.package_names) do for _, name in ipairs(Fk.package_names) do

View File

@ -143,6 +143,7 @@ FreeKill使用的是libgit2的C API与此同时使用Git完成拓展包的下
["#choose-trigger"] = "请选择一项技能发动", ["#choose-trigger"] = "请选择一项技能发动",
["trigger"] = "选择技能", ["trigger"] = "选择技能",
["Please arrange cards"] = "请拖拽移动卡牌", ["Please arrange cards"] = "请拖拽移动卡牌",
["Please click to move card"] = "请点击移动卡牌",
[" thinking..."] = " 思考中...", [" thinking..."] = " 思考中...",
["AskForGeneral"] = "选择武将", ["AskForGeneral"] = "选择武将",
@ -203,10 +204,6 @@ FreeKill使用的是libgit2的C API与此同时使用Git完成拓展包的下
["$NoWinner"] = "平局!", ["$NoWinner"] = "平局!",
["Back To Lobby"] = "返回大厅", ["Back To Lobby"] = "返回大厅",
["basic_char"] = "",
["trick_char"] = "",
["equip_char"] = "",
["Bulletin Info"] = "<h2>更新说明</h2>\ ["Bulletin Info"] = "<h2>更新说明</h2>\
1. 退/<br>\ 1. 退/<br>\
2. <br>\ 2. <br>\

View File

@ -22,6 +22,7 @@
---@field public skill Skill ---@field public skill Skill
---@field public special_skills string[] | nil ---@field public special_skills string[] | nil
---@field public is_damage_card boolean ---@field public is_damage_card boolean
---@field public is_derived boolean|null
local Card = class("Card") local Card = class("Card")
---@alias Suit integer ---@alias Suit integer
@ -96,6 +97,11 @@ function Card:initialize(name, suit, number, color)
self.skillNames = {} self.skillNames = {}
self.mark = {} self.mark = {}
if string.sub(name, 1, 1) == "&" then
self.name = string.sub(name, 2, #name)
self.is_derived = true
end
local mt = table.simpleClone(getmetatable(self)) local mt = table.simpleClone(getmetatable(self))
local newidx = mt.__newindex or rawset local newidx = mt.__newindex or rawset
mt.__newindex = function(t, k, v) mt.__newindex = function(t, k, v)
@ -139,6 +145,7 @@ function Card:clone(suit, number)
newCard.equip_skill = self.equip_skill newCard.equip_skill = self.equip_skill
newCard.attack_range = self.attack_range newCard.attack_range = self.attack_range
newCard.is_damage_card = self.is_damage_card newCard.is_damage_card = self.is_damage_card
newCard.is_derived = self.is_derived
return newCard return newCard
end end

View File

@ -742,4 +742,17 @@ function Player:getSwitchSkillState(skillName, afterUse)
end end
end end
function Player:canMoveCardInBoardTo(to, id)
local card = Fk:getCardById(id)
assert(card.type == Card.TypeEquip or card.sub_type == Card.SubtypeDelayedTrick)
if card.type == Card.TypeEquip then
return not to:getEquipment(card.sub_type)
else
return not table.find(to:getCardIds(Player.Judge), function(cardId)
return Fk:getCardById(cardId).name == card.name
end)
end
end
return Player return Player

View File

@ -208,6 +208,20 @@ function table:slice(begin, _end)
return ret return ret
end end
function table:assign(targetTbl)
for key, value in pairs(targetTbl) do
if self[key] then
if type(value) == "table" then
table.insertTable(self[key], value)
else
table.insert(self[key], value)
end
else
self[key] = value
end
end
end
-- allow a = "Hello"; a[1] == "H" -- allow a = "Hello"; a[1] == "H"
local str_mt = getmetatable("") local str_mt = getmetatable("")
str_mt.__index = function(str, k) str_mt.__index = function(str, k)

View File

@ -6,6 +6,7 @@
---@alias Event integer ---@alias Event integer
fk.NonTrigger = 1 fk.NonTrigger = 1
fk.GamePrepared = 78
fk.GameStart = 2 fk.GameStart = 2
fk.TurnStart = 3 fk.TurnStart = 3
fk.TurnEnd = 73 fk.TurnEnd = 73
@ -101,4 +102,8 @@ fk.BeforeTriggerSkillUse = 75
fk.BeforeDrawCard = 76 fk.BeforeDrawCard = 76
fk.NumOfEvents = 77 fk.CardShown = 77
-- 78 = GamePrepared
fk.NumOfEvents = 79

View File

@ -92,7 +92,7 @@ GameEvent.functions[GameEvent.DrawInitial] = function(self)
room:notifyMoveFocus(room.alive_players, "AskForLuckCard") room:notifyMoveFocus(room.alive_players, "AskForLuckCard")
room:doBroadcastNotify("AskForLuckCard", room.settings.luckTime or 4) room:doBroadcastNotify("AskForLuckCard", room.settings.luckTime or 4)
local remainTime = room.timeout local remainTime = room.timeout + 1
local currentTime = os.time() local currentTime = os.time()
local elapsed = 0 local elapsed = 0
@ -127,12 +127,17 @@ GameEvent.functions[GameEvent.Round] = function(self)
local logic = room.logic local logic = room.logic
local p local p
if room:getTag("FirstRound") then local isFirstRound = room:getTag("FirstRound")
if isFirstRound then
room:setTag("FirstRound", false) room:setTag("FirstRound", false)
end end
room:setTag("RoundCount", room:getTag("RoundCount") + 1) room:setTag("RoundCount", room:getTag("RoundCount") + 1)
room:doBroadcastNotify("UpdateRoundNum", room:getTag("RoundCount")) room:doBroadcastNotify("UpdateRoundNum", room:getTag("RoundCount"))
if isFirstRound then
logic:trigger(fk.GameStart, room.current)
end
logic:trigger(fk.RoundStart, room.current) logic:trigger(fk.RoundStart, room.current)
repeat repeat

View File

@ -37,6 +37,7 @@ GameEvent.functions[GameEvent.MoveCards] = function(self)
moveVisible = cardsMoveInfo.moveVisible, moveVisible = cardsMoveInfo.moveVisible,
specialName = cardsMoveInfo.specialName, specialName = cardsMoveInfo.specialName,
specialVisible = cardsMoveInfo.specialVisible, specialVisible = cardsMoveInfo.specialVisible,
drawPilePosition = cardsMoveInfo.drawPilePosition,
} }
table.insert(cardsMoveStructs, cardsMoveStruct) table.insert(cardsMoveStructs, cardsMoveStruct)
@ -100,7 +101,18 @@ GameEvent.functions[GameEvent.MoveCards] = function(self)
toAreaIds = self.void toAreaIds = self.void
end end
table.insert(toAreaIds, data.toArea == Card.DrawPile and 1 or #toAreaIds + 1, info.cardId) if data.toArea == Card.DrawPile then
local putIndex = data.drawPilePosition or 1
if putIndex == -1 then
putIndex = #self.draw_pile + 1
elseif putIndex < 1 or putIndex > #self.draw_pile + 1 then
putIndex = 1
end
table.insert(toAreaIds, putIndex, info.cardId)
else
table.insert(toAreaIds, info.cardId)
end
end end
self:setCardArea(info.cardId, data.toArea, data.to) self:setCardArea(info.cardId, data.toArea, data.to)
if data.toArea == Card.DrawPile or realFromArea == Card.DrawPile then if data.toArea == Card.DrawPile or realFromArea == Card.DrawPile then

View File

@ -187,7 +187,7 @@ GameEvent.functions[GameEvent.UseCard] = function(self)
table.insert(cardUseEvent.responseToEvent.cardsResponded, cardUseEvent.card) table.insert(cardUseEvent.responseToEvent.cardsResponded, cardUseEvent.card)
end end
for _, event in ipairs({ fk.AfterCardUseDeclared, fk.AfterCardTargetDeclared, fk.BeforeCardUseEffect, fk.CardUsing }) do for _, event in ipairs({ fk.AfterCardUseDeclared, fk.AfterCardTargetDeclared, fk.CardUsing }) do
if not cardUseEvent.toCard and #TargetGroup:getRealTargets(cardUseEvent.tos) == 0 then if not cardUseEvent.toCard and #TargetGroup:getRealTargets(cardUseEvent.tos) == 0 then
break break
end end

View File

@ -233,6 +233,16 @@ end
function GameLogic:prepareDrawPile() function GameLogic:prepareDrawPile()
local room = self.room local room = self.room
local allCardIds = Fk:getAllCardIds() local allCardIds = Fk:getAllCardIds()
for i = #allCardIds, 1, -1 do
if Fk:getCardById(allCardIds[i]).is_derived then
local id = allCardIds[i]
table.removeOne(allCardIds, id)
table.insert(room.void, id)
room:setCardArea(id, Card.Void, nil)
end
end
table.shuffle(allCardIds) table.shuffle(allCardIds)
room.draw_pile = allCardIds room.draw_pile = allCardIds
for _, id in ipairs(room.draw_pile) do for _, id in ipairs(room.draw_pile) do
@ -291,7 +301,7 @@ function GameLogic:prepareForStart()
end end
function GameLogic:action() function GameLogic:action()
self:trigger(fk.GameStart) self:trigger(fk.GamePrepared)
local room = self.room local room = self.room
execGameEvent(GameEvent.DrawInitial) execGameEvent(GameEvent.DrawInitial)

View File

@ -1572,6 +1572,82 @@ function Room:askForCustomDialog(player, focustxt, qmlPath, extra_data)
}) })
end end
---@field player ServerPlayer
---@field targetOne ServerPlayer
---@field targetTwo ServerPlayer
---@field skillName string
---@return cardId
function Room:askForMoveCardInBoard(player, targetOne, targetTwo, skillName)
local cards = {}
local cardsPosition = {}
for _, equipId in ipairs(targetOne:getCardIds(Player.Equip)) do
if targetOne:canMoveCardInBoardTo(targetTwo, equipId) then
table.insert(cards, equipId)
end
end
for _, equipId in ipairs(targetTwo:getCardIds(Player.Equip)) do
if targetTwo:canMoveCardInBoardTo(targetOne, equipId) then
table.insert(cards, equipId)
end
end
if #cards > 0 then
table.sort(cards, function(prev, next)
local prevSubType = Fk:getCardById(prev).sub_type
local nextSubType = Fk:getCardById(next).sub_type
return prevSubType < nextSubType
end)
for _, id in ipairs(cards) do
table.insert(cardsPosition, self:getCardOwner(id) == targetOne and 0 or 1)
end
end
for _, trickId in ipairs(targetOne:getCardIds(Player.Judge)) do
if targetOne:canMoveCardInBoardTo(targetTwo, trickId) then
table.insert(cards, trickId)
table.insert(cardsPosition, 0)
end
end
for _, trickId in ipairs(targetTwo:getCardIds(Player.Judge)) do
if targetTwo:canMoveCardInBoardTo(targetOne, trickId) then
table.insert(cards, trickId)
table.insert(cardsPosition, 1)
end
end
if #cards == 0 then
return
end
local firstGeneralName = targetOne.general + (targetOne.deputyGeneral ~= "" and ("/" .. targetOne.deputyGeneral) or "")
local secGeneralName = targetTwo.general + (targetTwo.deputyGeneral ~= "" and ("/" .. targetTwo.deputyGeneral) or "")
local data = { cards = cards, cardsPosition = cardsPosition, generalNames = { firstGeneralName, secGeneralName } }
local command = "AskForMoveCardInBoard"
self:notifyMoveFocus(player, command)
local result = self:doRequest(player, command, json.encode(data))
if result == "" then
local randomIndex = math.random(1, #cards)
result = { cardId = cards[randomIndex], pos = cardsPosition[randomIndex] }
else
result = json.decode(result)
end
local cardToMove = Fk:getCardById(result.cardId)
self:moveCardTo(
cardToMove,
cardToMove.type == Card.TypeEquip and Player.Equip or Player.Judge,
result.pos == 0 and targetTwo or targetOne,
fk.ReasonPut,
skillName,
nil,
true
)
end
------------------------------------------------------------------------ ------------------------------------------------------------------------
-- 使用牌 -- 使用牌
------------------------------------------------------------------------ ------------------------------------------------------------------------
@ -1712,6 +1788,7 @@ function Room:doCardUseEffect(cardUseEvent)
local realCardIds = self:getSubcardsByRule(cardUseEvent.card, { Card.Processing }) local realCardIds = self:getSubcardsByRule(cardUseEvent.card, { Card.Processing })
self.logic:trigger(fk.BeforeCardUseEffect, cardUseEvent.from, cardUseEvent)
-- If using Equip or Delayed trick, move them to the area and return -- If using Equip or Delayed trick, move them to the area and return
if cardUseEvent.card.type == Card.TypeEquip then if cardUseEvent.card.type == Card.TypeEquip then
if #realCardIds == 0 then if #realCardIds == 0 then

View File

@ -343,6 +343,8 @@ function ServerPlayer:showCards(cards)
from = self.id, from = self.id,
cards = cards, cards = cards,
}) })
room.logic:trigger(fk.CardShown, self, { cardIds = cards })
end end
---@param from_phase Phase ---@param from_phase Phase

View File

@ -28,6 +28,7 @@
---@field public moveVisible boolean|null ---@field public moveVisible boolean|null
---@field public specialName string|null ---@field public specialName string|null
---@field public specialVisible boolean|null ---@field public specialVisible boolean|null
---@field public drawPilePosition number|null @ 移至牌堆的索引位置,值为-1代表置入牌堆底或者牌堆牌数+1也为牌堆底
---@class PindianResult ---@class PindianResult
---@field public toCard Card ---@field public toCard Card

View File

@ -78,31 +78,9 @@ local maxCardsSkill = fk.CreateMaxCardsSkill{
end, end,
} }
local moveTokenSkill = fk.CreateTriggerSkill{
name = "move_token_skill",
global = true,
refresh_events = {fk.GameStart}, --refresh优先于on_use不要在正常的游戏开始发牌技能refresh中拿牌
can_refresh = function(self, event, target, player, data)
return player.seat == 1
end,
on_refresh = function(self, event, target, player, data)
local room = player.room
for i = #room.draw_pile, 1, -1 do
if Fk:getCardById(room.draw_pile[i]).name[1] == "&" then
local id = room.draw_pile[i]
table.removeOne(room.draw_pile, id)
table.insert(room.void, id)
room:setCardArea(id, Card.Void, nil)
end
end
end,
}
AuxSkills = { AuxSkills = {
discardSkill, discardSkill,
chooseCardsSkill, chooseCardsSkill,
choosePlayersSkill, choosePlayersSkill,
maxCardsSkill, maxCardsSkill,
moveTokenSkill,
} }

View File

@ -41,7 +41,7 @@ end
GameRule = fk.CreateTriggerSkill{ GameRule = fk.CreateTriggerSkill{
name = "game_rule", name = "game_rule",
events = { events = {
fk.GameStart, fk.GamePrepared,
fk.AskForPeaches, fk.AskForPeachesDone, fk.AskForPeaches, fk.AskForPeachesDone,
fk.GameOverJudge, fk.BuryVictim, fk.GameOverJudge, fk.BuryVictim,
}, },
@ -58,7 +58,7 @@ GameRule = fk.CreateTriggerSkill{
return false return false
end end
if event == fk.GameStart then if event == fk.GamePrepared then
room:setTag("FirstRound", true) room:setTag("FirstRound", true)
room:setTag("RoundCount", 0) room:setTag("RoundCount", 0)
return false return false

View File

@ -15,6 +15,17 @@ Fk:loadTranslationTable{
["club"] = "梅花", ["club"] = "梅花",
["diamond"] = "方块", ["diamond"] = "方块",
["basic_char"] = "",
["trick_char"] = "",
["equip_char"] = "",
["weapon"] = "武器牌",
["armor"] = "防具牌",
["defensive_horse"] = "防御坐骑牌",
["offensive_horse"] = "进攻坐骑牌",
["treasure"] = "宝物牌",
["delayed_trick"] = "延时类锦囊牌",
["slash"] = "", ["slash"] = "",
[":slash"] = "基本牌<br /><b>时机</b>:出牌阶段<br /><b>目标</b>:攻击范围内的一名其他角色<br /><b>效果</b>对目标角色造成1点伤害。", [":slash"] = "基本牌<br /><b>时机</b>:出牌阶段<br /><b>目标</b>:攻击范围内的一名其他角色<br /><b>效果</b>对目标角色造成1点伤害。",
["#slash-jink"] = "%src 对你使用了杀,请使用 %arg 张闪", ["#slash-jink"] = "%src 对你使用了杀,请使用 %arg 张闪",