// SPDX-License-Identifier: GPL-3.0-or-later import QtQuick import QtQuick.Controls import QtQuick.Layouts import QtMultimedia import Fk import Fk.Common import Fk.RoomElement import "RoomLogic.js" as Logic Item { id: roomScene property int playerNum: 0 // property var dashboardModel property bool isOwner: false property bool isStarted: false property alias popupBox: popupBox property alias manualBox: manualBox property alias bigAnim: bigAnim property alias promptText: prompt.text property alias okCancel: okCancel property alias okButton: okButton property alias cancelButton: cancelButton property alias dynamicCardArea: dynamicCardArea property alias tableCards: tablePile.cards property alias dashboard: dashboard property alias drawPile: drawPile property alias skillInteraction: skillInteraction property alias miscStatus: miscStatus property var selected_targets: [] property string responding_card property bool respond_play: false property bool autoPending: false property var extra_data: ({}) Image { source: config.roomBg anchors.fill: parent fillMode: Image.PreserveAspectCrop } MediaPlayer { id: bgm source: config.bgmFile // loops: MediaPlayer.Infinite onPlaybackStateChanged: { if (playbackState == MediaPlayer.StoppedState && roomScene.isStarted) play(); } audioOutput: AudioOutput { volume: config.bgmVolume / 100 } } onIsStartedChanged: { if (isStarted) { bgm.play(); } else { // bgm.stop(); } } // tmp DelayButton { id: quitButton text: "quit" anchors.top: parent.top anchors.right: parent.right delay: Debugging ? 10 : 1000 onActivated: { // ClientInstance.clearPlayers(); ClientInstance.notifyServer("QuitRoom", "[]"); } } Button { text: "add robot" visible: isOwner && !isStarted anchors.centerIn: parent onClicked: { ClientInstance.notifyServer("AddRobot", "[]"); } } states: [ State { name: "notactive" }, // Normal status State { name: "playing" }, // Playing cards in playing phase State { name: "responding" }, // all requests need to operate dashboard State { name: "replying" } // requests only operate a popup window ] state: "notactive" transitions: [ Transition { from: "*"; to: "notactive" ScriptAction { script: { skillInteraction.sourceComponent = undefined; promptText = ""; progress.visible = false; okCancel.visible = false; endPhaseButton.visible = false; respond_play = false; extra_data = {}; mainWindow.pending_message = []; mainWindow.is_pending = false; if (dashboard.pending_skill !== "") dashboard.stopPending(); dashboard.disableAllCards(); dashboard.disableSkills(); dashboard.retractAllPiles(); selected_targets = []; if (popupBox.item != null) { popupBox.item.finished(); } } } }, Transition { from: "*"; to: "playing" ScriptAction { script: { skillInteraction.sourceComponent = undefined; dashboard.enableCards(); dashboard.enableSkills(); progress.visible = true; okCancel.visible = true; endPhaseButton.visible = true; respond_play = false; } } }, Transition { from: "*"; to: "responding" ScriptAction { script: { skillInteraction.sourceComponent = undefined; dashboard.enableCards(responding_card); dashboard.enableSkills(responding_card, respond_play); autoPending = false; progress.visible = true; okCancel.visible = true; } } }, Transition { from: "*"; to: "replying" ScriptAction { script: { skillInteraction.sourceComponent = undefined; dashboard.disableAllCards(); dashboard.disableSkills(); progress.visible = true; respond_play = false; roomScene.okCancel.visible = false; roomScene.okButton.enabled = false; roomScene.cancelButton.enabled = false; } } } ] /* Layout: * +---------------------+ * | Photos, get more | * | in arrangePhotos() | * | tablePile | * | progress,prompt,btn | * +---------------------+ * | dashboard | * +---------------------+ */ ListModel { id: photoModel } Item { id: roomArea width: roomScene.width height: roomScene.height - dashboard.height + 20 Repeater { id: photos model: photoModel Photo { playerid: model.id general: model.general deputyGeneral: model.deputyGeneral screenName: model.screenName role: model.role kingdom: model.kingdom netstate: model.netstate maxHp: model.maxHp hp: model.hp shield: model.shield seatNumber: model.seatNumber dead: model.dead dying: model.dying faceup: model.faceup chained: model.chained drank: model.drank isOwner: model.isOwner onSelectedChanged: { Logic.updateSelectedTargets(playerid, selected); } Component.onCompleted: { if (index === 0) dashboard.self = this; } } } onWidthChanged: Logic.arrangePhotos(); onHeightChanged: Logic.arrangePhotos(); InvisibleCardArea { id: drawPile x: parent.width / 2 y: roomScene.height / 2 } TablePile { id: tablePile width: parent.width * 0.6 height: 150 x: parent.width * 0.2 y: parent.height * 0.6 + 10 } } Item { id: dashboardBtn width: childrenRect.width height: childrenRect.height anchors.bottom: parent.bottom ColumnLayout { MetroButton { text: Backend.translate("Trust") } MetroButton { text: Backend.translate("Sort Cards") } MetroButton { text: Backend.translate("Chat") onClicked: roomDrawer.open(); } } } Dashboard { id: dashboard width: roomScene.width - dashboardBtn.width anchors.top: roomArea.bottom anchors.left: dashboardBtn.right onCardSelected: function(card) { Logic.enableTargets(card); if (typeof card === "number" && card !== -1 && roomScene.state === "playing") { let skills = JSON.parse(Backend.callLuaFunction("GetCardSpecialSkills", [card])); if (JSON.parse(Backend.callLuaFunction("CanUseCard", [card, Self.id]))) { skills.unshift("_normal_use"); } specialCardSkills.model = skills; } else { specialCardSkills.model = []; } } } GlowText { text: Backend.translate("Observing ...") visible: config.observing color: "#4B83CD" font.family: fontLi2.name font.pixelSize: 48 } Item { id: controls anchors.bottom: dashboard.top anchors.bottomMargin: -60 width: roomScene.width Text { id: prompt visible: progress.visible anchors.bottom: progress.bottom z: 1 color: "#F0E5DA" font.pixelSize: 16 font.family: fontLibian.name style: Text.Outline styleColor: "#3D2D1C" textFormat: TextEdit.RichText anchors.horizontalCenter: progress.horizontalCenter } ProgressBar { id: progress width: parent.width * 0.6 anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: okCancel.top anchors.bottomMargin: 4 from: 0.0 to: 100.0 visible: false background: Rectangle { implicitWidth: 200 implicitHeight: 12 color: "black" radius: 6 } contentItem: Item { implicitWidth: 196 implicitHeight: 10 Rectangle { width: progress.visualPosition * parent.width height: parent.height radius: 6 gradient: Gradient { GradientStop { position: 0.0; color: "orange" } GradientStop { position: 0.3; color: "red" } GradientStop { position: 0.7; color: "red" } GradientStop { position: 1.0; color: "orange" } } } } NumberAnimation on value { running: progress.visible from: 100.0 to: 0.0 duration: config.roomTimeout * 1000 onFinished: { roomScene.state = "notactive" } } } Rectangle { anchors.bottom: parent.bottom anchors.bottomMargin: 8 anchors.right: okCancel.left anchors.rightMargin: 20 color: "#88EEEEEE" radius: 8 visible: roomScene.state == "playing" && (specialCardSkills.count > 1 || specialCardSkills.model[0] !== "_normal_use") width: childrenRect.width height: childrenRect.height - 20 RowLayout { y: -10 Repeater { id: specialCardSkills RadioButton { property string orig_text: modelData text: Backend.translate(modelData) checked: index === 0 onCheckedChanged: { if (modelData === "_normal_use") { Logic.enableTargets(dashboard.selected_card); } else { Logic.enableTargets(JSON.stringify({ skill: modelData, subcards: [dashboard.selected_card], })); } } } } } } Loader { id: skillInteraction visible: dashboard.pending_skill !== "" anchors.bottom: parent.bottom anchors.bottomMargin: 8 anchors.right: okCancel.left anchors.rightMargin: 20 } Row { id: okCancel anchors.bottom: parent.bottom anchors.horizontalCenter: progress.horizontalCenter spacing: 20 visible: false Button { id: okButton text: Backend.translate("OK") onClicked: Logic.doOkButton(); } Button { id: cancelButton text: Backend.translate("Cancel") onClicked: Logic.doCancelButton(); } } Button { id: endPhaseButton text: Backend.translate("End") anchors.bottom: parent.bottom anchors.bottomMargin: 40 anchors.right: parent.right anchors.rightMargin: 30 visible: false; onClicked: Logic.replyToServer(""); } } Loader { id: popupBox z: 999 onSourceChanged: { if (item === null) return; item.finished.connect(function(){ sourceComponent = undefined; }); item.widthChanged.connect(function(){ popupBox.moveToCenter(); }); item.heightChanged.connect(function(){ popupBox.moveToCenter(); }); moveToCenter(); } onSourceComponentChanged: sourceChanged(); function moveToCenter() { item.x = Math.round((roomArea.width - item.width) / 2); item.y = Math.round(roomArea.height * 0.67 - item.height / 2); } } // manualBox: same as popupBox, but must be closed manually Loader { id: manualBox z: 999 onSourceChanged: { if (item === null) return; item.finished.connect(() => sourceComponent = undefined); item.widthChanged.connect(() => manualBox.moveToCenter()); item.heightChanged.connect(() => manualBox.moveToCenter()); moveToCenter(); } onSourceComponentChanged: sourceChanged(); function moveToCenter() { item.x = Math.round((roomArea.width - item.width) / 2); item.y = Math.round(roomArea.height * 0.67 - item.height / 2); } } Loader { id: bigAnim anchors.fill: parent z: 999 } function activateSkill(skill_name, pressed) { if (pressed) { let data = JSON.parse(Backend.callLuaFunction("GetInteractionOfSkill", [skill_name])); if (data) { Backend.callLuaFunction("SetInteractionDataOfSkill", [skill_name, "null"]); switch (data.type) { case "combo": skillInteraction.sourceComponent = Qt.createComponent("../SkillInteraction/SkillCombo.qml"); skillInteraction.item.skill = skill_name; skillInteraction.item.default_choice = data["default"]; skillInteraction.item.choices = data.choices; // skillInteraction.item.clicked(); break; case "spin": skillInteraction.sourceComponent = Qt.createComponent("../SkillInteraction/SkillSpin.qml"); skillInteraction.item.skill = skill_name; skillInteraction.item.from = data.from; skillInteraction.item.to = data.to; break; default: skillInteraction.sourceComponent = undefined; break; } } else { skillInteraction.sourceComponent = undefined; } dashboard.startPending(skill_name); cancelButton.enabled = true; } else { skillInteraction.sourceComponent = undefined; Logic.doCancelButton(); } } Drawer { id: roomDrawer width: parent.width * 0.3 / mainWindow.scale height: parent.height / mainWindow.scale dim: false clip: true dragMargin: 0 scale: mainWindow.scale transformOrigin: Item.TopLeft ColumnLayout { anchors.fill: parent SwipeView { Layout.fillWidth: true Layout.fillHeight: true interactive: false currentIndex: drawerBar.currentIndex Item { LogEdit { id: log anchors.fill: parent } } Item { ChatBox { id: chat anchors.fill: parent } } } TabBar { id: drawerBar width: roomDrawer.width TabButton { width: roomDrawer.width / 2 text: Backend.translate("Log") } TabButton { width: roomDrawer.width / 2 text: Backend.translate("Chat") } } } } Drawer { id: cheatDrawer edge: Qt.RightEdge width: parent.width * 0.4 / mainWindow.scale height: parent.height / mainWindow.scale dim: false clip: true dragMargin: 0 scale: mainWindow.scale transformOrigin: Item.TopRight Loader { id: cheatLoader anchors.fill: parent onSourceChanged: { if (item === null) return; item.finish.connect(() => { cheatDrawer.close(); }); } onSourceComponentChanged: sourceChanged(); } } Item { id: dynamicCardArea anchors.fill: parent } Rectangle { id: easyChat width: parent.width height: 28 anchors.bottom: parent.bottom visible: false color: "#040403" radius: 3 border.width: 1 border.color: "#A6967A" TextInput { id: easyChatEdit anchors.fill: parent anchors.margins: 6 color: "white" clip: true font.pixelSize: 14 onAccepted: { if (text != "") { ClientInstance.notifyServer( "Chat", JSON.stringify({ type: 0, msg: text }) ); text = ""; easyChat.visible = false; easyChatEdit.enabled = false; } } } } MiscStatus { id: miscStatus anchors.right: quitButton.left anchors.top: parent.top anchors.rightMargin: 16 anchors.topMargin: 8 } Danmaku { id: danmaku width: parent.width } Shortcut { sequence: "T" onActivated: { easyChat.visible = true; easyChatEdit.enabled = true; easyChatEdit.forceActiveFocus(); } } Shortcut { sequence: "D" property bool show_distance: false onActivated: { show_distance = !show_distance; showDistance(show_distance); } } Shortcut { sequence: "Esc" onActivated: { easyChat.visible = false; easyChatEdit.enabled = false; } } Shortcut { sequence: "Return" enabled: okButton.enabled onActivated: Logic.doOkButton(); } Shortcut { sequence: "Space" enabled: cancelButton.enabled onActivated: Logic.doCancelButton(); } function getCurrentCardUseMethod() { if (specialCardSkills.count === 1 && specialCardSkills.model[0] !== "_normal_use") { return specialCardSkills.model[0]; } for (let i = 1; i < specialCardSkills.count; i++) { let item = specialCardSkills.itemAt(i); if (item.checked) { let ret = item.orig_text; return ret; } } } function addToChat(pid, raw, msg) { if (raw.type === 1) return; msg = msg.replace(/\{emoji([0-9]+)\}/g, ''); raw.msg = raw.msg.replace(/\{emoji([0-9]+)\}/g, ''); if (raw.msg.startsWith("$")) { if (specialChat(pid, raw, raw.msg.slice(1))) return; } chat.append(msg); let photo = Logic.getPhoto(pid); if (photo === undefined) { let user = raw.userName; let m = raw.msg; danmaku.sendLog(`${user}: ${m}`); return; } photo.chat(raw.msg); } function specialChat(pid, data, msg) { // skill audio: %s%d // death audio: ~%s // something special: !%s:... let time = data.time; let userName = data.userName; let general = Backend.translate(data.general); if (msg.startsWith("!")) { let splited = msg.split(":"); let type = splited[0].slice(1); switch (type) { case "Egg": case "GiantEgg": case "Shoe": case "Flower": { const fromId = pid; const toId = parseInt(splited[1]); const component = Qt.createComponent("../ChatAnim/" + type + ".qml"); //if (component.status !== Component.Ready) // return false; const fromItem = Logic.getPhotoOrDashboard(fromId); const fromPos = mapFromItem(fromItem, fromItem.width / 2, fromItem.height / 2); const toItem = Logic.getPhoto(toId); const toPos = mapFromItem(toItem, toItem.width / 2, toItem.height / 2); const egg = component.createObject(roomScene, { start: fromPos, end: toPos }); egg.finished.connect(() => egg.destroy()); egg.running = true; return true; } default: return false; } } else if (msg.startsWith("~")) { let g = msg.slice(1); let extension = JSON.parse(Backend.callLuaFunction("GetGeneralData", [g])).extension; if (!config.disableMsgAudio) Backend.playSound("./packages/" + extension + "/audio/death/" + g); let m = Backend.translate("~" + g); if (general === "") chat.append(`[${time}] ${userName}: ${m}`); else chat.append(`[${time}] ${userName}(${general}): ${m}`); let photo = Logic.getPhoto(pid); if (photo === undefined) { danmaku.sendLog(`${userName}: ${m}`); return true; } photo.chat(m); return true; } else { let splited = msg.split(":"); if (splited.length < 2) return false; let skill = splited[0]; let idx = parseInt(splited[1]); let data2 = JSON.parse(Backend.callLuaFunction("GetSkillData", [skill])); if (!data2) return false; let extension = data2.extension; if (!config.disableMsgAudio) Backend.playSound("./packages/" + extension + "/audio/skill/" + skill, idx); let m = Backend.translate("$" + skill + idx.toString()); if (general === "") chat.append(`[${time}] ${userName}: ${m}`); else chat.append(`[${time}] ${userName}(${general}): ${m}`); let photo = Logic.getPhoto(pid); if (photo === undefined) { danmaku.sendLog(`${userName}: ${m}`); return true; } photo.chat(m); return true; } return false; } function addToLog(msg) { log.append(msg); } function sendDanmaku(msg) { danmaku.sendLog(msg); chat.append(msg); } function showDistance(show) { for (let i = 0; i < photoModel.count; i++) { let item = photos.itemAt(i); if (show) { let dis = Backend.callLuaFunction("DistanceTo",[Self.id, item.playerid]); item.distance = parseInt(dis); } else { item.distance = 0; } } } function startCheat(type, data) { cheatLoader.sourceComponent = Qt.createComponent(`../Cheat/${type}.qml`); cheatLoader.item.extra_data = data; cheatDrawer.open(); } Component.onCompleted: { toast.show(Backend.translate("$EnterRoom")); playerNum = config.roomCapacity; for (let i = 0; i < playerNum; i++) { photoModel.append({ id: i ? -1 : Self.id, index: i, // For animating seat swap general: i ? "" : Self.avatar, deputyGeneral: "", screenName: i ? "" : Self.screenName, role: "unknown", kingdom: "unknown", netstate: "online", maxHp: 0, hp: 0, shield: 0, seatNumber: i + 1, dead: false, dying: false, faceup: true, chained: false, drank: 0, isOwner: false }); } Logic.arrangePhotos(); } }