aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/companion/Companion.qml
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/companion/Companion.qml')
-rw-r--r--desktop/companion/Companion.qml50
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"