132 lines
4.4 KiB
C++
132 lines
4.4 KiB
C++
#include "BuddyLogic.h"
|
|
#include <algorithm>
|
|
|
|
// Replaces Arduino constrain()
|
|
template<typename T>
|
|
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;
|
|
}
|
|
}
|