#include "catcontroller.h" #include "appsettings.h" #include #include #include namespace { constexpr qreal kChipReach = 26.0; // how close the nose has to get constexpr int kChipMinDelayMs = 45000; constexpr int kChipMaxDelayMs = 150000; } 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(); m_chipTimer.setSingleShot(true); connect(&m_chipTimer, &QTimer::timeout, this, &CatController::tossChip); // TNCAT_POSE=Sleeping freezes the cat in one mood (handy for screenshots). const QByteArray pose = qgetenv("TNCAT_POSE"); if (!pose.isEmpty()) { const QMetaEnum me = QMetaEnum::fromType(); bool ok = false; const int value = me.keyToValue(pose.constData(), &ok); if (ok) { m_frozen = true; setMood(static_cast(value)); } } if (!m_frozen) { pickNextMood(); scheduleChip(); } } qreal CatController::catNose() const { return m_facingRight ? m_x + m_catWidth * 0.78 : m_x + m_catWidth * 0.22; } void CatController::setX(qreal x) { if (qFuzzyCompare(x, m_x)) return; m_x = x; emit xChanged(); } void CatController::setStageWidth(qreal width) { if (qFuzzyCompare(width, m_stageWidth)) return; m_stageWidth = width; emit stageWidthChanged(); } void CatController::setCatWidth(qreal width) { if (qFuzzyCompare(width, m_catWidth)) return; m_catWidth = width; emit catWidthChanged(); } void CatController::setCatCenter(const QPointF ¢er) { if (center == m_catCenter) return; m_catCenter = center; emit catCenterChanged(); } void CatController::setPaused(bool paused) { if (paused == m_paused) return; m_paused = paused; emit pausedChanged(); if (paused) { m_moodTimer.stop(); m_chipTimer.stop(); endChase(); endCoffee(); setMood(Mood::Sleeping); } else { pickNextMood(); scheduleChip(); } } void CatController::setDragging(bool dragging) { if (dragging == m_dragging) return; m_dragging = dragging; emit draggingChanged(); if (m_frozen) return; if (dragging) { m_moodTimer.stop(); endCoffee(); 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(); endChase(); endCoffee(); holdMood(Mood::Loving, 1800); } void CatController::doTrick(Mood mood) { if (m_paused) return; m_frozen = false; if (mood == Mood::Drinking) { coffeeBreak(); return; } endChase(); if (mood != Mood::Drinking) endCoffee(); holdMood(mood, mood == Mood::Sleeping ? 15000 : 3500); } void CatController::holdMood(Mood mood, int ms) { setMood(mood); m_moodTimer.start(ms); } // A curled metal chip skitters in from one side and the cat goes after it. void CatController::tossChip() { if (m_paused || m_frozen || m_chipVisible) return; if (m_cupVisible) { // finish the coffee first scheduleChip(); return; } if (!m_settings->chipsEnabled()) { scheduleChip(); return; } auto *rng = QRandomGenerator::global(); const bool fromLeft = rng->bounded(2) == 0; const qreal margin = qMin(200.0, m_stageWidth * 0.2); setChipX(fromLeft ? margin : m_stageWidth - margin); m_batsLeft = rng->bounded(2, 5); m_chipVisible = true; emit chipVisibleChanged(); m_moodTimer.stop(); 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() { auto *rng = QRandomGenerator::global(); ++m_chipKick; emit chipKickChanged(); const qreal distance = rng->bounded(140, 380); qreal next = m_chipX + (m_facingRight ? distance : -distance); const qreal edge = 40.0; if (next < edge || next > m_stageWidth - edge) { next = qBound(edge, next, m_stageWidth - edge); --m_batsLeft; // a chip against the wall is nearly done for } setChipX(next); if (--m_batsLeft <= 0) { endChase(); holdMood(Mood::Sitting, 2500); // pleased with itself scheduleChip(); } else { holdMood(Mood::Batting, 420); } } void CatController::endChase() { if (!m_chipVisible) return; m_chipVisible = false; m_batsLeft = 0; emit chipVisibleChanged(); } void CatController::scheduleChip() { m_chipTimer.start(QRandomGenerator::global()->bounded(kChipMinDelayMs, kChipMaxDelayMs)); } void CatController::setChipX(qreal x) { if (qFuzzyCompare(x, m_chipX)) return; m_chipX = x; emit chipXChanged(); } void CatController::trackCursor() { if (m_catCenter.isNull() || !m_settings->followCursor()) { if (!m_look.isNull()) { m_look = {}; emit lookChanged(); } return; } QPointF delta = QCursor::pos() - m_catCenter; 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; if (m_dragging || m_paused || m_frozen) return; const qreal maxX = qMax(0.0, m_stageWidth - m_catWidth); if (m_mood == Mood::Chasing) { const qreal delta = m_chipX - catNose(); setFacingRight(delta >= 0); if (qAbs(delta) <= kChipReach) { batChip(); return; } const qreal step = m_settings->walkSpeed() * 4.0 * dt; setX(qBound(0.0, m_x + (delta > 0 ? step : -step), maxX)); return; } if (m_mood != Mood::Walking && m_mood != Mood::Running) return; const qreal speed = m_settings->walkSpeed() * (m_mood == Mood::Running ? 4.5 : 1.0); qreal next = m_x + (m_facingRight ? speed * dt : -speed * dt); 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; if (m_chipVisible) { // a chase is still on, keep after the chip setMood(Mood::Chasing); return; } endCoffee(); auto *rng = QRandomGenerator::global(); const int roll = rng->bounded(100); if (roll < 8 && m_settings->coffeeEnabled()) { coffeeBreak(); return; } 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)); } }