diff options
Diffstat (limited to 'desktop/companion/Companion.qml')
| -rw-r--r-- | desktop/companion/Companion.qml | 50 |
1 files changed, 43 insertions, 7 deletions
diff --git a/desktop/companion/Companion.qml b/desktop/companion/Companion.qml index fe2d751..6eb7370 100644 --- a/desktop/companion/Companion.qml +++ b/desktop/companion/Companion.qml @@ -33,13 +33,31 @@ PanelWindow { // SUPER+\ (the IPC below) and the Companion settings page, through Prefs, // so it survives a restart. readonly property bool present: Prefs.ready && Prefs.companion + // A hide plays a goodbye first: the wave frames, then the drop. + // Re-showing her cancels it and greets as usual. + property bool leaving: false + property int waveIndex: 0 + readonly property var waveCycle: ["wave", "wave-l", "wave", "wave-r"] onPresentChanged: { if (root.present) { + root.leaving = false; root.destructAt = 0; // the skirt is back root.now = Date.now(); greet.restart(); } else { root.stopMonologue(); + // Nothing to wave at when she is already out of view. + if (root.fullscreen !== 2 && !root.hopping) { + root.leaving = true; + root.waveIndex = 0; + root.now = Date.now(); + // A line one hide in four, like the monologue's roll, and not + // forced, so the usual 30 s line cooldown still applies. A + // silent wave is only as long as it takes to read nothing. + const spoke = Math.random() < 0.25 && root.say("bye"); + goodbyeTimer.interval = spoke ? Mood.T.goodbye : Mood.T.goodbyeSilent; + goodbyeTimer.start(); + } } } @@ -48,9 +66,13 @@ PanelWindow { readonly property string focused: Hyprland.focusedMonitor?.name ?? "" property string here: "" property bool hopping: false + // No focused monitor (outputs gone over a suspend) is not a destination. + // Already out of view, there is no exit to wait for: settled would never + // fire, hopping would stick and hide her for good, so move at once. onFocusedChanged: { - if (root.here === "") root.here = root.focused; - else if (root.focused !== root.here) root.hopping = true; + if (root.focused === "" || root.focused === root.here) return; + if (root.here === "" || root.away) root.here = root.focused; + else root.hopping = true; } Component.onCompleted: root.here = root.focused @@ -238,6 +260,7 @@ PanelWindow { // --- state ---------------------------------------------------------- readonly property var mood: Mood.pick({ + leaving: root.leaving, now: root.now, destructAt: root.destructAt, dodging: root.dodging, shoved: root.shoved, shovedAt: root.shovedAt, recording: root.asst.recording, busy: root.asst.busy, repliedAt: root.repliedAt, @@ -249,19 +272,21 @@ PanelWindow { onMoodChanged: if (root.monologue && root.mood.state !== "muse") root.stopMonologue() // Hidden: no sprite, no bubble. Away adds the dodge, which hides the - // sprite but lets its line show. - readonly property bool hidden: !root.present || root.fullscreen === 2 || root.hopping + // sprite but lets its line show. While waving goodbye she is still on + // screen, so a hidden present does not hide her until the wave is done. + readonly property bool hidden: (!root.present && !root.leaving) || root.fullscreen === 2 || root.hopping readonly property bool away: root.hidden || root.dodging || root.remapping // --- speech --------------------------------------------------------- function say(kind, force) { const t = Date.now(); - if (root.hidden || (!force && !Mood.canSpeak(root.lastLineAt, t))) return; + if (root.hidden || (!force && !Mood.canSpeak(root.lastLineAt, t))) return false; const line = Lines.pick(kind, Math.random(), Prefs.companionApps); - if (line === "") return; + if (line === "") return false; root.lastLineAt = t; bubble.show(line, 3000); + return true; } // The opening monologue: about 15 characters a second, one sentence @@ -353,6 +378,17 @@ PanelWindow { } } + // The goodbye: she waves, then leaves. With a line she holds long enough + // to read it. The hand sways side to side while it lasts. + Timer { id: goodbyeTimer; onTriggered: root.leaving = false } + Timer { + id: waveFlip + interval: 250 + repeat: true + running: root.leaving + onTriggered: root.waveIndex = (root.waveIndex + 1) % root.waveCycle.length + } + // qs -p <desktop> ipc call companion toggle -> SUPER+\ IpcHandler { target: "companion" @@ -388,7 +424,7 @@ PanelWindow { id: sprite x: seat.x y: seat.y - source: `file://${root.art}/${root.outfit}/${root.mood.frame}.png` + source: `file://${root.art}/${root.outfit}/${root.leaving ? root.waveCycle[root.waveIndex] : root.mood.frame}.png` fallback: `file://${root.art}/${root.outfit}/base.png` out: root.away trembling: root.mood.state === "admire" && root.mood.frame === "blush3" |
