// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Controls
import QtQuick.Dialogs
import QtQuick.Layouts
import QtMultimedia
import Fk
import Fk.Common
import Fk.RoomElement
import Fk.PhotoElement as PhotoElement
import "RoomLogic.js" as Logic
Item {
id: roomScene
property int playerNum: 0
// property var dashboardModel
property bool isOwner: false
property bool isStarted: false
property bool isFull: false
property bool isAllReady: false
property bool isReady: false
property bool canKickOwner: false
property alias popupBox: popupBox
property alias manualBox: manualBox
property alias bigAnim: bigAnim
property alias promptText: prompt.text
property var currentPrompt
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 alias banner: banner
property var selected_targets: []
property string responding_card
property var extra_data: ({})
property var skippedUseEventId: []
property real replayerSpeed
property int replayerElapsed
property int replayerDuration
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) {
Backend.playSound("./audio/system/gamestart");
bgm.play();
canKickOwner = false;
kickOwnerTimer.stop();
} else {
bgm.stop();
}
}
Button {
id: menuButton
anchors.top: parent.top
anchors.topMargin: 12
anchors.right: parent.right
anchors.rightMargin: 12
text: luatr("Menu")
onClicked: {
if (menuContainer.visible){
menuContainer.close();
} else {
menuContainer.open();
}
}
Menu {
id: menuContainer
y: menuButton.height - 12
width: parent.width * 1.8
MenuItem {
id: quitButton
text: luatr("Quit")
icon.source: AppPath + "/image/modmaker/back"
onClicked: {
if (config.replaying) {
Backend.controlReplayer("shutdown");
mainStack.pop();
} else if (config.observing) {
ClientInstance.notifyServer("QuitRoom", "[]");
} else {
quitDialog.open();
}
}
}
MenuItem {
id: volumeButton
text: luatr("Audio Settings")
icon.source: AppPath + "/image/button/tileicon/configure"
onClicked: {
volumeDialog.open();
}
}
Menu {
title: luatr("Overview")
icon.source: AppPath + "/image/button/tileicon/rule_summary"
icon.width: 24
icon.height: 24
icon.color: palette.windowText
MenuItem {
id: generalButton
text: luatr("Generals Overview")
icon.source: AppPath + "/image/button/tileicon/general_overview"
onClicked: {
overviewLoader.overviewType = "Generals";
overviewDialog.open();
overviewLoader.item.loadPackages();
}
}
MenuItem {
id: cardslButton
text: luatr("Cards Overview")
icon.source: AppPath + "/image/button/tileicon/card_overview"
onClicked: {
overviewLoader.overviewType = "Cards";
overviewDialog.open();
overviewLoader.item.loadPackages();
}
}
MenuItem {
id: modesButton
text: luatr("Modes Overview")
icon.source: AppPath + "/image/misc/paper"
onClicked: {
overviewLoader.overviewType = "Modes";
overviewDialog.open();
}
}
}
MenuItem {
id: surrenderButton
enabled: !config.observing && !config.replaying && isStarted
text: luatr("Surrender")
icon.source: AppPath + "/image/misc/surrender"
onClicked: {
const photo = getPhoto(Self.id);
if (isStarted && !(photo.dead && photo.rest <= 0)) {
const surrenderCheck = lcall('CheckSurrenderAvailable', miscStatus.playedTime);
if (!surrenderCheck.length) {
surrenderDialog.informativeText =
luatr('Surrender is disabled in this mode');
} else {
surrenderDialog.informativeText = surrenderCheck
.map(str => `${luatr(str.text)}(${str.passed ? '✓' : '✗'})`)
.join('
');
}
surrenderDialog.open();
}
}
}
}
}
Button {
text: luatr("Add Robot")
visible: isOwner && !isStarted && !isFull
anchors.centerIn: parent
enabled: config.serverEnableBot
onClicked: {
ClientInstance.notifyServer("AddRobot", "[]");
}
}
Button {
text: luatr("Start Game")
visible: isOwner && !isStarted && isFull
enabled: isAllReady
anchors.centerIn: parent
onClicked: {
ClientInstance.notifyServer("StartGame", "[]");
}
}
Timer {
id: opTimer
interval: 1000
}
Button {
text: isReady ? luatr("Cancel Ready") : luatr("Ready")
visible: !isOwner && !isStarted
enabled: !opTimer.running
anchors.centerIn: parent
onClicked: {
opTimer.start();
ClientInstance.notifyServer("Ready", "");
}
}
Button {
id: kickOwner
anchors.horizontalCenter: parent.horizontalCenter
y: parent.height / 2 + 30
text: "踢出房主"
visible: canKickOwner && !isStarted && isFull && !isOwner
onClicked: {
for (let i = 0; i < photoModel.count; i++) {
let item = photoModel.get(i);
if (item.isOwner) {
ClientInstance.notifyServer("KickPlayer", item.id.toString());
}
}
}
}
Timer {
id: kickOwnerTimer
interval: 15000
onTriggered: {
canKickOwner = true;
}
}
onIsAllReadyChanged: {
if (!isAllReady) {
canKickOwner = false;
kickOwnerTimer.stop();
} else {
Backend.playSound("./audio/system/ready");
kickOwnerTimer.start();
}
}
Rectangle {
x: parent.width / 2 + 60
y: parent.height / 2 - 30
color: "snow"
opacity: 0.8
radius: 6
visible: !isStarted
width: 280
height: 280
Flickable {
id: flickableContainer
ScrollBar.vertical: ScrollBar {}
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 10
flickableDirection: Flickable.VerticalFlick
width: parent.width - 10
height: parent.height - 10
contentHeight: roominfo.height
clip: true
Text {
id: roominfo
font.pixelSize: 16
width: parent.width
wrapMode: TextEdit.WordWrap
Component.onCompleted: {
const data = lcall("GetRoomConfig");
let cardpack = lcall("GetAllCardPack");
cardpack = cardpack.filter(p => !data.disabledPack.includes(p));
text = luatr("GameMode") + luatr(data.gameMode) + "
"
+ luatr("LuckCardNum") + "" + data.luckTime + "
"
+ luatr("ResponseTime") + "" + config.roomTimeout + "
"
+ luatr("GeneralBoxNum") + "" + data.generalNum + ""
+ (data.enableFreeAssign ? "
" + luatr("IncludeFreeAssign")
: "")
+ (data.enableDeputy ? " " + luatr("IncludeDeputy") : "")
+ '
' + luatr('CardPackages') + cardpack.map(e => {
let ret = luatr(e);
// TODO: 这种东西最好还是变量名规范化= =
if (ret.search(/特殊牌|衍生牌/) === -1) {
ret = "" + ret + "";
}
return ret;
}).join(',');
}
}
}
}
states: [
State { name: "notactive" },
State { name: "active" }
]
state: "notactive"
transitions: [
Transition {
from: "*"; to: "notactive"
ScriptAction {
script: {
skillInteraction.sourceComponent = undefined;
promptText = "";
okCancel.visible = false;
okButton.enabled = false;
cancelButton.enabled = false;
endPhaseButton.visible = false;
progress.visible = false;
extra_data = {};
dashboard.disableAllCards();
dashboard.disableSkills();
dashboard.pending_skill = "";
// dashboard.retractAllPiles();
for (let i = 0; i < photoModel.count; i++) {
const item = photos.itemAt(i);
item.state = "normal";
item.selected = false;
// item.selectable = false;
}
if (popupBox.item != null) {
popupBox.item.finished();
}
lcall("FinishRequestUI");
applyChange({});
}
}
},
Transition {
from: "notactive"; to: "active"
ScriptAction {
script: {
const dat = Backend.getRequestData();
const total = dat["timeout"] * 1000;
const now = Date.now(); // ms
const elapsed = now - (dat["timestamp"] ?? now);
if (total <= elapsed) {
roomScene.state = "notactive";
}
progressAnim.from = (1 - elapsed / total) * 100.0;
progressAnim.duration = total - elapsed;
progress.visible = true;
}
}
}
]
/* 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
avatar: model.avatar
deputyGeneral: model.deputyGeneral
screenName: model.screenName
role: model.role
role_shown: model.role_shown
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
rest: model.rest
isOwner: model.isOwner
ready: model.ready
surrendered: model.surrendered
sealedSlots: JSON.parse(model.sealedSlots)
onSelectedChanged: {
if ( state === "candidate" ) lcall("UpdateRequestUI", "Photo", playerid, "click", { 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
anchors.bottomMargin: 8
anchors.left: parent.left
anchors.leftMargin: 8
ColumnLayout {
MetroButton {
text: luatr("Choose one handcard")
textFont.pixelSize: 28
visible: {
if (roomScene.state === "notactive") return false;
if (dashboard.handcardArea.length <= 15) {
return false;
}
const cards = dashboard.handcardArea.cards;
for (const card of cards) {
if (card.selectable) return true;
}
return false;
}
onClicked: roomScene.startCheat("../RoomElement/ChooseHandcard");
}
MetroButton {
id: revertSelectionBtn
text: luatr("Revert Selection")
textFont.pixelSize: 28
enabled: dashboard.pending_skill !== ""
onClicked: //dashboard.revertSelection();
{
lcall("RevertSelection");
}
}
// MetroButton {
// text: luatr("Trust")
// }
MetroButton {
text: luatr("Sort Cards")
textFont.pixelSize: 28
onClicked: {
if (lcall("CanSortHandcards", Self.id)) {
let sortMethods = [];
for (let index = 0; index < sortMenuRepeater.count; index++) {
var tCheckBox = sortMenuRepeater.itemAt(index)
sortMethods.push(tCheckBox.checked)
}
Logic.sortHandcards(sortMethods);
}
}
onRightClicked: {
if (sortMenu.visible) {
sortMenu.close();
} else {
sortMenu.open();
}
}
Menu {
id: sortMenu
x: parent.width
y: -25
width: parent.width * 2
background: Rectangle {
color: "black"
border.width: 3
border.color: "white"
opacity: 0.8
}
Repeater {
id: sortMenuRepeater
model: ["Sort by Type", "Sort by Number", "Sort by Suit"]
CheckBox {
id: control
text: "" + luatr(modelData) + ""
checked: modelData === "Sort by Type"
font.pixelSize: 20
indicator: Rectangle {
implicitWidth: 26
implicitHeight: 26
x: control.leftPadding
y: control.height / 2 - height / 2
radius: 3
border.color: "white"
Rectangle {
width: 14
height: 14
x: 6
y: 6
radius: 2
color: control.down ? "#17a81a" : "#21be2b"
visible: control.checked
}
}
}
}
}
}
MetroButton {
text: luatr("Chat")
textFont.pixelSize: 28
onClicked: roomDrawer.open();
}
}
}
Dashboard {
id: dashboard
width: roomScene.width - dashboardBtn.width
anchors.top: roomArea.bottom
anchors.left: dashboardBtn.right
}
Rectangle {
id: replayControls
visible: config.replaying
anchors.bottom: dashboard.top
anchors.bottomMargin: -60
anchors.horizontalCenter: parent.horizontalCenter
width: childrenRect.width + 8
height: childrenRect.height + 8
color: "#88EEEEEE"
radius: 4
RowLayout {
x: 4; y: 4
Text {
font.pixelSize: 20
font.bold: true
text: {
const elapsedMin = Math.floor(replayerElapsed / 60);
const elapsedSec = replayerElapsed % 60;
const totalMin = Math.floor(replayerDuration / 60);
const totalSec = replayerDuration % 60;
return elapsedMin.toString() + ":" + elapsedSec + "/" + totalMin
+ ":" + totalSec;
}
}
Switch {
text: luatr("Show All Cards")
checked: config.replayingShowCards
onCheckedChanged: config.replayingShowCards = checked;
}
Switch {
text: luatr("Speed Resume")
checked: false
onCheckedChanged: Backend.controlReplayer("uniform");
}
Button {
text: luatr("Speed Down")
onClicked: Backend.controlReplayer("slowdown");
}
Text {
font.pixelSize: 20
font.bold: true
text: "x" + replayerSpeed;
}
Button {
text: luatr("Speed Up")
onClicked: Backend.controlReplayer("speedup");
}
Button {
property bool running: true
text: luatr(running ? "Pause" : "Resume")
onClicked: {
running = !running;
Backend.controlReplayer("toggle");
}
}
}
}
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 {
id: progressAnim
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: {
if (roomScene.state !== "active") {
return false;
}
if (!specialCardSkills) {
return false;
}
if (specialCardSkills.count > 1) {
return true;
}
return (specialCardSkills.model ?? false)
&& 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: luatr(modelData)
checked: index === 0
onCheckedChanged: {
lcall("UpdateRequestUI", "SpecialSkills", "1", "click", modelData);
}
}
}
}
}
Loader {
id: skillInteraction
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: skipNullificationButton
text: luatr("SkipNullification")
visible: !!extra_data.useEventId
&& !skippedUseEventId.find(id => id === extra_data.useEventId)
onClicked: {
skippedUseEventId.push(extra_data.useEventId);
lcall("UpdateRequestUI", "Button", "Cancel");
}
}
Button {
id: okButton
text: luatr("OK")
onClicked: lcall("UpdateRequestUI", "Button", "OK");
}
Button {
id: cancelButton
text: luatr("Cancel")
onClicked: lcall("UpdateRequestUI", "Button", "Cancel");
}
}
Button {
id: endPhaseButton
text: luatr("End")
anchors.bottom: parent.bottom
anchors.bottomMargin: 40
anchors.right: parent.right
anchors.rightMargin: 30
visible: false;
onClicked: lcall("UpdateRequestUI", "Button", "End");
}
}
// 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: popupBox
z: 999
onSourceChanged: {
if (item === null)
return;
item.finished.connect(() => {
sourceComponent = undefined;
});
item.widthChanged.connect(() => {
popupBox.moveToCenter();
});
item.heightChanged.connect(() => {
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);
}
}
Loader {
id: bigAnim
anchors.fill: parent
z: 999
}
function activateSkill(skill_name, selected) {
lcall("UpdateRequestUI", "SkillButton", skill_name, "click", { selected } );
}
Drawer {
id: roomDrawer
width: parent.width * 0.36 / 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 {
visible: !config.replaying
AvatarChatBox {
id: chat
anchors.fill: parent
}
}
}
TabBar {
id: drawerBar
width: roomDrawer.width
TabButton {
width: roomDrawer.width / 2
text: luatr("Log")
}
TabButton {
width: roomDrawer.width / 2
text: luatr("Chat")
}
}
}
}
Popup {
id: cheatDrawer
width: realMainWin.width * 0.60
height: realMainWin.height * 0.8
anchors.centerIn: parent
background: Rectangle {
color: "#CC2E2C27"
radius: 5
border.color: "#A6967A"
border.width: 1
}
Loader {
id: cheatLoader
anchors.centerIn: parent
width: parent.width / mainWindow.scale
height: parent.height / mainWindow.scale
scale: mainWindow.scale
clip: true
onSourceChanged: {
if (item === null)
return;
item.finish.connect(() => {
cheatDrawer.close();
});
}
onSourceComponentChanged: sourceChanged();
}
}
Item {
id: dynamicCardArea
anchors.fill: parent
}
MessageDialog {
id: quitDialog
title: luatr("Quit")
informativeText: luatr("Are you sure to quit?")
buttons: MessageDialog.Ok | MessageDialog.Cancel
onButtonClicked: function (button) {
switch (button) {
case MessageDialog.Ok: {
ClientInstance.notifyServer("QuitRoom", "[]");
break;
}
case MessageDialog.Cancel: {
quitDialog.close();
}
}
}
}
MessageDialog {
id: surrenderDialog
title: luatr("Surrender")
informativeText: ''
buttons: MessageDialog.Ok | MessageDialog.Cancel
onButtonClicked: function (button, role) {
switch (button) {
case MessageDialog.Ok: {
const surrenderCheck =
lcall('CheckSurrenderAvailable', miscStatus.playedTime);
if (surrenderCheck.length &&
!surrenderCheck.find(check => !check.passed)) {
ClientInstance.notifyServer("PushRequest", [
"surrender", true
]);
}
surrenderDialog.close();
break;
}
case MessageDialog.Cancel: {
surrenderDialog.close();
}
}
}
}
Popup {
id: volumeDialog
width: realMainWin.width * 0.5
height: realMainWin.height * 0.5
anchors.centerIn: parent
background: Rectangle {
color: "#EEEEEEEE"
radius: 5
border.color: "#A6967A"
border.width: 1
}
Loader {
anchors.centerIn: parent
width: parent.width / mainWindow.scale
height: parent.height / mainWindow.scale
scale: mainWindow.scale
source: AppPath + "/Fk/LobbyElement/AudioSetting.qml"
}
}
Popup {
id: overviewDialog
width: realMainWin.width * 0.75
height: realMainWin.height * 0.75
anchors.centerIn: parent
background: Rectangle {
color: "#EEEEEEEE"
radius: 5
border.color: "#A6967A"
border.width: 1
}
Loader {
id: overviewLoader
property string overviewType: "Generals"
anchors.centerIn: parent
width: parent.width / mainWindow.scale
height: parent.height / mainWindow.scale
scale: mainWindow.scale
source: AppPath + "/Fk/Pages/" + overviewType + "Overview.qml"
}
}
GlowText {
anchors.centerIn: dashboard
visible: Logic.getPhoto(Self.id).rest > 0 && !config.observing
text: luatr("Resting, don't leave!")
color: "#DBCC69"
font.family: fontLibian.name
font.pixelSize: 28
glow.color: "#2E200F"
glow.spread: 0.6
}
Rectangle {
anchors.fill: dashboard
visible: config.observing && !config.replaying
color: "transparent"
GlowText {
anchors.centerIn: parent
text: luatr("Observing ...")
color: "#4B83CD"
font.family: fontLi2.name
font.pixelSize: 48
}
}
MiscStatus {
id: miscStatus
anchors.right: menuButton.left
anchors.top: parent.top
anchors.rightMargin: 16
anchors.topMargin: 8
}
PhotoElement.MarkArea {
id: banner
x: 12; y: 12
width: (roomScene.width - 175 * 0.75 * 7) / 4 + 175 - 16
transformOrigin: Item.TopLeft
scale: 0.75
bgColor: "#BB838AEA"
}
Danmaku {
id: danmaku
width: parent.width
}
Shortcut {
sequence: "D"
property bool show_distance: false
onActivated: {
show_distance = !show_distance;
showDistance(show_distance);
}
}
Shortcut {
sequence: "T"
onActivated: {
roomDrawer.open();
}
}
Shortcut {
sequence: "Return"
enabled: okButton.enabled
onActivated: lcall("UpdateRequestUI", "Button", "OK");
}
Shortcut {
sequence: "Space"
enabled: cancelButton.enabled || endPhaseButton.visible;
onActivated: if (cancelButton.enabled) {
lcall("UpdateRequestUI", "Button", "Cancel");
} else {
Logic.replyToServer("");
}
}
Shortcut {
sequence: "Escape"
onActivated: menuContainer.open();
}
Timer {
id: statusSkillTimer
interval: 200
running: isStarted
repeat: true
onTriggered: {
lcall("RefreshStatusSkills");
// 刷大家的明置手牌提示框
for (let i = 0; i < photos.count; i++)
photos.itemAt(i).handcardsChanged();
}
}
function addToChat(pid, raw, msg) {
if (raw.type === 1) return;
const photo = Logic.getPhoto(pid);
if (photo === undefined && config.hideObserverChatter)
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, raw);
if (photo === undefined) {
const user = raw.userName;
const m = raw.msg;
danmaku.sendLog(`${user}: ${m}`);
return;
}
photo.chat(raw.msg);
}
function specialChat(pid, data, msg) {
// skill audio: %s%d[%s]
// death audio: ~%s
// something special: !%s:...
const time = data.time;
const userName = data.userName;
const general = luatr(data.general);
if (msg.startsWith("!")) {
if (config.hidePresents)
return true;
const splited = msg.split(":");
const type = splited[0].slice(1);
switch (type) {
case "Egg":
case "GiantEgg":
case "Shoe":
case "Wine":
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("~")) {
const g = msg.slice(1);
const extension = lcall("GetGeneralData", g).extension;
if (!config.disableMsgAudio)
Backend.playSound("./packages/" + extension + "/audio/death/" + g);
const m = luatr("~" + g);
data.msg = m;
if (general === "")
chat.append(`[${time}] ${userName}: ${m}`, data);
else
chat.append(`[${time}] ${userName}(${general}): ${m}`, data);
const photo = Logic.getPhoto(pid);
if (photo === undefined) {
danmaku.sendLog(`${userName}: ${m}`);
return true;
}
photo.chat(m);
return true;
} else {
const splited = msg.split(":");
if (splited.length < 2) return false;
const skill = splited[0];
const idx = parseInt(splited[1]);
const gene = splited[2];
if (!config.disableMsgAudio)
try {
callbacks["LogEvent"]({
type: "PlaySkillSound",
name: skill,
general: gene,
i: idx,
});
} catch (e) {}
const m = luatr("$" + skill + (gene ? "_" + gene : "")
+ (idx ? idx.toString() : ""));
data.msg = m;
if (general === "")
chat.append(`[${time}] ${userName}: ${m}`, data);
else
chat.append(`[${time}] ${userName}(${general}): ${m}`, data)
const 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({ logText: msg });
}
function sendDanmaku(msg) {
danmaku.sendLog(msg);
chat.append(null, {
msg: msg,
general: "__server", // FIXME: 基于默认读取貂蝉的数据
userName: "",
time: "Server",
});
}
function showDistance(show) {
for (let i = 0; i < photoModel.count; i++) {
const item = photos.itemAt(i);
if (show) {
item.distance = lcall("DistanceTo", Self.id, item.playerid);
} else {
item.distance = -1;
}
}
}
function startCheat(type, data) {
cheatLoader.sourceComponent = Qt.createComponent(`../Cheat/${type}.qml`);
cheatLoader.item.extra_data = data;
cheatDrawer.open();
}
function resetToInit() {
const datalist = [];
for (let i = 0; i < photoModel.count; i++) {
const item = photoModel.get(i);
let gameData;
try {
gameData = lcall("GetPlayerGameData", item.id);
} catch (e) {
console.log(e);
gameData = [0, 0, 0, 0];
}
if (item.id > 0) {
datalist.push({
id: item.id,
avatar: item.avatar,
name: item.screenName,
isOwner: item.isOwner,
ready: item.ready,
gameData: gameData,
});
}
}
mainStack.pop();
lcall("ResetClientLua");
mainStack.push(room);
mainStack.currentItem.loadPlayerData(datalist);
}
function setPrompt(text, iscur) {
promptText = text;
if (iscur) currentPrompt = text;
}
function resetPrompt() {
promptText = currentPrompt;
}
function loadPlayerData(datalist) {
datalist.forEach(d => {
if (d.id === Self.id) {
roomScene.isOwner = d.isOwner;
} else {
lcall("ResetAddPlayer",
[d.id, d.name, d.avatar, d.ready, d.gameData[3]]);
}
lcall("SetPlayerGameData", d.id, d.gameData);
Logic.getPhotoModel(d.id).isOwner = d.isOwner;
});
}
function getPhoto(id) {
return Logic.getPhoto(id);
}
function activate() {
if (state === "active") state = "notactive";
state = "active";
}
function applyChange(uiUpdate) {
const sskilldata = uiUpdate["SpecialSkills"]?.[0]
if (sskilldata) {
specialCardSkills.model = sskilldata?.skills ?? [];
}
dashboard.applyChange(uiUpdate);
const pdatas = uiUpdate["Photo"];
pdatas?.forEach(pdata => {
const photo = Logic.getPhoto(pdata.id);
photo.state = pdata.state;
photo.selectable = pdata.enabled;
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"];
if (buttons) {
okCancel.visible = true;
}
buttons?.forEach(bdata => {
switch (bdata.id) {
case "OK":
okButton.enabled = bdata.enabled;
break;
case "Cancel":
cancelButton.enabled = bdata.enabled;
break;
case "End":
endPhaseButton.enabled = bdata.enabled;
endPhaseButton.visible = bdata.enabled;
break;
}
})
// Interaction最后上桌 太给脸了居然插结
uiUpdate["_delete"]?.forEach(data => {
if (data.type == "Interaction") {
skillInteraction.sourceComponent = undefined;
if (roomScene.popupBox.item)
roomScene.popupBox.item.close();
}
});
uiUpdate["_new"]?.forEach(dat => {
if (dat.type == "Interaction") {
const data = dat.data.spec;
const skill_name = dat.data.skill_name;
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.detailed = data.detailed;
skillInteraction.item.all_choices = data.all_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;
skillInteraction.item?.clicked();
break;
case "custom":
skillInteraction.sourceComponent =
Qt.createComponent(AppPath + "/" + data.qml_path + ".qml");
skillInteraction.item.skill = skill_name;
skillInteraction.item.extra_data = data;
skillInteraction.item?.clicked();
break;
default:
skillInteraction.sourceComponent = undefined;
break;
}
}
});
}
Component.onCompleted: {
toast.show(luatr("$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,
avatar: i ? "" : Self.avatar,
deputyGeneral: "",
screenName: i ? "" : Self.screenName,
role: "unknown",
role_shown: false,
kingdom: "unknown",
netstate: "online",
maxHp: 0,
hp: 0,
shield: 0,
seatNumber: i + 1,
dead: false,
dying: false,
faceup: true,
chained: false,
drank: 0,
rest: 0,
isOwner: false,
ready: false,
surrendered: false,
sealedSlots: "[]",
});
}
Logic.arrangePhotos();
}
}