1
0

feat: make gui?

This commit is contained in:
2026-06-04 19:53:41 +08:00
parent af2d264346
commit 5e652a3207
20 changed files with 836 additions and 2 deletions
+28
View File
@@ -0,0 +1,28 @@
Round 1
- Item name is announced in chat
- First person to find in inventory and send wins 1 point
Round 2
- Clues are given that relate to the item
- Person that guess the item win 1 point
- Then wait 1 min to showcase highest quantity
- Person with the most item quantity win 1 point
### Items
Round 1
1. Hero's Radiance
2. Jellyfish
3. Medal of Light (quest)
4. S2 Exquisite box
5. Focus Potion
6. Toy - ee chan fireworks
Round 2
1. Mixing Agent
2. Cheer up treat
3. Gold quicksand
4. Emerald apple
5. Honey
6. Slumberdream feathers
7. Lionfish
+3 -2
View File
@@ -21,6 +21,7 @@ __all__ = (
"ChitChatNtf", "ChitChatNtf",
"HypertextVariant", "HypertextVariant",
"decode_placeholder", "decode_placeholder",
"ITEM_MAPPINGS",
"get_item_name" "get_item_name"
) )
@@ -75,11 +76,11 @@ def decode_placeholder(placeholder: PlaceHolder) -> SupportedPlaceholders:
return decoder.FromString(placeholder.bytesContent) # type: ignore[attr-defined] return decoder.FromString(placeholder.bytesContent) # type: ignore[attr-defined]
ITEM_NAME = pl.read_json("./ref/StarResonanceData/ztable/ItemTable.json").transpose().unnest() ITEM_MAPPINGS = pl.read_json("./ref/StarResonanceData/ztable/ItemTable.json").transpose().unnest()
def get_item_name(item_config_id: int) -> str | None: def get_item_name(item_config_id: int) -> str | None:
try: try:
return ITEM_NAME.filter(pl.col.Id == item_config_id).select("Name").item() return ITEM_MAPPINGS.filter(pl.col.Id == item_config_id).select("Name").item()
except ValueError: except ValueError:
return None return None
+4
View File
@@ -31,6 +31,10 @@ class Game:
self.scapy = get_sniffer(sniffer) self.scapy = get_sniffer(sniffer)
self.scapy.start() self.scapy.start()
@property
def state(self) -> State:
return self._state
def set_state(self, state: State) -> None: def set_state(self, state: State) -> None:
self._state = state self._state = state
@@ -0,0 +1,106 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Effects
import QtQuick.Shapes
import Qt.labs.qmlmodels
import InventoryWars
import Style
Rectangle {
id: root
color: UIStyle.background
required property ListModel rounds
property QtObject selectedRound
signal roundSelected()
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 20
Item { Layout.fillHeight: true }
Image {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: 256
source: "images/modules.png"
fillMode: Image.PreserveAspectFit
}
Label {
Layout.alignment: Qt.AlignHCenter
font.pixelSize: UIStyle.fontSizeXL
color: UIStyle.titletextColor
text: "Start a round"
}
ComboBox {
id: roundSelect
Layout.alignment: Qt.AlignHCenter
model: root.rounds
textRole: "name"
}
Button {
Layout.alignment: Qt.AlignHCenter
buttonColor: UIStyle.highlightColor
buttonBorderColor: UIStyle.highlightBorderColor
textColor: UIStyle.textColor
text: "Start"
onClicked: {
root.selectedRound = rounds.get(roundSelect.currentIndex)
roundSelected()
}
}
TableView {
Layout.fillHeight: true
columnSpacing: 1
rowSpacing: 1
delegate: Rectangle {
border.width: 1
implicitHeight: 50
implicitWidth: 100
Text {
anchors.centerIn: parent
text: display
}
}
model: TableModel {
rows: [
{
User: "me",
Score: "1"
}
]
TableModelColumn {
display: "User"
}
TableModelColumn {
display: "Score"
}
}
}
Item { Layout.fillHeight: true }
}
}
@@ -0,0 +1,85 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Effects
import QtQuick.Shapes
import Qt.labs.qmlmodels
import InventoryWars
ApplicationWindow {
height: 480
title: "Inventory Wars"
visible: true
width: 640
ListModel {
id: rounds
ListElement {
name: "Round 1"
mode: GameService.GameScoring.FirstGuess
item_id: 100
item_name: "Item 1"
item_image: ""
clue: "Clue 1"
}
ListElement {
name: "Round 2"
mode: GameService.GameScoring.FirstGuess
item_id: 100
item_name: "Item 2"
item_image: ""
clue: "Clue 2"
}
ListElement {
name: "Round 3"
mode: GameService.GameScoring.FirstGuess
item_id: 100
item_name: "Item 3"
item_image: ""
clue: "Clue 3"
}
ListElement {
name: "Round 4"
mode: GameService.GameScoring.FirstGuess
item_id: 100
item_name: "Item 4"
item_image: ""
clue: "Clue 4"
}
}
IdleView {
id: idleView
anchors.fill: parent
rounds: rounds
selectedRound: rounds.get(0)
onRoundSelected: {
idleView.visible = false
ongoingView.start()
ongoingView.visible = true
}
}
OngoingView {
id: ongoingView
anchors.fill: parent
visible: false
round: idleView.selectedRound
gameService: gameService
onClose: {
idleView.visible = true
ongoingView.visible = false
}
}
GameService {
id: gameService
}
}
@@ -0,0 +1,188 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Effects
import QtQuick.Shapes
import Qt.labs.qmlmodels
import InventoryWars
import Style
Rectangle {
id: root
color: UIStyle.background
required property GameService gameService
required property QtObject round
property QtObject guessed: QtObject {
property string username
property double seconds
}
property QtObject highest: QtObject {
property string username
property int count
}
function start() {
gameService.start(round.item_id, round.mode)
}
signal close()
Connections {
target: root.gameService
function onGuessed(username: str, seconds: double) {
root.guessed.username = username
root.guessed.seconds = seconds
}
function onHighest(username: str, count: int) {
root.highest.username = username
root.highest.count = count
}
}
ColumnLayout {
anchors.fill: parent
anchors.bottomMargin: 10
spacing: 5
ToolBar {
Layout.fillWidth: true
Layout.minimumHeight: 35
RowLayout {
anchors.fill: parent
anchors.leftMargin: 5
anchors.rightMargin: 5
Layout.alignment: Qt.AlignVCenter
Button {
Layout.preferredWidth: 25
Layout.preferredHeight: 25
icon.name: "back"
icon.source: "icons/x.svg"
onClicked: root.close()
}
Item {
Layout.fillWidth: true
}
Text {
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
font.pixelSize: UIStyle.fontSizeM
font.bold: true
color: UIStyle.titletextColor
text: root.round.name
}
Item {
Layout.fillWidth: true
}
}
}
Item { Layout.fillHeight: true }
Label {
Layout.alignment: Qt.AlignHCenter
font.pixelSize: UIStyle.fontSizeL
font.bold: true
color: UIStyle.titletextColor
text: "Current Item:"
}
Text {
Layout.alignment: Qt.AlignHCenter
font.pixelSize: UIStyle.fontSizeM
color: UIStyle.textColor
text: root.gameService.state == GameService.Ongoing ? "???" : root.round.item_name
}
Image {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: 256
Layout.preferredHeight: 256
source: root.gameService.state == GameService.Ongoing ? "images/unknown.png" : root.round.item_image
fillMode: Image.PreserveAspectFit
}
Text {
Layout.alignment: Qt.AlignHCenter
font.pixelSize: UIStyle.fontSizeXL
font.bold: true
color: UIStyle.textColor
text: ""
Timer {
id: elapsedTimer
interval: 10
running: root.gameService.state != GameService.Ended
repeat: true
onTriggered: {
parent.text = root.gameService.elapsedSeconds.toFixed(2) + "s"
}
}
}
Text {
Layout.alignment: Qt.AlignHCenter
font.pixelSize: UIStyle.fontSizeM
font.bold: true
color: UIStyle.textColor
visible: root.gameService.state == GameService.GameState.Hidden
text: "Clue: " + root.round.clue
}
Text {
Layout.alignment: Qt.AlignHCenter
font.pixelSize: UIStyle.fontSizeM
font.bold: true
color: UIStyle.textColor
visible: root.gameService.state != GameService.GameState.Hidden
text: "Fastest guess: " + root.guessed.seconds + "s (by " + root.guessed.username + ")"
}
Text {
Layout.alignment: Qt.AlignHCenter
font.pixelSize: UIStyle.fontSizeM
font.bold: true
color: UIStyle.textColor
visible: root.gameService.state != GameService.GameState.Hidden && root.round.mode == GameService.GameScoring.FirstGuessThenHighestAmount
text: "Highest count: " + root.highest.count + " (by " + root.highest.username + ")"
}
Item { Layout.fillHeight: true }
Button {
Layout.alignment: Qt.AlignHCenter
visible: root.gameService.state != GameService.GameState.Ended
text: "End"
onClicked: root.gameService.end()
}
}
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M504.6 148.5C515.9 134.9 514.1 114.7 500.5 103.4C486.9 92.1 466.7 93.9 455.4 107.5L320 270L184.6 107.5C173.3 93.9 153.1 92.1 139.5 103.4C125.9 114.7 124.1 134.9 135.4 148.5L278.3 320L135.4 491.5C124.1 505.1 125.9 525.3 139.5 536.6C153.1 547.9 173.3 546.1 184.6 532.5L320 370L455.4 532.5C466.7 546.1 486.9 547.9 500.5 536.6C514.1 525.3 515.9 505.1 504.6 491.5L361.7 320L504.6 148.5z"/></svg>

After

Width:  |  Height:  |  Size: 613 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

@@ -0,0 +1,4 @@
module InventoryWars
Main 1.0 Main.qml
IdleView 1.0 IdleView.qml
OngoingView 1.0 OngoingView.qml
+48
View File
@@ -0,0 +1,48 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Controls.impl
import QtQuick.Templates as T
T.Button {
id: control
property alias buttonColor: rect.color
property alias buttonBorderColor: rect.border.color
property alias textColor: label.color
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
implicitContentWidth + leftPadding + rightPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
implicitContentHeight + topPadding + bottomPadding)
leftPadding: 15
rightPadding: 15
topPadding: 10
bottomPadding: 10
background: Rectangle {
id: rect
radius: 8
border.color: UIStyle.buttonOutline
border.width: 1
color: UIStyle.buttonBackground
}
icon.width: 24
icon.height: 24
icon.color: UIStyle.textColor
contentItem: IconLabel {
id: label
spacing: control.spacing
mirrored: control.mirrored
display: control.display
icon: control.icon
text: control.text
font.pixelSize: UIStyle.fontSizeS
color: UIStyle.textColor
}
}
@@ -0,0 +1,54 @@
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
cmake_minimum_required(VERSION 3.16)
project(qtexamplestyle LANGUAGES CXX)
set(CMAKE_AUTOMOC ON)
if(NOT DEFINED INSTALL_EXAMPLESDIR)
set(INSTALL_EXAMPLESDIR "examples")
endif()
set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}/quickcontrols/colorpaletteclient/QtExampleStyle")
find_package(Qt6 REQUIRED COMPONENTS Core Gui Quick QuickControls2)
set_source_files_properties(UIStyle.qml
PROPERTIES
QT_QML_SINGLETON_TYPE TRUE
)
qt_policy(SET QTP0001 NEW)
qt_add_qml_module(qtexamplestyle
URI QtExampleStyle
PLUGIN_TARGET qtexamplestyle
QML_FILES
Button.qml
Popup.qml
UIStyle.qml
TextField.qml
)
target_link_libraries(qtexamplestyle PUBLIC
Qt6::Core
Qt6::Gui
Qt6::Quick
Qt6::QuickControls2
)
if(UNIX AND NOT APPLE AND CMAKE_CROSSCOMPILING)
find_package(Qt6 REQUIRED COMPONENTS QuickTemplates2)
# Work around QTBUG-86533
target_link_libraries(qtexamplestyle PRIVATE Qt6::QuickTemplates2)
endif()
install(TARGETS qtexamplestyle
RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}"
BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}"
LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}"
)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/qmldir
DESTINATION "${INSTALL_EXAMPLEDIR}"
)
+13
View File
@@ -0,0 +1,13 @@
// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Templates as T
T.Label {
id: control
font.pixelSize: UIStyle.fontSizeS
color: UIStyle.textColor
}
+27
View File
@@ -0,0 +1,27 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Templates as T
T.Popup {
id: control
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
implicitContentWidth + leftPadding + rightPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
implicitContentHeight + topPadding + bottomPadding)
leftPadding: 15
rightPadding: 15
topPadding: 10
bottomPadding: 10
background: Rectangle {
id: bg
radius: 8
border.color: UIStyle.buttonOutline
border.width: 2
color: UIStyle.background
}
}
@@ -0,0 +1,32 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Templates as T
T.TextField {
id: control
placeholderText: ""
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, contentWidth + leftPadding + rightPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
contentHeight + topPadding + bottomPadding)
background: Rectangle {
implicitWidth: 200
radius: 5
color: control.readOnly
? UIStyle.buttonGray
: UIStyle.background
border.color: UIStyle.buttonOutline
}
color: control.readOnly
? Qt.rgba(UIStyle.textColor.r,
UIStyle.textColor.g,
UIStyle.textColor.b,
0.6)
: UIStyle.textColor
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Templates as T
T.ToolBar {
id: control
implicitHeight: 25
spacing: 8
background: Rectangle {
color: UIStyle.buttonBackground
Rectangle {
height: 1
width: parent.width
anchors.top: parent.top
anchors.left: parent.left
color: UIStyle.buttonOutline
}
Rectangle {
height: 1
width: parent.width
anchors.bottom: parent.bottom
anchors.left: parent.left
color: UIStyle.buttonOutline
}
}
}
@@ -0,0 +1,50 @@
// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Controls.impl
import QtQuick.Templates as T
T.ToolButton {
id: control
property alias buttonColor: rect.color
property alias buttonBorderColor: rect.border.color
property alias textColor: label.color
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
implicitContentWidth + leftPadding + rightPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
implicitContentHeight + topPadding + bottomPadding)
leftPadding: 4
rightPadding: 4
topPadding: 4
bottomPadding: 4
background: Rectangle {
id: rect
color: "transparent"
border.width: 1
radius: 3
border.color: control.hovered
? UIStyle.buttonOutline
: "transparent"
}
icon.width: 15
icon.height: 15
icon.color: UIStyle.textColor
contentItem: IconLabel {
id: label
spacing: control.spacing
mirrored: control.mirrored
display: control.display
icon: control.icon
text: control.text
font.pixelSize: UIStyle.fontSizeS
color: UIStyle.textColor
}
}
+51
View File
@@ -0,0 +1,51 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
pragma Singleton
import QtQuick
QtObject {
id: uiStyle
property bool darkMode: (Application.styleHints.colorScheme === Qt.ColorScheme.Dark)
// Font Sizes
readonly property int fontSizeXXS: 8
readonly property int fontSizeXS: 10
readonly property int fontSizeS: 12
readonly property int fontSizeM: 16
readonly property int fontSizeL: 20
readonly property int fontSizeXL: 24
// Color Scheme
readonly property color colorRed: "#E91E63"
readonly property color buttonGray: darkMode ? "#808080" : "#f3f3f4"
readonly property color buttonGrayPressed: darkMode ? "#707070" : "#cecfd5"
readonly property color buttonGrayOutline: darkMode ? "#0D0D0D" : "#999999"
readonly property color buttonBackground: darkMode ? "#262626" : "#CCCCCC"
readonly property color buttonPressed: darkMode ? "#1E1E1E" : "#BEBEC4"
readonly property color buttonOutline: darkMode ? "#0D0D0D" : "#999999"
readonly property color background: darkMode ? "#262626" : "#E6E6E6"
readonly property color background1: darkMode ? "#00414A" : "#ceded6"
readonly property color textOnLightBackground: "#191919"
readonly property color textOnDarkBackground: "#E6E6E6"
readonly property color textColor: darkMode ? "#E6E6E6" : "#191919"
readonly property color titletextColor: darkMode ? "#2CDE85" : "#191919"
readonly property color highlightColor: darkMode ? "#33676E" : "#28C878"
readonly property color highlightBorderColor: darkMode ? "#4F8C95" : "#1FA05E"
function iconPath(baseImagePath) {
if (darkMode)
return `qrc:/qt/qml/ColorPalette/icons/${baseImagePath}_dark.svg`
else
return `qrc:/qt/qml/ColorPalette/icons/${baseImagePath}.svg`
}
}
+5
View File
@@ -0,0 +1,5 @@
module Style
Button 1.0 Button.qml
Popup 1.0 Popup.qml
TextField 1.0 TextField.qml
singleton UIStyle 1.0 UIStyle.qml
+107
View File
@@ -0,0 +1,107 @@
import logging
import sys
from datetime import datetime
from enum import Enum
from PySide6.QtCore import QObject, Signal, Slot, Property, QEnum, QAbstractItemModel
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine, QmlElement
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from star_resonance_tracer.proto.enum_chit_chat_channel_type_pb2 import ChitChatChannelType
from inventory_wars.game import Game
from inventory_wars.models import Base
from inventory_wars.scoring import Scoring, FirstGuess, FirstThenHighest
from inventory_wars.state import GameOngoing, GameIdle
QML_IMPORT_NAME = "InventoryWars"
QML_IMPORT_MAJOR_VERSION = 1
engine = create_engine("sqlite:///app.db")
Base.metadata.create_all(engine)
session = Session(engine)
class GameScoring(Enum):
FirstGuess = 0
FirstGuessThenHighestAmount = 1
@property
def as_scoring(self) -> type[Scoring]:
match self:
case self.FirstGuess:
return FirstGuess
case self.FirstGuessThenHighestAmount:
return FirstThenHighest
case _:
raise NotImplementedError
class GameState(Enum):
Hidden = 0
Revealed = 1
Ended = 3
@QmlElement
class GameService(QObject):
QEnum(GameScoring)
QEnum(GameState)
stateChanged = Signal()
guessed = Signal(str, float, arguments=["username", "seconds"])
highest = Signal(str, int, arguments=["username", "count"])
def __init__(self, parent=None):
super().__init__(parent)
self.m_game = Game(session, listening_channels=[ChitChatChannelType.ChannelTeam])
self.m_started_at: datetime | None = None
self.m_revealed_at: datetime | None = None
@Slot(int, int)
def start(self, item_id: int, mode: int):
mode = GameScoring(mode)
self.m_game.start(item_id, mode.as_scoring())
self.m_started_at = datetime.now()
self.m_revealed_at = None
self.stateChanged.emit()
@Slot()
def end(self):
self.m_game.end()
self.stateChanged.emit()
@Property(GameState, notify=stateChanged)
def state(self) -> GameState:
match self.m_game.state:
case GameIdle():
return GameState.Ended
case GameOngoing():
if self.m_revealed_at is None:
return GameState.Hidden
return GameState.Revealed
case _:
raise NotImplementedError
@Property(float)
def elapsedSeconds(self) -> float | None:
if self.m_started_at:
return (datetime.now() - self.m_started_at).total_seconds()
return 0
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG)
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
engine.addImportPath(sys.path[1])
engine.loadFromModule("InventoryWars", "Main")
if not engine.rootObjects():
sys.exit(-1)
exit_code = app.exec()
del engine
sys.exit(exit_code)