Workcat: cute desktop cat, no window closing
Reworks the example into a pure desktop-pet: the cat wanders, sits, grooms, stretches, dashes and naps, reacts to petting and dragging, and follows the cursor with its eyes. The window-scanning and Accessibility code is gone. The cat is drawn from rounded primitives plus Shape paths in a fixed 100x90 design space, so it scales cleanly to any size. Linux/X11 is the primary target: the tray is optional, Wayland gets a warning, and WORKCAT_X11_BYPASS / WORKCAT_OPAQUE cover uncooperative window managers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
commit
acef398f49
17 files changed
+1584
No files matched your search
@@ -0,0 +1,217 @@
|
||||
#include "catcontroller.h"
|
||||
#include "appsettings.h"
|
||||
|
||||
#include <QCursor>
|
||||
#include <QMetaEnum>
|
||||
#include <QRandomGenerator>
|
||||
#include <QWindow>
|
||||
|
||||
CatController::CatController(AppSettings *settings, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_settings(settings)
|
||||
{
|
||||
m_clock.start();
|
||||
|
||||
m_tickTimer.setInterval(16);
|
||||
connect(&m_tickTimer, &QTimer::timeout, this, &CatController::tick);
|
||||
m_tickTimer.start();
|
||||
|
||||
m_moodTimer.setSingleShot(true);
|
||||
connect(&m_moodTimer, &QTimer::timeout, this, &CatController::pickNextMood);
|
||||
|
||||
m_cursorTimer.setInterval(40);
|
||||
connect(&m_cursorTimer, &QTimer::timeout, this, &CatController::trackCursor);
|
||||
m_cursorTimer.start();
|
||||
|
||||
// WORKCAT_POSE=Sleeping freezes the cat in one mood (handy for screenshots).
|
||||
const QByteArray pose = qgetenv("WORKCAT_POSE");
|
||||
if (!pose.isEmpty()) {
|
||||
const QMetaEnum me = QMetaEnum::fromType<Mood>();
|
||||
bool ok = false;
|
||||
const int value = me.keyToValue(pose.constData(), &ok);
|
||||
if (ok) {
|
||||
m_frozen = true;
|
||||
setMood(static_cast<Mood>(value));
|
||||
}
|
||||
}
|
||||
if (!m_frozen)
|
||||
pickNextMood();
|
||||
}
|
||||
|
||||
void CatController::setWindow(QWindow *window)
|
||||
{
|
||||
m_window = window;
|
||||
}
|
||||
|
||||
void CatController::setX(qreal x)
|
||||
{
|
||||
if (qFuzzyCompare(x, m_x))
|
||||
return;
|
||||
m_x = x;
|
||||
emit xChanged();
|
||||
}
|
||||
|
||||
void CatController::setScreenWidth(qreal width)
|
||||
{
|
||||
if (qFuzzyCompare(width, m_screenWidth))
|
||||
return;
|
||||
m_screenWidth = width;
|
||||
emit screenWidthChanged();
|
||||
}
|
||||
|
||||
void CatController::setCatWidth(qreal width)
|
||||
{
|
||||
if (qFuzzyCompare(width, m_catWidth))
|
||||
return;
|
||||
m_catWidth = width;
|
||||
emit catWidthChanged();
|
||||
}
|
||||
|
||||
void CatController::setPaused(bool paused)
|
||||
{
|
||||
if (paused == m_paused)
|
||||
return;
|
||||
m_paused = paused;
|
||||
emit pausedChanged();
|
||||
if (paused) {
|
||||
m_moodTimer.stop();
|
||||
setMood(Mood::Sleeping);
|
||||
} else {
|
||||
pickNextMood();
|
||||
}
|
||||
}
|
||||
|
||||
void CatController::setDragging(bool dragging)
|
||||
{
|
||||
if (dragging == m_dragging)
|
||||
return;
|
||||
m_dragging = dragging;
|
||||
emit draggingChanged();
|
||||
if (m_frozen)
|
||||
return;
|
||||
if (dragging) {
|
||||
m_moodTimer.stop();
|
||||
setMood(Mood::Alert);
|
||||
} else if (!m_paused) {
|
||||
holdMood(Mood::Sitting, 1500);
|
||||
}
|
||||
}
|
||||
|
||||
void CatController::setHovered(bool hovered)
|
||||
{
|
||||
if (hovered == m_hovered)
|
||||
return;
|
||||
m_hovered = hovered;
|
||||
emit hoveredChanged();
|
||||
}
|
||||
|
||||
void CatController::pet()
|
||||
{
|
||||
if (m_paused || m_frozen)
|
||||
return;
|
||||
m_settings->incrementPetCount();
|
||||
holdMood(Mood::Loving, 1800);
|
||||
}
|
||||
|
||||
void CatController::doTrick(Mood mood)
|
||||
{
|
||||
if (m_paused)
|
||||
return;
|
||||
m_frozen = false;
|
||||
holdMood(mood, mood == Mood::Sleeping ? 15000 : 3500);
|
||||
}
|
||||
|
||||
void CatController::holdMood(Mood mood, int ms)
|
||||
{
|
||||
setMood(mood);
|
||||
m_moodTimer.start(ms);
|
||||
}
|
||||
|
||||
void CatController::trackCursor()
|
||||
{
|
||||
if (!m_window || !m_settings->followCursor()) {
|
||||
if (!m_look.isNull()) {
|
||||
m_look = {};
|
||||
emit lookChanged();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const QPointF center = m_window->geometry().center();
|
||||
QPointF delta = QCursor::pos() - center;
|
||||
if (!m_facingRight)
|
||||
delta.setX(-delta.x());
|
||||
const qreal range = 400.0;
|
||||
const QPointF look(qBound(-1.0, delta.x() / range, 1.0), qBound(-1.0, delta.y() / range, 1.0));
|
||||
if ((look - m_look).manhattanLength() > 0.005) {
|
||||
m_look = look;
|
||||
emit lookChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void CatController::tick()
|
||||
{
|
||||
const qint64 now = m_clock.elapsed();
|
||||
const qreal dt = qMin(now - m_lastTickMs, qint64(100)) / 1000.0;
|
||||
m_lastTickMs = now;
|
||||
|
||||
const bool moving = m_mood == Mood::Walking || m_mood == Mood::Running;
|
||||
if (!moving || m_dragging || m_paused || m_frozen)
|
||||
return;
|
||||
|
||||
const qreal speed = m_settings->walkSpeed() * (m_mood == Mood::Running ? 4.5 : 1.0);
|
||||
const qreal step = speed * dt;
|
||||
qreal next = m_x + (m_facingRight ? step : -step);
|
||||
const qreal maxX = qMax(0.0, m_screenWidth - m_catWidth);
|
||||
if (next >= maxX) {
|
||||
next = maxX;
|
||||
setFacingRight(false);
|
||||
if (m_mood == Mood::Running)
|
||||
holdMood(Mood::Sitting, 2000);
|
||||
} else if (next <= 0) {
|
||||
next = 0;
|
||||
setFacingRight(true);
|
||||
if (m_mood == Mood::Running)
|
||||
holdMood(Mood::Sitting, 2000);
|
||||
}
|
||||
setX(next);
|
||||
}
|
||||
|
||||
void CatController::setMood(Mood mood)
|
||||
{
|
||||
if (mood == m_mood)
|
||||
return;
|
||||
m_mood = mood;
|
||||
emit moodChanged();
|
||||
}
|
||||
|
||||
void CatController::setFacingRight(bool right)
|
||||
{
|
||||
if (right == m_facingRight)
|
||||
return;
|
||||
m_facingRight = right;
|
||||
emit facingRightChanged();
|
||||
}
|
||||
|
||||
void CatController::pickNextMood()
|
||||
{
|
||||
if (m_paused || m_dragging || m_frozen)
|
||||
return;
|
||||
auto *rng = QRandomGenerator::global();
|
||||
const int roll = rng->bounded(100);
|
||||
|
||||
if (roll < 45) {
|
||||
if (rng->bounded(100) < 20)
|
||||
setFacingRight(!m_facingRight);
|
||||
holdMood(Mood::Walking, rng->bounded(3000, 9000));
|
||||
} else if (roll < 55) {
|
||||
holdMood(Mood::Running, rng->bounded(1200, 2600));
|
||||
} else if (roll < 72) {
|
||||
holdMood(Mood::Sitting, rng->bounded(2500, 6000));
|
||||
} else if (roll < 82) {
|
||||
holdMood(Mood::Grooming, rng->bounded(2500, 4500));
|
||||
} else if (roll < 90) {
|
||||
holdMood(Mood::Stretching, rng->bounded(1800, 3000));
|
||||
} else {
|
||||
holdMood(Mood::Sleeping, rng->bounded(8000, 20000));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user