|
|
|
@@ -0,0 +1,397 @@
|
|
|
|
|
// googly — standalone desk pet for Mega 2560 + 3.5" 8-bit ILI9486 touch shield.
|
|
|
|
|
// Big procedural face: blinks, wanders, sleeps, reacts to pokes and pets.
|
|
|
|
|
// Mood (0-100) persists in EEPROM. See ../IDEA.md and ../DECISIONS.md.
|
|
|
|
|
|
|
|
|
|
#include <MCUFRIEND_kbv.h>
|
|
|
|
|
#include <TouchScreen.h>
|
|
|
|
|
#include <EEPROM.h>
|
|
|
|
|
|
|
|
|
|
MCUFRIEND_kbv tft;
|
|
|
|
|
|
|
|
|
|
// ---- touch (shield shares LCD pins; pressure-only, no x/y calibration) ----
|
|
|
|
|
#define XP 6
|
|
|
|
|
#define XM A2
|
|
|
|
|
#define YP A1
|
|
|
|
|
#define YM 7
|
|
|
|
|
TouchScreen ts(XP, YP, XM, YM, 300);
|
|
|
|
|
const int PRESS_MIN = 80; // ponytail: pressure window is the ghost-touch filter;
|
|
|
|
|
const int PRESS_MAX = 1000; // widen/narrow here if pokes misfire on flaky USB power
|
|
|
|
|
|
|
|
|
|
// ---- tuning knobs ----
|
|
|
|
|
const uint32_t SLEEP_AFTER_MS = 30UL * 60 * 1000; // untouched -> sleep
|
|
|
|
|
const uint32_t EVENT_MIN_MS = 5UL * 60 * 1000; // rare idle events every 5-15 min
|
|
|
|
|
const uint32_t EVENT_MAX_MS = 15UL * 60 * 1000;
|
|
|
|
|
const uint32_t MOOD_DECAY_MS = 15UL * 60 * 1000; // -1 mood per 15 min awake
|
|
|
|
|
const uint32_t EEPROM_SAVE_MS = 5UL * 60 * 1000; // throttle EEPROM writes
|
|
|
|
|
const uint16_t HOLD_MS = 600; // press longer = petting
|
|
|
|
|
const uint16_t MULTIPOKE_MS = 1500; // 3 pokes inside this = dizzy
|
|
|
|
|
const int8_t MOOD_PER_POKE = 4;
|
|
|
|
|
const int8_t MOOD_PER_PET = 12;
|
|
|
|
|
|
|
|
|
|
// ---- colors (RGB565) ----
|
|
|
|
|
#define BG 0x1106 // deep navy
|
|
|
|
|
#define SCLERA 0xFFFF
|
|
|
|
|
#define PUPIL 0x18E3
|
|
|
|
|
#define PINK 0xFB56
|
|
|
|
|
#define RED 0xF800
|
|
|
|
|
#define FLYCLR 0xFFE0
|
|
|
|
|
|
|
|
|
|
// ---- face geometry (480x320 landscape) ----
|
|
|
|
|
const int EYE_LX = 155, EYE_RX = 325, EYE_Y = 135, EYE_R = 75;
|
|
|
|
|
const int PUPIL_R = 26, PUPIL_RANGE = 30;
|
|
|
|
|
const int MOUTH_X = 240, MOUTH_Y = 262, MOUTH_HW = 60;
|
|
|
|
|
|
|
|
|
|
// ---- state ----
|
|
|
|
|
enum State : uint8_t { IDLE, SLEEPING, PETTING };
|
|
|
|
|
State state = IDLE;
|
|
|
|
|
|
|
|
|
|
struct Face { // what's currently on screen, so we only redraw what changed
|
|
|
|
|
int8_t lid = -1; // 0..100 % closed
|
|
|
|
|
int8_t pdx = 0, pdy = 0;
|
|
|
|
|
int8_t curve = -128; // -100 frown .. +100 smile
|
|
|
|
|
int8_t openR = 0; // >0: round open mouth instead of curve
|
|
|
|
|
bool blush = false;
|
|
|
|
|
} cur;
|
|
|
|
|
|
|
|
|
|
struct Pet {
|
|
|
|
|
uint8_t magic; // 0x67 'g'
|
|
|
|
|
uint8_t mood;
|
|
|
|
|
uint32_t pokes;
|
|
|
|
|
uint32_t pets;
|
|
|
|
|
} pet;
|
|
|
|
|
bool petDirty = false;
|
|
|
|
|
uint32_t lastSaveMs = 0;
|
|
|
|
|
|
|
|
|
|
int8_t targPdx = 0, targPdy = 0;
|
|
|
|
|
uint32_t lastTouchMs = 0, nextBlinkMs = 0, nextWanderMs = 0, nextEventMs = 0, nextDecayMs = 0;
|
|
|
|
|
uint32_t pressStartMs = 0, pokeTimes[3] = {0, 0, 0};
|
|
|
|
|
bool pressing = false, wasTouching = false;
|
|
|
|
|
uint32_t petSessionGain = 0, zPhaseMs = 0;
|
|
|
|
|
uint8_t zFrame = 0;
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------- drawing --
|
|
|
|
|
|
|
|
|
|
void drawEye(int cx, int8_t lid, int8_t pdx, int8_t pdy) {
|
|
|
|
|
tft.fillCircle(cx, EYE_Y, EYE_R, SCLERA);
|
|
|
|
|
tft.fillCircle(cx + pdx, EYE_Y + pdy, PUPIL_R, PUPIL);
|
|
|
|
|
tft.fillCircle(cx + pdx - 8, EYE_Y + pdy - 8, 7, SCLERA); // catchlight
|
|
|
|
|
if (lid > 0) {
|
|
|
|
|
int h = (int32_t)lid * 2 * EYE_R / 100;
|
|
|
|
|
tft.fillRect(cx - EYE_R, EYE_Y - EYE_R, 2 * EYE_R + 1, h, BG);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void drawEyes(int8_t lid, int8_t pdx, int8_t pdy) {
|
|
|
|
|
drawEye(EYE_LX, lid, pdx, pdy);
|
|
|
|
|
drawEye(EYE_RX, lid, pdx, pdy);
|
|
|
|
|
cur.lid = lid; cur.pdx = pdx; cur.pdy = pdy;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void drawMouth(int8_t curve, int8_t openR) {
|
|
|
|
|
tft.fillRect(MOUTH_X - MOUTH_HW - 8, MOUTH_Y - 30, 2 * (MOUTH_HW + 8), 64, BG);
|
|
|
|
|
if (openR > 0) {
|
|
|
|
|
tft.fillCircle(MOUTH_X, MOUTH_Y, openR, PUPIL);
|
|
|
|
|
tft.drawCircle(MOUTH_X, MOUTH_Y, openR, SCLERA);
|
|
|
|
|
tft.drawCircle(MOUTH_X, MOUTH_Y, openR + 1, SCLERA);
|
|
|
|
|
} else {
|
|
|
|
|
for (int x = -MOUTH_HW; x <= MOUTH_HW; x += 4) {
|
|
|
|
|
int y = MOUTH_Y - (int32_t)curve * (20 - (x * x) / 180) / 100;
|
|
|
|
|
tft.fillCircle(MOUTH_X + x, y, 4, SCLERA);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
cur.curve = curve; cur.openR = openR;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void drawBlush(bool on) {
|
|
|
|
|
const int xs[2] = {105, 375};
|
|
|
|
|
for (uint8_t i = 0; i < 2; i++) {
|
|
|
|
|
int x = xs[i];
|
|
|
|
|
if (on) {
|
|
|
|
|
tft.fillCircle(x, 232, 15, PINK);
|
|
|
|
|
} else {
|
|
|
|
|
tft.fillCircle(x, 232, 16, BG);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
cur.blush = on;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void drawHeartEyes() {
|
|
|
|
|
const int cxs[2] = {EYE_LX, EYE_RX};
|
|
|
|
|
for (uint8_t i = 0; i < 2; i++) {
|
|
|
|
|
int cx = cxs[i];
|
|
|
|
|
tft.fillCircle(cx, EYE_Y, EYE_R, SCLERA);
|
|
|
|
|
int s = 30;
|
|
|
|
|
tft.fillCircle(cx - s / 2, EYE_Y - s / 3, s / 2 + 2, RED);
|
|
|
|
|
tft.fillCircle(cx + s / 2, EYE_Y - s / 3, s / 2 + 2, RED);
|
|
|
|
|
tft.fillTriangle(cx - s - 1, EYE_Y - s / 6, cx + s + 1, EYE_Y - s / 6, cx, EYE_Y + s, RED);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void setFace(int8_t lid, int8_t pdx, int8_t pdy, int8_t curve, int8_t openR, bool blush) {
|
|
|
|
|
if (lid != cur.lid || pdx != cur.pdx || pdy != cur.pdy) drawEyes(lid, pdx, pdy);
|
|
|
|
|
if (curve != cur.curve || openR != cur.openR) drawMouth(curve, openR);
|
|
|
|
|
if (blush != cur.blush) drawBlush(blush);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int8_t moodCurve() { return pet.mood > 66 ? 80 : (pet.mood > 33 ? 30 : -40); }
|
|
|
|
|
int8_t moodLid() { return pet.mood > 66 ? 0 : (pet.mood > 33 ? 10 : 30); }
|
|
|
|
|
|
|
|
|
|
void clearZs() { tft.fillRect(380, 20, 100, 90, BG); }
|
|
|
|
|
|
|
|
|
|
// ------------------------------------------------------------------ touch --
|
|
|
|
|
|
|
|
|
|
bool rawTouch() {
|
|
|
|
|
TSPoint p = ts.getPoint();
|
|
|
|
|
pinMode(YP, OUTPUT); digitalWrite(YP, HIGH); // give shared pins back to the LCD
|
|
|
|
|
pinMode(XM, OUTPUT); digitalWrite(XM, HIGH);
|
|
|
|
|
return p.z > PRESS_MIN && p.z < PRESS_MAX;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool touched() { // two consecutive samples = ghost-touch debounce
|
|
|
|
|
if (!rawTouch()) return false;
|
|
|
|
|
delay(4);
|
|
|
|
|
return rawTouch();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ------------------------------------------------------------ persistence --
|
|
|
|
|
|
|
|
|
|
void loadPet() {
|
|
|
|
|
EEPROM.get(0, pet);
|
|
|
|
|
if (pet.magic != 0x67) { pet = {0x67, 60, 0, 0}; EEPROM.put(0, pet); }
|
|
|
|
|
if (pet.mood > 100) pet.mood = 60;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void savePet(bool force = false) {
|
|
|
|
|
uint32_t now = millis();
|
|
|
|
|
if (!petDirty || (!force && now - lastSaveMs < EEPROM_SAVE_MS)) return;
|
|
|
|
|
EEPROM.put(0, pet);
|
|
|
|
|
petDirty = false; lastSaveMs = now;
|
|
|
|
|
Serial.print(F("saved mood=")); Serial.println(pet.mood);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void bumpMood(int8_t d) {
|
|
|
|
|
int m = pet.mood + d;
|
|
|
|
|
pet.mood = m < 0 ? 0 : (m > 100 ? 100 : m);
|
|
|
|
|
petDirty = true;
|
|
|
|
|
Serial.print(F("mood=")); Serial.println(pet.mood);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -------------------------------------------------- blocking mini-anims ----
|
|
|
|
|
// ponytail: short animations just block (<=4s); a poke mid-anim is dropped,
|
|
|
|
|
// which reads as the pet being busy emoting. Async engine only if that annoys.
|
|
|
|
|
|
|
|
|
|
void blinkOnce() {
|
|
|
|
|
int8_t base = moodLid();
|
|
|
|
|
const int8_t seq[3] = {60, 100, 60};
|
|
|
|
|
for (uint8_t i = 0; i < 3; i++) { drawEyes(seq[i], cur.pdx, cur.pdy); delay(35); }
|
|
|
|
|
drawEyes(base, cur.pdx, cur.pdy);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void wakeAnim() {
|
|
|
|
|
clearZs();
|
|
|
|
|
drawEyes(100, 0, 0); drawMouth(0, 0); drawBlush(false);
|
|
|
|
|
delay(250);
|
|
|
|
|
const int8_t seq[6] = {70, 40, 90, 40, 10, 0};
|
|
|
|
|
for (uint8_t i = 0; i < 6; i++) { drawEyes(seq[i], 0, 0); delay(70); }
|
|
|
|
|
drawEyes(0, -20, 0); delay(280);
|
|
|
|
|
drawEyes(0, 20, 0); delay(280);
|
|
|
|
|
drawEyes(moodLid(), 0, 0);
|
|
|
|
|
drawMouth(moodCurve(), 0);
|
|
|
|
|
Serial.println(F("awake"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pokeAnim() {
|
|
|
|
|
pet.pokes++; bumpMood(MOOD_PER_POKE);
|
|
|
|
|
drawEyes(0, 0, -6); drawMouth(0, 14); // startled
|
|
|
|
|
delay(350);
|
|
|
|
|
if (pet.mood > 70) { drawHeartEyes(); drawMouth(90, 0); delay(700); drawEyes(moodLid(), 0, 0); }
|
|
|
|
|
else { drawEyes(0, 0, 4); drawMouth(80, 0); delay(500); }
|
|
|
|
|
setFace(moodLid(), 0, 0, moodCurve(), 0, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void dizzyAnim() {
|
|
|
|
|
Serial.println(F("dizzy"));
|
|
|
|
|
drawMouth(0, 10);
|
|
|
|
|
for (int a = 0; a < 720; a += 40) {
|
|
|
|
|
float r = a * 0.01745;
|
|
|
|
|
drawEyes(0, (int8_t)(PUPIL_RANGE * cos(r)), (int8_t)(PUPIL_RANGE * sin(r)));
|
|
|
|
|
delay(28);
|
|
|
|
|
}
|
|
|
|
|
drawEyes(30, 0, 8); drawMouth(-30, 0); delay(500);
|
|
|
|
|
setFace(moodLid(), 0, 0, moodCurve(), 0, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void yawnAnim() {
|
|
|
|
|
Serial.println(F("yawn"));
|
|
|
|
|
const int8_t seq[8] = {8, 14, 20, 24, 24, 20, 12, 0};
|
|
|
|
|
for (uint8_t i = 0; i < 8; i++) {
|
|
|
|
|
drawMouth(0, seq[i]); drawEyes(50, cur.pdx, cur.pdy); delay(140);
|
|
|
|
|
}
|
|
|
|
|
setFace(moodLid(), cur.pdx, cur.pdy, moodCurve(), 0, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void sneezeAnim() {
|
|
|
|
|
Serial.println(F("sneeze"));
|
|
|
|
|
drawEyes(80, 0, 0); drawMouth(0, 10); delay(400);
|
|
|
|
|
drawEyes(100, 0, 0); drawMouth(0, 22);
|
|
|
|
|
tft.setTextColor(SCLERA); tft.setTextSize(3);
|
|
|
|
|
tft.setCursor(186, 16); tft.print(F("achoo!"));
|
|
|
|
|
delay(700);
|
|
|
|
|
tft.fillRect(180, 10, 130, 32, BG);
|
|
|
|
|
setFace(moodLid(), 0, 0, moodCurve(), 0, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void flyAnim() {
|
|
|
|
|
Serial.println(F("fly"));
|
|
|
|
|
int px = -1, py = -1;
|
|
|
|
|
for (int t = 0; t < 130; t++) {
|
|
|
|
|
int fx = 15 + (t * 450) / 130;
|
|
|
|
|
int fy = 30 + (int)(14 * sin(t * 0.35));
|
|
|
|
|
if (px >= 0) tft.fillCircle(px, py, 3, BG);
|
|
|
|
|
tft.fillCircle(fx, fy, 3, FLYCLR);
|
|
|
|
|
px = fx; py = fy;
|
|
|
|
|
int8_t pdx = constrain((fx - MOUTH_X) / 8, -PUPIL_RANGE, PUPIL_RANGE);
|
|
|
|
|
if (pdx != cur.pdx) drawEyes(0, pdx, -14);
|
|
|
|
|
delay(30);
|
|
|
|
|
if (touched()) break; // a poke scares the fly off
|
|
|
|
|
}
|
|
|
|
|
if (px >= 0) tft.fillCircle(px, py, 3, BG);
|
|
|
|
|
setFace(moodLid(), 0, 0, moodCurve(), 0, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void sighAnim() {
|
|
|
|
|
Serial.println(F("sigh"));
|
|
|
|
|
drawEyes(60, 0, 6); drawMouth(0, 8); delay(900);
|
|
|
|
|
setFace(moodLid(), 0, 0, moodCurve(), 0, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void idleEvent() {
|
|
|
|
|
long roll = random(100);
|
|
|
|
|
if (pet.mood <= 40 && roll < 30) { sighAnim(); return; }
|
|
|
|
|
if (roll < 40) flyAnim();
|
|
|
|
|
else if (roll < 70) yawnAnim();
|
|
|
|
|
else if (roll < 90) sneezeAnim();
|
|
|
|
|
else { blinkOnce(); delay(120); blinkOnce(); } // double-blink derp
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ------------------------------------------------------------------ setup --
|
|
|
|
|
|
|
|
|
|
void setup() {
|
|
|
|
|
Serial.begin(115200);
|
|
|
|
|
randomSeed(analogRead(A5) ^ micros());
|
|
|
|
|
loadPet();
|
|
|
|
|
Serial.print(F("googly up. mood=")); Serial.print(pet.mood);
|
|
|
|
|
Serial.print(F(" pokes=")); Serial.print(pet.pokes);
|
|
|
|
|
Serial.print(F(" pets=")); Serial.println(pet.pets);
|
|
|
|
|
|
|
|
|
|
uint16_t id = tft.readID();
|
|
|
|
|
if (id == 0xD3D3) id = 0x9486;
|
|
|
|
|
tft.begin(id);
|
|
|
|
|
tft.setRotation(1);
|
|
|
|
|
tft.fillScreen(BG);
|
|
|
|
|
|
|
|
|
|
wakeAnim();
|
|
|
|
|
uint32_t now = millis();
|
|
|
|
|
lastTouchMs = now;
|
|
|
|
|
nextBlinkMs = now + 2500;
|
|
|
|
|
nextWanderMs = now + 1200;
|
|
|
|
|
nextEventMs = now + random(EVENT_MIN_MS, EVENT_MAX_MS);
|
|
|
|
|
nextDecayMs = now + MOOD_DECAY_MS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ------------------------------------------------------------------- loop --
|
|
|
|
|
|
|
|
|
|
void loop() {
|
|
|
|
|
uint32_t now = millis();
|
|
|
|
|
bool t = touched();
|
|
|
|
|
|
|
|
|
|
// ---- gesture edges ----
|
|
|
|
|
if (t && !wasTouching) { pressStartMs = now; pressing = true; }
|
|
|
|
|
|
|
|
|
|
if (state == SLEEPING) {
|
|
|
|
|
if (t) {
|
|
|
|
|
state = IDLE;
|
|
|
|
|
lastTouchMs = now;
|
|
|
|
|
bumpMood(3);
|
|
|
|
|
wakeAnim();
|
|
|
|
|
nextEventMs = now + random(EVENT_MIN_MS, EVENT_MAX_MS);
|
|
|
|
|
}
|
|
|
|
|
} else if (state == PETTING) {
|
|
|
|
|
if (!t) { // hand lifted
|
|
|
|
|
pet.pets++; petDirty = true;
|
|
|
|
|
if (pet.mood > 70) { drawHeartEyes(); drawMouth(90, 0); delay(800); }
|
|
|
|
|
drawBlush(false);
|
|
|
|
|
setFace(moodLid(), 0, 0, moodCurve(), 0, false);
|
|
|
|
|
state = IDLE;
|
|
|
|
|
Serial.println(F("petted"));
|
|
|
|
|
} else if (now - zPhaseMs > 500 && petSessionGain < (uint32_t)MOOD_PER_PET) {
|
|
|
|
|
bumpMood(1); petSessionGain++; zPhaseMs = now;
|
|
|
|
|
}
|
|
|
|
|
} else { // IDLE
|
|
|
|
|
if (pressing && t && now - pressStartMs > HOLD_MS) {
|
|
|
|
|
state = PETTING;
|
|
|
|
|
petSessionGain = 0; zPhaseMs = now;
|
|
|
|
|
setFace(80, 0, 6, 60, 0, true); // closed-ish eyes, smile, blush
|
|
|
|
|
Serial.println(F("petting..."));
|
|
|
|
|
} else if (!t && wasTouching && pressing && now - pressStartMs <= HOLD_MS) {
|
|
|
|
|
// completed tap = poke
|
|
|
|
|
pokeTimes[0] = pokeTimes[1]; pokeTimes[1] = pokeTimes[2]; pokeTimes[2] = now;
|
|
|
|
|
if (pokeTimes[2] - pokeTimes[0] < MULTIPOKE_MS) dizzyAnim();
|
|
|
|
|
else pokeAnim();
|
|
|
|
|
lastTouchMs = now;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!t) {
|
|
|
|
|
// ---- ambient idle behavior ----
|
|
|
|
|
if (now > nextBlinkMs) { blinkOnce(); nextBlinkMs = now + random(2500, 6000); }
|
|
|
|
|
if (now > nextWanderMs) {
|
|
|
|
|
if (cur.pdx == targPdx && cur.pdy == targPdy) {
|
|
|
|
|
targPdx = random(-PUPIL_RANGE, PUPIL_RANGE + 1);
|
|
|
|
|
targPdy = random(-16, 17);
|
|
|
|
|
nextWanderMs = now + random(1500, 3500);
|
|
|
|
|
} else {
|
|
|
|
|
int8_t sx = constrain(targPdx - cur.pdx, -6, 6);
|
|
|
|
|
int8_t sy = constrain(targPdy - cur.pdy, -6, 6);
|
|
|
|
|
drawEyes(cur.lid, cur.pdx + sx, cur.pdy + sy);
|
|
|
|
|
nextWanderMs = now + 60;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (now > nextEventMs) {
|
|
|
|
|
idleEvent();
|
|
|
|
|
nextEventMs = now + random(EVENT_MIN_MS, EVENT_MAX_MS);
|
|
|
|
|
}
|
|
|
|
|
if (now - lastTouchMs > SLEEP_AFTER_MS) {
|
|
|
|
|
state = SLEEPING;
|
|
|
|
|
setFace(100, 0, 0, 10, 0, false);
|
|
|
|
|
zPhaseMs = now; zFrame = 0;
|
|
|
|
|
savePet(true);
|
|
|
|
|
Serial.println(F("zzz"));
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
lastTouchMs = now;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- sleeping z's ----
|
|
|
|
|
if (state == SLEEPING && now - zPhaseMs > 900) {
|
|
|
|
|
clearZs();
|
|
|
|
|
tft.setTextColor(SCLERA); tft.setTextSize(2);
|
|
|
|
|
for (uint8_t i = 0; i < 3; i++) {
|
|
|
|
|
uint8_t ph = (zFrame + i) % 3;
|
|
|
|
|
tft.setCursor(395 + i * 22, 88 - ph * 26 - i * 6);
|
|
|
|
|
tft.print('z');
|
|
|
|
|
}
|
|
|
|
|
zFrame++; zPhaseMs = now;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- mood decay + persistence ----
|
|
|
|
|
if (state != SLEEPING && now > nextDecayMs) {
|
|
|
|
|
if (pet.mood > 0) bumpMood(-1);
|
|
|
|
|
nextDecayMs = now + MOOD_DECAY_MS;
|
|
|
|
|
}
|
|
|
|
|
savePet();
|
|
|
|
|
|
|
|
|
|
if (!t) pressing = false;
|
|
|
|
|
wasTouching = t;
|
|
|
|
|
delay(25);
|
|
|
|
|
}
|