#include "BuddyLogic.h" #include // Replaces Arduino constrain() template static T clamp(T v, T lo, T hi) { return std::max(lo, std::min(hi, v)); } void initBuddy(BuddyState &b, uint32_t now) { b = {}; b.blinkState = BLINK_OPEN; b.blinkRy = EYE_RY; b.lastEvent = now; b.nextBlink = now + 3000; b.nextLook = now + 2000; b.nextZzz = now + 3000; b.nextMicroTremor = now + 500; } void setBuddyMood(BuddyState &b, Mood m, uint32_t now, uint32_t durationMs) { b.mood = m; b.revertAt = (durationMs > 0) ? now + durationMs : 0; b.lastEvent = now; switch (m) { case MOOD_HAPPY: b.pupilTargetDx = 0; b.pupilTargetDy = -2; break; case MOOD_SLEEPY: b.pupilTargetDx = 0; b.pupilTargetDy = 4; break; case MOOD_SURPRISED: b.pupilTargetDx = 0; b.pupilTargetDy = 0; break; case MOOD_SAD: b.pupilTargetDx = 0; b.pupilTargetDy = 4; break; case MOOD_ANGRY: b.pupilTargetDx = -2; b.pupilTargetDy = 2; break; default: break; } } void updateBuddyAnim(BuddyState &b, uint32_t now, RngFn rng) { // Timed mood revert if (b.revertAt > 0 && now >= b.revertAt) { b.mood = MOOD_NORMAL; b.revertAt = 0; } // Auto-sleep after 5 min idle if (b.mood == MOOD_NORMAL && now - b.lastEvent > 300000UL) setBuddyMood(b, MOOD_SLEEPY, now, 0); // Blink state machine (disabled for SURPRISED, EXCITED, WINKs, ALERT) if (b.mood != MOOD_SURPRISED && b.mood != MOOD_EXCITED && b.mood != MOOD_WINK_L && b.mood != MOOD_WINK_R && b.mood != MOOD_ALERT) { switch (b.blinkState) { case BLINK_OPEN: if (now >= b.nextBlink) b.blinkState = BLINK_CLOSING; break; case BLINK_CLOSING: if (b.blinkRy > 3) b.blinkRy -= 4; else { b.blinkRy = 1; b.blinkState = BLINK_CLOSED; b.closedTicks = 0; } break; case BLINK_CLOSED: if (b.closedTicks++ >= 2) b.blinkState = BLINK_OPENING; break; case BLINK_OPENING: b.blinkRy += 4; if (b.blinkRy >= EYE_RY) { b.blinkRy = EYE_RY; b.blinkState = BLINK_OPEN; b.nextBlink = now + (int32_t)((b.mood == MOOD_SLEEPY) ? rng(800, 2000) : rng(2500, 6000)); } break; } } else { b.blinkRy = EYE_RY; } // Saccadic pupil movement — fast (3px/tick) when far (|delta|>=4), slow (1px) when close { int8_t ddx = b.pupilTargetDx - b.pupilDx; int8_t ddy = b.pupilTargetDy - b.pupilDy; int8_t absDdx = ddx < 0 ? (int8_t)-ddx : ddx; int8_t absDdy = ddy < 0 ? (int8_t)-ddy : ddy; if (ddx) b.pupilDx = (int8_t)(b.pupilDx + (ddx > 0 ? 1 : -1) * (absDdx >= 4 ? 3 : 1)); if (ddy) b.pupilDy = (int8_t)(b.pupilDy + (ddy > 0 ? 1 : -1) * (absDdy >= 4 ? 3 : 1)); } // Gaze — random saccades + micro-tremor { bool fixated = (b.pupilDx == b.pupilTargetDx && b.pupilDy == b.pupilTargetDy); if (b.mood == MOOD_SLEEPY) { if (fixated && now >= b.nextLook) { b.pupilTargetDx = (int8_t)rng(-3, 4); b.pupilTargetDy = (int8_t)rng(2, 6); b.nextLook = now + (uint32_t)rng(4000, 9000); } } else if (b.mood != MOOD_WINK_L && b.mood != MOOD_WINK_R) { if (fixated && now >= b.nextLook) { b.pupilTargetDx = (int8_t)rng(-3, 11); b.pupilTargetDy = (int8_t)rng(-5, 6); b.nextLook = now + (uint32_t)rng(1500, 4500); } if ((b.mood == MOOD_NORMAL || b.mood == MOOD_HAPPY) && fixated && now >= b.nextMicroTremor) { b.pupilTargetDx = (int8_t)clamp( (int32_t)b.pupilTargetDx + rng(-1, 2), (int32_t)-7, (int32_t)7); b.pupilTargetDy = (int8_t)clamp( (int32_t)b.pupilTargetDy + rng(-1, 2), (int32_t)-5, (int32_t)5); b.nextMicroTremor = now + (uint32_t)rng(300, 700); } } } // ZZZ bubbles when sleepy if (b.mood == MOOD_SLEEPY && now >= b.nextZzz) { b.zzzPhase = (b.zzzPhase % 3) + 1; b.nextZzz = now + 700; } }