Add a coffee break
Every so often a mug appears at the cat's nose, it sits down, dips its head in and takes a few slow sips while steam rises. M01, optional stop. The mug lives in its own click-through window like the chip, or inside the overlay on Wayland. It goes away as soon as the cat is petted, dragged, sent to sleep or moves on to something else, so no mug is ever left standing. Also renames OverlayWindow's mode flag, which shadowed QWindow::active. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0f1df75a1d
commit
403ac30b3a
14 files changed
+283
-18
No files matched your search
+1
-1
@@ -22,7 +22,7 @@ qt_add_executable(tncat
|
|||||||
target_include_directories(tncat PRIVATE src)
|
target_include_directories(tncat PRIVATE src)
|
||||||
|
|
||||||
# QML files live in qml/ but register at the module root (TNCat.Main etc.)
|
# QML files live in qml/ but register at the module root (TNCat.Main etc.)
|
||||||
set(TNCAT_QML Main Cat Chip CatWindow ChipWindow OverlayWindow SettingsWindow CatMoods)
|
set(TNCAT_QML Main Cat Chip Cup CatWindow ChipWindow CupWindow OverlayWindow SettingsWindow CatMoods)
|
||||||
foreach(qmlfile IN LISTS TNCAT_QML)
|
foreach(qmlfile IN LISTS TNCAT_QML)
|
||||||
set_source_files_properties(qml/${qmlfile}.qml PROPERTIES QT_RESOURCE_ALIAS ${qmlfile}.qml)
|
set_source_files_properties(qml/${qmlfile}.qml PROPERTIES QT_RESOURCE_ALIAS ${qmlfile}.qml)
|
||||||
list(APPEND TNCAT_QML_FILES qml/${qmlfile}.qml)
|
list(APPEND TNCAT_QML_FILES qml/${qmlfile}.qml)
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
A little cat that wanders along the bottom edge of the screen, sits down, grooms
|
A little cat that wanders along the bottom edge of the screen, sits down, grooms
|
||||||
itself, stretches, dashes off and takes naps. Every now and then a curled steel
|
itself, stretches, dashes off and takes naps. Every now and then a curled steel
|
||||||
chip skitters past and the cat goes after it. Drag it around, tap it to pet it,
|
chip skitters past and the cat goes after it, or a mug appears and the cat takes
|
||||||
and it purrs.
|
a coffee break. Drag it around, tap it to pet it, and it purrs.
|
||||||
|
|
||||||
It announces what it is doing in the machine's own language: `G00` when it
|
It announces what it is doing in the machine's own language: `G00` when it
|
||||||
sprints, `G01` when it strolls, `M08` while it washes, `G28` when it stretches
|
sprints, `G01` when it strolls, `M08` while it washes, `G28` when it stretches
|
||||||
back to reference, `M30` when it goes to sleep.
|
back to reference, `M30` when it goes to sleep, `M01` when it stops for coffee.
|
||||||
|
|
||||||
The name is `TNC` plus `cat`, because the intended home is a HEIDENHAIN control
|
The name is `TNC` plus `cat`, because the intended home is a HEIDENHAIN control
|
||||||
(TNC7, TNC 640). Primary target is therefore **Linux / X11**; development happens
|
(TNC7, TNC 640). Primary target is therefore **Linux / X11**; development happens
|
||||||
@@ -22,16 +22,17 @@ plain Qt 6 / QML project.
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `CMakeLists.txt` | `qt_add_executable` + `qt_add_qml_module` (URI `TNCat`), macOS bundle, `.desktop` file on Linux |
|
| `CMakeLists.txt` | `qt_add_executable` + `qt_add_qml_module` (URI `TNCat`), macOS bundle, `.desktop` file on Linux |
|
||||||
| `src/main.cpp` | Wires `AppSettings` and `CatController` into the QML engine, warns on Wayland |
|
| `src/main.cpp` | Wires `AppSettings` and `CatController` into the QML engine, warns on Wayland |
|
||||||
| `src/catcontroller.*` | Mood machine (walk, run, sit, groom, stretch, sleep, love, alert, chase, bat), movement tick, chip chase, cursor tracking |
|
| `src/catcontroller.*` | Mood machine (walk, run, sit, groom, stretch, sleep, love, alert, chase, bat, drink), movement tick, chip chase, coffee break, cursor tracking |
|
||||||
| `src/appsettings.*` | `QSettings`-backed coat, size, pace, ground line, eye tracking, pet counter |
|
| `src/appsettings.*` | `QSettings`-backed coat, size, pace, ground line, eye tracking, pet counter |
|
||||||
| `src/platforminfo.*` | QML singleton `Platform`: platform name plus the X11 escape hatches |
|
| `src/platforminfo.*` | QML singleton `Platform`: platform name plus the X11 escape hatches |
|
||||||
| `src/traycontroller.*` | Tray icon and menu, silently skipped when the desktop has no tray |
|
| `src/traycontroller.*` | Tray icon and menu, silently skipped when the desktop has no tray |
|
||||||
| `qml/Main.qml` | Non-visual root that picks the window layout for the platform |
|
| `qml/Main.qml` | Non-visual root that picks the window layout for the platform |
|
||||||
| `qml/CatWindow.qml`, `qml/ChipWindow.qml` | One small window each, positioned by the app. X11, macOS, Windows |
|
| `qml/CatWindow.qml`, `qml/ChipWindow.qml`, `qml/CupWindow.qml` | One small window each, positioned by the app. X11, macOS, Windows |
|
||||||
| `qml/OverlayWindow.qml` | One screen-filling click-through window holding both. Wayland |
|
| `qml/OverlayWindow.qml` | One screen-filling click-through window holding both. Wayland |
|
||||||
| `qml/Cat.qml` | The drawing: rounded primitives plus `Shape` paths for tail, ears and mouth |
|
| `qml/Cat.qml` | The drawing: rounded primitives plus `Shape` paths for tail, ears and mouth |
|
||||||
| `qml/Chip.qml` | The curled chip, a stroked `Shape` spiral |
|
| `qml/Chip.qml` | The curled chip, a stroked `Shape` spiral |
|
||||||
| `qml/SettingsWindow.qml` | Coat swatches, size, pace, ground line, eye tracking, codes, chips, tricks |
|
| `qml/Cup.qml` | The coffee mug, with `Shape` steam wisps |
|
||||||
|
| `qml/SettingsWindow.qml` | Coat swatches, size, pace, ground line, eye tracking, codes, chips, coffee, tricks |
|
||||||
|
|
||||||
The cat is drawn in a fixed 100 x 90 design space and scaled by `width / 100`,
|
The cat is drawn in a fixed 100 x 90 design space and scaled by `width / 100`,
|
||||||
so every size setting stays crisp. There are no image assets, which keeps the
|
so every size setting stays crisp. There are no image assets, which keeps the
|
||||||
@@ -71,7 +72,7 @@ context menu.
|
|||||||
|
|
||||||
## Two window layouts
|
## Two window layouts
|
||||||
|
|
||||||
On X11, macOS and Windows the cat and the chip each get their own small,
|
On X11, macOS and Windows the cat, the chip and the mug each get their own small,
|
||||||
frameless, always-on-top window, and the app moves those windows around. Nothing
|
frameless, always-on-top window, and the app moves those windows around. Nothing
|
||||||
else on screen is covered and no clicks are intercepted.
|
else on screen is covered and no clicks are intercepted.
|
||||||
|
|
||||||
|
|||||||
+24
-2
@@ -14,6 +14,7 @@ Item {
|
|||||||
property bool petted: false
|
property bool petted: false
|
||||||
property point look: Qt.point(0, 0)
|
property point look: Qt.point(0, 0)
|
||||||
property bool showCodes: true
|
property bool showCodes: true
|
||||||
|
property real sipDip: 0
|
||||||
|
|
||||||
// The cat announces what it is doing in the machine's own language.
|
// The cat announces what it is doing in the machine's own language.
|
||||||
readonly property string moodCode: mood === "running" || mood === "chasing" ? "G00"
|
readonly property string moodCode: mood === "running" || mood === "chasing" ? "G00"
|
||||||
@@ -22,6 +23,7 @@ Item {
|
|||||||
: mood === "grooming" ? "M08"
|
: mood === "grooming" ? "M08"
|
||||||
: mood === "stretching" ? "G28"
|
: mood === "stretching" ? "G28"
|
||||||
: mood === "sleeping" ? "M30"
|
: mood === "sleeping" ? "M30"
|
||||||
|
: mood === "drinking" ? "M01"
|
||||||
: ""
|
: ""
|
||||||
|
|
||||||
readonly property bool dark: coat.hslLightness < 0.45
|
readonly property bool dark: coat.hslLightness < 0.45
|
||||||
@@ -36,7 +38,8 @@ Item {
|
|||||||
readonly property bool running: mood === "running" || mood === "chasing"
|
readonly property bool running: mood === "running" || mood === "chasing"
|
||||||
readonly property bool moving: (walking || running) && !lifted
|
readonly property bool moving: (walking || running) && !lifted
|
||||||
readonly property bool sleeping: mood === "sleeping"
|
readonly property bool sleeping: mood === "sleeping"
|
||||||
readonly property bool happy: mood === "loving" || mood === "grooming" || mood === "stretching" || petted
|
readonly property bool happy: mood === "loving" || mood === "grooming" || mood === "stretching"
|
||||||
|
|| mood === "drinking" || petted
|
||||||
readonly property real unit: width / 100
|
readonly property real unit: width / 100
|
||||||
|
|
||||||
// tail pose (design units); swish is added on top by the idle animation
|
// tail pose (design units); swish is added on top by the idle animation
|
||||||
@@ -531,6 +534,17 @@ Item {
|
|||||||
|
|
||||||
onMoodCodeChanged: if (showCodes && moodCode.length > 0) codeFlash.restart()
|
onMoodCodeChanged: if (showCodes && moodCode.length > 0) codeFlash.restart()
|
||||||
|
|
||||||
|
// Head dips into the mug, holds, comes back up.
|
||||||
|
SequentialAnimation {
|
||||||
|
running: cat.mood === "drinking" && !cat.lifted
|
||||||
|
loops: Animation.Infinite
|
||||||
|
alwaysRunToEnd: true
|
||||||
|
NumberAnimation { target: cat; property: "sipDip"; to: 13; duration: 700; easing.type: Easing.InOutQuad }
|
||||||
|
PauseAnimation { duration: 900 }
|
||||||
|
NumberAnimation { target: cat; property: "sipDip"; to: 0; duration: 600; easing.type: Easing.InOutQuad }
|
||||||
|
PauseAnimation { duration: 1400 }
|
||||||
|
}
|
||||||
|
|
||||||
states: [
|
states: [
|
||||||
State {
|
State {
|
||||||
name: "running"
|
name: "running"
|
||||||
@@ -566,6 +580,13 @@ Item {
|
|||||||
PropertyChanges { head.y: 24; head.x: 44; head.rotation: 4 }
|
PropertyChanges { head.y: 24; head.x: 44; head.rotation: 4 }
|
||||||
PropertyChanges { cat.tailC1X: 24; cat.tailC1Y: 86; cat.tailC2X: 60; cat.tailC2Y: 90; cat.tailX: 76; cat.tailY: 78 }
|
PropertyChanges { cat.tailC1X: 24; cat.tailC1Y: 86; cat.tailC2X: 60; cat.tailC2Y: 90; cat.tailX: 76; cat.tailY: 78 }
|
||||||
},
|
},
|
||||||
|
State {
|
||||||
|
name: "drinking"
|
||||||
|
when: cat.mood === "drinking" && !cat.lifted
|
||||||
|
PropertyChanges { body.x: 32; body.y: 42; body.width: 42; body.height: 38 }
|
||||||
|
PropertyChanges { head.x: 50; head.y: 20 + cat.sipDip; head.rotation: 16 }
|
||||||
|
PropertyChanges { cat.tailC1X: 26; cat.tailC1Y: 84; cat.tailC2X: 40; cat.tailC2Y: 88; cat.tailX: 52; cat.tailY: 80 }
|
||||||
|
},
|
||||||
State {
|
State {
|
||||||
name: "batting"
|
name: "batting"
|
||||||
when: cat.mood === "batting" && !cat.lifted
|
when: cat.mood === "batting" && !cat.lifted
|
||||||
@@ -588,5 +609,6 @@ Item {
|
|||||||
NumberAnimation { target: shadow; properties: "opacity,scale"; duration: 260 }
|
NumberAnimation { target: shadow; properties: "opacity,scale"; duration: 260 }
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property bool seated: mood === "sitting" || mood === "loving" || mood === "alert" || mood === "grooming"
|
readonly property bool seated: mood === "sitting" || mood === "loving" || mood === "alert"
|
||||||
|
|| mood === "grooming" || mood === "drinking"
|
||||||
}
|
}
|
||||||
+1
-1
@@ -5,7 +5,7 @@ import TNCat
|
|||||||
// Maps CatController::Mood onto the strings Cat.qml understands.
|
// Maps CatController::Mood onto the strings Cat.qml understands.
|
||||||
QtObject {
|
QtObject {
|
||||||
readonly property var names: ["walking", "running", "sitting", "grooming", "stretching",
|
readonly property var names: ["walking", "running", "sitting", "grooming", "stretching",
|
||||||
"sleeping", "loving", "alert", "chasing", "batting"]
|
"sleeping", "loving", "alert", "chasing", "batting", "drinking"]
|
||||||
|
|
||||||
function name(mood) {
|
function name(mood) {
|
||||||
return names[mood] !== undefined ? names[mood] : "sitting"
|
return names[mood] !== undefined ? names[mood] : "sitting"
|
||||||
|
|||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Shapes
|
||||||
|
|
||||||
|
// A mug of coffee with steam. Design space is 44 x 52 units, mug on the ground.
|
||||||
|
Item {
|
||||||
|
id: cup
|
||||||
|
|
||||||
|
readonly property real unit: width / 44
|
||||||
|
|
||||||
|
implicitWidth: 44
|
||||||
|
implicitHeight: 52
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 44
|
||||||
|
height: 52
|
||||||
|
transformOrigin: Item.TopLeft
|
||||||
|
scale: cup.unit
|
||||||
|
|
||||||
|
Rectangle { // ground shadow
|
||||||
|
x: 8; y: 46
|
||||||
|
width: 26; height: 5
|
||||||
|
radius: 2.5
|
||||||
|
color: "#000000"
|
||||||
|
opacity: 0.18
|
||||||
|
antialiasing: true
|
||||||
|
}
|
||||||
|
|
||||||
|
// steam
|
||||||
|
Repeater {
|
||||||
|
model: 3
|
||||||
|
Shape {
|
||||||
|
id: wisp
|
||||||
|
required property int index
|
||||||
|
x: 10 + index * 7
|
||||||
|
y: 2
|
||||||
|
width: 8
|
||||||
|
height: 16
|
||||||
|
opacity: 0
|
||||||
|
preferredRendererType: Shape.CurveRenderer
|
||||||
|
|
||||||
|
ShapePath {
|
||||||
|
strokeColor: "#ffffff"
|
||||||
|
strokeWidth: 1.8
|
||||||
|
capStyle: ShapePath.RoundCap
|
||||||
|
fillColor: "transparent"
|
||||||
|
startX: 4; startY: 15
|
||||||
|
PathQuad { controlX: 0; controlY: 10; x: 4; y: 7 }
|
||||||
|
PathQuad { controlX: 8; controlY: 4; x: 4; y: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
SequentialAnimation {
|
||||||
|
running: true
|
||||||
|
loops: Animation.Infinite
|
||||||
|
PauseAnimation { duration: wisp.index * 600 }
|
||||||
|
ParallelAnimation {
|
||||||
|
NumberAnimation { target: wisp; property: "opacity"; from: 0; to: 0.55; duration: 700 }
|
||||||
|
NumberAnimation { target: wisp; property: "y"; from: 8; to: 2; duration: 700 }
|
||||||
|
}
|
||||||
|
ParallelAnimation {
|
||||||
|
NumberAnimation { target: wisp; property: "opacity"; to: 0; duration: 1000 }
|
||||||
|
NumberAnimation { target: wisp; property: "y"; to: -6; duration: 1000 }
|
||||||
|
}
|
||||||
|
PauseAnimation { duration: 1800 - wisp.index * 600 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle
|
||||||
|
Shape {
|
||||||
|
x: 28; y: 24
|
||||||
|
width: 14; height: 18
|
||||||
|
preferredRendererType: Shape.CurveRenderer
|
||||||
|
ShapePath {
|
||||||
|
strokeColor: "#e8e3da"
|
||||||
|
strokeWidth: 3.5
|
||||||
|
capStyle: ShapePath.RoundCap
|
||||||
|
fillColor: "transparent"
|
||||||
|
startX: 1; startY: 2
|
||||||
|
PathCubic { control1X: 12; control1Y: 0; control2X: 12; control2Y: 14; x: 1; y: 13 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mug
|
||||||
|
Rectangle {
|
||||||
|
x: 9; y: 20
|
||||||
|
width: 22; height: 27
|
||||||
|
radius: 4
|
||||||
|
color: "#f7f4ee"
|
||||||
|
antialiasing: true
|
||||||
|
|
||||||
|
Rectangle { // band
|
||||||
|
x: 0; y: 13
|
||||||
|
width: parent.width
|
||||||
|
height: 5
|
||||||
|
color: "#4d7ea8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// coffee surface
|
||||||
|
Rectangle {
|
||||||
|
x: 10.5; y: 19
|
||||||
|
width: 19; height: 5
|
||||||
|
radius: 2.5
|
||||||
|
color: "#4a2c17"
|
||||||
|
antialiasing: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
import QtQuick
|
||||||
|
import TNCat
|
||||||
|
|
||||||
|
// The mug stands on the ground next to the cat, click-through like the chip.
|
||||||
|
Window {
|
||||||
|
id: win
|
||||||
|
|
||||||
|
required property CatController cat
|
||||||
|
required property AppSettings settings
|
||||||
|
|
||||||
|
readonly property int cupSize: Math.round(settings.catSize * 0.38)
|
||||||
|
|
||||||
|
width: cupSize
|
||||||
|
height: Math.round(cupSize * 1.18)
|
||||||
|
x: cat.cupX - width / 2
|
||||||
|
y: Screen.desktopAvailableHeight - height - settings.bottomMargin
|
||||||
|
color: "transparent"
|
||||||
|
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
|
||||||
|
| Qt.NoDropShadowWindowHint | Qt.WindowDoesNotAcceptFocus
|
||||||
|
| Qt.WindowTransparentForInput
|
||||||
|
| (Platform.bypassWindowManager ? Qt.X11BypassWindowManagerHint : 0)
|
||||||
|
visible: cat.cupVisible && !Platform.opaqueBackground
|
||||||
|
|
||||||
|
Cup {
|
||||||
|
anchors.fill: parent
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-1
@@ -28,10 +28,16 @@ QtObject {
|
|||||||
visible: !Platform.overlayMode && app.cat.chipVisible
|
visible: !Platform.overlayMode && app.cat.chipVisible
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly property CupWindow cupWindow: CupWindow {
|
||||||
|
cat: app.cat
|
||||||
|
settings: app.settings
|
||||||
|
visible: !Platform.overlayMode && app.cat.cupVisible
|
||||||
|
}
|
||||||
|
|
||||||
readonly property OverlayWindow overlayWindow: OverlayWindow {
|
readonly property OverlayWindow overlayWindow: OverlayWindow {
|
||||||
cat: app.cat
|
cat: app.cat
|
||||||
settings: app.settings
|
settings: app.settings
|
||||||
active: Platform.overlayMode
|
overlayActive: Platform.overlayMode
|
||||||
onSettingsRequested: app.openSettings()
|
onSettingsRequested: app.openSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-5
@@ -15,7 +15,7 @@ Window {
|
|||||||
|
|
||||||
// Fullscreen is a visibility, not a visible flag, so the mode switch has to
|
// Fullscreen is a visibility, not a visible flag, so the mode switch has to
|
||||||
// go through visibility or the two fight each other.
|
// go through visibility or the two fight each other.
|
||||||
property bool active: false
|
property bool overlayActive: false
|
||||||
|
|
||||||
readonly property int catW: settings.catSize
|
readonly property int catW: settings.catSize
|
||||||
readonly property int catH: Math.round(settings.catSize * 0.9)
|
readonly property int catH: Math.round(settings.catSize * 0.9)
|
||||||
@@ -26,14 +26,14 @@ Window {
|
|||||||
Platform.setInputRegion(win, Qt.rect(Math.round(cat.x), Math.round(groundY), catW, catH))
|
Platform.setInputRegion(win, Qt.rect(Math.round(cat.x), Math.round(groundY), catW, catH))
|
||||||
}
|
}
|
||||||
|
|
||||||
visibility: active ? Window.FullScreen : Window.Hidden
|
visibility: overlayActive ? Window.FullScreen : Window.Hidden
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
|
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
|
||||||
| Qt.NoDropShadowWindowHint | Qt.WindowDoesNotAcceptFocus
|
| Qt.NoDropShadowWindowHint | Qt.WindowDoesNotAcceptFocus
|
||||||
title: qsTr("TNCat")
|
title: qsTr("TNCat")
|
||||||
|
|
||||||
onActiveChanged: {
|
onOverlayActiveChanged: {
|
||||||
if (!active)
|
if (!overlayActive)
|
||||||
return
|
return
|
||||||
cat.stageWidth = width
|
cat.stageWidth = width
|
||||||
cat.catWidth = catW
|
cat.catWidth = catW
|
||||||
@@ -51,7 +51,7 @@ Window {
|
|||||||
// Coalesce mask updates: one X11 round trip per frame of walking is plenty.
|
// Coalesce mask updates: one X11 round trip per frame of walking is plenty.
|
||||||
Timer {
|
Timer {
|
||||||
interval: 33
|
interval: 33
|
||||||
running: win.active
|
running: win.overlayActive
|
||||||
repeat: true
|
repeat: true
|
||||||
onTriggered: win.refreshInputRegion()
|
onTriggered: win.refreshInputRegion()
|
||||||
}
|
}
|
||||||
@@ -69,6 +69,14 @@ Window {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Cup {
|
||||||
|
x: win.cat.cupX - width / 2
|
||||||
|
y: win.height - height - win.settings.bottomMargin
|
||||||
|
width: Math.round(win.settings.catSize * 0.38)
|
||||||
|
height: Math.round(width * 1.18)
|
||||||
|
visible: win.cat.cupVisible
|
||||||
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: catItem
|
id: catItem
|
||||||
x: win.cat.x
|
x: win.cat.x
|
||||||
|
|||||||
@@ -163,6 +163,13 @@ Window {
|
|||||||
onToggled: win.settings.chipsEnabled = checked
|
onToggled: win.settings.chipsEnabled = checked
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Label { text: qsTr("Coffee") }
|
||||||
|
Switch {
|
||||||
|
text: qsTr("Take a coffee break now and then")
|
||||||
|
checked: win.settings.coffeeEnabled
|
||||||
|
onToggled: win.settings.coffeeEnabled = checked
|
||||||
|
}
|
||||||
|
|
||||||
Label { text: qsTr("Cat") }
|
Label { text: qsTr("Cat") }
|
||||||
Switch {
|
Switch {
|
||||||
text: checked ? qsTr("Napping") : qsTr("Awake")
|
text: checked ? qsTr("Napping") : qsTr("Awake")
|
||||||
@@ -183,6 +190,7 @@ Window {
|
|||||||
Button { text: qsTr("Dash"); onClicked: win.cat.doTrick(CatController.Running) }
|
Button { text: qsTr("Dash"); onClicked: win.cat.doTrick(CatController.Running) }
|
||||||
Button { text: qsTr("Nap"); onClicked: win.cat.doTrick(CatController.Sleeping) }
|
Button { text: qsTr("Nap"); onClicked: win.cat.doTrick(CatController.Sleeping) }
|
||||||
Button { text: qsTr("Toss a chip"); onClicked: win.cat.tossChip() }
|
Button { text: qsTr("Toss a chip"); onClicked: win.cat.tossChip() }
|
||||||
|
Button { text: qsTr("Coffee"); onClicked: win.cat.coffeeBreak() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,19 @@ void AppSettings::setChipsEnabled(bool enabled)
|
|||||||
emit chipsEnabledChanged();
|
emit chipsEnabledChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool AppSettings::coffeeEnabled() const
|
||||||
|
{
|
||||||
|
return m_settings.value("coffeeEnabled", true).toBool();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppSettings::setCoffeeEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
if (enabled == coffeeEnabled())
|
||||||
|
return;
|
||||||
|
m_settings.setValue("coffeeEnabled", enabled);
|
||||||
|
emit coffeeEnabledChanged();
|
||||||
|
}
|
||||||
|
|
||||||
int AppSettings::petCount() const
|
int AppSettings::petCount() const
|
||||||
{
|
{
|
||||||
return m_settings.value("petCount", 0).toInt();
|
return m_settings.value("petCount", 0).toInt();
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class AppSettings : public QObject
|
|||||||
Q_PROPERTY(int bottomMargin READ bottomMargin WRITE setBottomMargin NOTIFY bottomMarginChanged)
|
Q_PROPERTY(int bottomMargin READ bottomMargin WRITE setBottomMargin NOTIFY bottomMarginChanged)
|
||||||
Q_PROPERTY(bool showCodes READ showCodes WRITE setShowCodes NOTIFY showCodesChanged)
|
Q_PROPERTY(bool showCodes READ showCodes WRITE setShowCodes NOTIFY showCodesChanged)
|
||||||
Q_PROPERTY(bool chipsEnabled READ chipsEnabled WRITE setChipsEnabled NOTIFY chipsEnabledChanged)
|
Q_PROPERTY(bool chipsEnabled READ chipsEnabled WRITE setChipsEnabled NOTIFY chipsEnabledChanged)
|
||||||
|
Q_PROPERTY(bool coffeeEnabled READ coffeeEnabled WRITE setCoffeeEnabled NOTIFY coffeeEnabledChanged)
|
||||||
Q_PROPERTY(int petCount READ petCount NOTIFY petCountChanged)
|
Q_PROPERTY(int petCount READ petCount NOTIFY petCountChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -44,6 +45,9 @@ public:
|
|||||||
bool chipsEnabled() const;
|
bool chipsEnabled() const;
|
||||||
void setChipsEnabled(bool enabled);
|
void setChipsEnabled(bool enabled);
|
||||||
|
|
||||||
|
bool coffeeEnabled() const;
|
||||||
|
void setCoffeeEnabled(bool enabled);
|
||||||
|
|
||||||
int petCount() const;
|
int petCount() const;
|
||||||
void incrementPetCount();
|
void incrementPetCount();
|
||||||
|
|
||||||
@@ -55,6 +59,7 @@ signals:
|
|||||||
void bottomMarginChanged();
|
void bottomMarginChanged();
|
||||||
void showCodesChanged();
|
void showCodesChanged();
|
||||||
void chipsEnabledChanged();
|
void chipsEnabledChanged();
|
||||||
|
void coffeeEnabledChanged();
|
||||||
void petCountChanged();
|
void petCountChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ void CatController::setPaused(bool paused)
|
|||||||
m_moodTimer.stop();
|
m_moodTimer.stop();
|
||||||
m_chipTimer.stop();
|
m_chipTimer.stop();
|
||||||
endChase();
|
endChase();
|
||||||
|
endCoffee();
|
||||||
setMood(Mood::Sleeping);
|
setMood(Mood::Sleeping);
|
||||||
} else {
|
} else {
|
||||||
pickNextMood();
|
pickNextMood();
|
||||||
@@ -112,6 +113,7 @@ void CatController::setDragging(bool dragging)
|
|||||||
return;
|
return;
|
||||||
if (dragging) {
|
if (dragging) {
|
||||||
m_moodTimer.stop();
|
m_moodTimer.stop();
|
||||||
|
endCoffee();
|
||||||
setMood(Mood::Alert);
|
setMood(Mood::Alert);
|
||||||
} else if (!m_paused) {
|
} else if (!m_paused) {
|
||||||
holdMood(Mood::Sitting, 1500);
|
holdMood(Mood::Sitting, 1500);
|
||||||
@@ -132,6 +134,7 @@ void CatController::pet()
|
|||||||
return;
|
return;
|
||||||
m_settings->incrementPetCount();
|
m_settings->incrementPetCount();
|
||||||
endChase();
|
endChase();
|
||||||
|
endCoffee();
|
||||||
holdMood(Mood::Loving, 1800);
|
holdMood(Mood::Loving, 1800);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +143,13 @@ void CatController::doTrick(Mood mood)
|
|||||||
if (m_paused)
|
if (m_paused)
|
||||||
return;
|
return;
|
||||||
m_frozen = false;
|
m_frozen = false;
|
||||||
|
if (mood == Mood::Drinking) {
|
||||||
|
coffeeBreak();
|
||||||
|
return;
|
||||||
|
}
|
||||||
endChase();
|
endChase();
|
||||||
|
if (mood != Mood::Drinking)
|
||||||
|
endCoffee();
|
||||||
holdMood(mood, mood == Mood::Sleeping ? 15000 : 3500);
|
holdMood(mood, mood == Mood::Sleeping ? 15000 : 3500);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,6 +164,10 @@ void CatController::tossChip()
|
|||||||
{
|
{
|
||||||
if (m_paused || m_frozen || m_chipVisible)
|
if (m_paused || m_frozen || m_chipVisible)
|
||||||
return;
|
return;
|
||||||
|
if (m_cupVisible) { // finish the coffee first
|
||||||
|
scheduleChip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!m_settings->chipsEnabled()) {
|
if (!m_settings->chipsEnabled()) {
|
||||||
scheduleChip();
|
scheduleChip();
|
||||||
return;
|
return;
|
||||||
@@ -173,6 +186,40 @@ void CatController::tossChip()
|
|||||||
setMood(Mood::Chasing);
|
setMood(Mood::Chasing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A mug appears at the cat's nose, it sits down and takes its time.
|
||||||
|
void CatController::coffeeBreak()
|
||||||
|
{
|
||||||
|
if (m_paused || m_frozen)
|
||||||
|
return;
|
||||||
|
|
||||||
|
endChase();
|
||||||
|
const qreal reach = m_catWidth * 1.08; // just past the whiskers
|
||||||
|
const qreal edge = m_catWidth * 0.5;
|
||||||
|
|
||||||
|
// Put the mug on the side the cat faces, unless that runs off the screen.
|
||||||
|
qreal wanted = m_facingRight ? m_x + reach : m_x + m_catWidth - reach;
|
||||||
|
if (wanted < edge || wanted > m_stageWidth - edge) {
|
||||||
|
setFacingRight(!m_facingRight);
|
||||||
|
wanted = m_facingRight ? m_x + reach : m_x + m_catWidth - reach;
|
||||||
|
}
|
||||||
|
m_cupX = qBound(edge, wanted, m_stageWidth - edge);
|
||||||
|
emit cupXChanged();
|
||||||
|
|
||||||
|
if (!m_cupVisible) {
|
||||||
|
m_cupVisible = true;
|
||||||
|
emit cupVisibleChanged();
|
||||||
|
}
|
||||||
|
holdMood(Mood::Drinking, QRandomGenerator::global()->bounded(7000, 12000));
|
||||||
|
}
|
||||||
|
|
||||||
|
void CatController::endCoffee()
|
||||||
|
{
|
||||||
|
if (!m_cupVisible)
|
||||||
|
return;
|
||||||
|
m_cupVisible = false;
|
||||||
|
emit cupVisibleChanged();
|
||||||
|
}
|
||||||
|
|
||||||
void CatController::batChip()
|
void CatController::batChip()
|
||||||
{
|
{
|
||||||
auto *rng = QRandomGenerator::global();
|
auto *rng = QRandomGenerator::global();
|
||||||
@@ -306,9 +353,15 @@ void CatController::pickNextMood()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
endCoffee();
|
||||||
|
|
||||||
auto *rng = QRandomGenerator::global();
|
auto *rng = QRandomGenerator::global();
|
||||||
const int roll = rng->bounded(100);
|
const int roll = rng->bounded(100);
|
||||||
|
|
||||||
|
if (roll < 8 && m_settings->coffeeEnabled()) {
|
||||||
|
coffeeBreak();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (roll < 45) {
|
if (roll < 45) {
|
||||||
if (rng->bounded(100) < 20)
|
if (rng->bounded(100) < 20)
|
||||||
setFacingRight(!m_facingRight);
|
setFacingRight(!m_facingRight);
|
||||||
|
|||||||
+12
-1
@@ -30,9 +30,11 @@ class CatController : public QObject
|
|||||||
Q_PROPERTY(bool chipVisible READ chipVisible NOTIFY chipVisibleChanged)
|
Q_PROPERTY(bool chipVisible READ chipVisible NOTIFY chipVisibleChanged)
|
||||||
Q_PROPERTY(qreal chipX READ chipX NOTIFY chipXChanged)
|
Q_PROPERTY(qreal chipX READ chipX NOTIFY chipXChanged)
|
||||||
Q_PROPERTY(int chipKick READ chipKick NOTIFY chipKickChanged)
|
Q_PROPERTY(int chipKick READ chipKick NOTIFY chipKickChanged)
|
||||||
|
Q_PROPERTY(bool cupVisible READ cupVisible NOTIFY cupVisibleChanged)
|
||||||
|
Q_PROPERTY(qreal cupX READ cupX NOTIFY cupXChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum class Mood { Walking, Running, Sitting, Grooming, Stretching, Sleeping, Loving, Alert, Chasing, Batting };
|
enum class Mood { Walking, Running, Sitting, Grooming, Stretching, Sleeping, Loving, Alert, Chasing, Batting, Drinking };
|
||||||
Q_ENUM(Mood)
|
Q_ENUM(Mood)
|
||||||
|
|
||||||
explicit CatController(AppSettings *settings, QObject *parent = nullptr);
|
explicit CatController(AppSettings *settings, QObject *parent = nullptr);
|
||||||
@@ -67,10 +69,13 @@ public:
|
|||||||
bool chipVisible() const { return m_chipVisible; }
|
bool chipVisible() const { return m_chipVisible; }
|
||||||
qreal chipX() const { return m_chipX; }
|
qreal chipX() const { return m_chipX; }
|
||||||
int chipKick() const { return m_chipKick; }
|
int chipKick() const { return m_chipKick; }
|
||||||
|
bool cupVisible() const { return m_cupVisible; }
|
||||||
|
qreal cupX() const { return m_cupX; }
|
||||||
|
|
||||||
Q_INVOKABLE void pet();
|
Q_INVOKABLE void pet();
|
||||||
Q_INVOKABLE void doTrick(Mood mood);
|
Q_INVOKABLE void doTrick(Mood mood);
|
||||||
Q_INVOKABLE void tossChip();
|
Q_INVOKABLE void tossChip();
|
||||||
|
Q_INVOKABLE void coffeeBreak();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void xChanged();
|
void xChanged();
|
||||||
@@ -86,6 +91,8 @@ signals:
|
|||||||
void chipVisibleChanged();
|
void chipVisibleChanged();
|
||||||
void chipXChanged();
|
void chipXChanged();
|
||||||
void chipKickChanged();
|
void chipKickChanged();
|
||||||
|
void cupVisibleChanged();
|
||||||
|
void cupXChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void tick();
|
void tick();
|
||||||
@@ -97,6 +104,7 @@ private:
|
|||||||
void holdMood(Mood mood, int ms);
|
void holdMood(Mood mood, int ms);
|
||||||
void batChip();
|
void batChip();
|
||||||
void endChase();
|
void endChase();
|
||||||
|
void endCoffee();
|
||||||
void scheduleChip();
|
void scheduleChip();
|
||||||
qreal catNose() const;
|
qreal catNose() const;
|
||||||
|
|
||||||
@@ -124,4 +132,7 @@ private:
|
|||||||
qreal m_chipX = 0;
|
qreal m_chipX = 0;
|
||||||
int m_chipKick = 0;
|
int m_chipKick = 0;
|
||||||
int m_batsLeft = 0;
|
int m_batsLeft = 0;
|
||||||
|
|
||||||
|
bool m_cupVisible = false;
|
||||||
|
qreal m_cupX = 0;
|
||||||
};
|
};
|
||||||
@@ -17,6 +17,7 @@ TrayController::TrayController(CatController *cat, QObject *parent)
|
|||||||
{ QT_TR_NOOP("Stretch"), CatController::Mood::Stretching },
|
{ QT_TR_NOOP("Stretch"), CatController::Mood::Stretching },
|
||||||
{ QT_TR_NOOP("Dash"), CatController::Mood::Running },
|
{ QT_TR_NOOP("Dash"), CatController::Mood::Running },
|
||||||
{ QT_TR_NOOP("Nap"), CatController::Mood::Sleeping },
|
{ QT_TR_NOOP("Nap"), CatController::Mood::Sleeping },
|
||||||
|
{ QT_TR_NOOP("Coffee break"), CatController::Mood::Drinking },
|
||||||
};
|
};
|
||||||
for (const auto &e : entries) {
|
for (const auto &e : entries) {
|
||||||
const auto mood = e.mood;
|
const auto mood = e.mood;
|
||||||
|
|||||||
Reference in new issue
Block a user