#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <MPU6050.h>
// -------------------------------------------------------------------------
// HARDWARE CONFIGURATION: SEEED XIAO ESP32-C3
// -------------------------------------------------------------------------
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
// Pin Definitions - CAREFULLY MAPPED FOR ESP32-C3
// D4 (GPIO 6) and D5 (GPIO 7) are the hardware I2C pins.
#define PIN_SDA 6 // D4
#define PIN_SCL 7 // D5
// Buttons (Using D0-D3 to keep I2C free)
#define BUTTON_UP 2 // D0
#define BUTTON_DOWN 3 // D1
#define BUTTON_OPEN 4 // D2 (Select/Enter)
#define BUTTON_MENUEXIT 5 // D3 (Exit/Menu)
// Buzzer
#define BUZZER_PIN 10 // D10 (GPIO 10 on XIAO ESP32C3)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
MPU6050 mpu;
// -------------------------------------------------------------------------
// GLOBAL VARIABLES (Original Logic Preserved)
// -------------------------------------------------------------------------
int leftEyeX = 40, rightEyeX = 80, eyeY = 18;
int eyeWidth = 25, eyeHeight = 30;
int targetLeftEyeX = leftEyeX, targetRightEyeX = rightEyeX, targetEyeY = eyeY;
int moveSpeed = 2;
int pupilRadius = 5;
int leftPupilX, leftPupilY, rightPupilX, rightPupilY;
int targetLeftPupilX, targetLeftPupilY, targetRightPupilX, targetRightPupilY;
int blinkState = 0, blinkDelay = 2000;
unsigned long lastBlinkTime = 0;
int expression = 0;
enum FaceAnim {
FACE_NONE, FACE_WINK_L, FACE_WINK_R, FACE_HEARTS, FACE_SPARKLE, FACE_CROSS,
FACE_SLEEPY, FACE_TONGUE, FACE_SURPRISE, FACE_WHISTLE, FACE_BLUSH,
FACE_EYEBROW_WIGGLE, FACE_SERENE,
FACE_GIZMO_HAPPY, FACE_GIZMO_CURIOUS, FACE_GIZMO_CONFUSED, FACE_GIZMO_SAD,
FACE_LAUGH, FACE_ANGRY, FACE_SKEPTICAL, FACE_CRY, FACE_TEARS,
FACE_DIZZY, FACE_KISS, FACE_SUSPICIOUS,
FACE_CAT_HAPPY, FACE_CAT_PURR, FACE_CAT_BLINK
};
FaceAnim faceAnim = FACE_NONE;
unsigned long faceAnimStart = 0;
unsigned long faceAnimNext = 0;
unsigned long faceAnimDur = 0;
int faceAnimFrame = 0;
int eyelidL = 0, eyelidR = 0;
int pupilR = 5, targetPupilR = 5;
int microX = 0, microY = 0;
unsigned long nextMicro = 0, microEnd = 0;
int browLiftL = 0, browLiftR = 0;
int mouthType = 0;
// Dizzy Logic
int16_t lastAx = 0, lastAy = 0;
unsigned long lastShakeTime = 0;
int shakeCount = 0;
bool soundEnabled = true;
unsigned long sfxNext = 0;
int sfxType = 0;
int sfxStep = 0;
unsigned long sfxTs = 0;
enum Mode { MODE_ANIM, MODE_MENU, MODE_GAME_FLAPPY, MODE_GAME_TTT, MODE_GAME_RACER, MODE_GAME_SHOOTER, MODE_GAME_BREAKOUT, MODE_TOOL_LEVEL, MODE_TOOL_THEREMIN, MODE_TOOL_DICE };
Mode mode = MODE_ANIM;
enum MenuId { MENU_FLAPPY, MENU_TICTAC, MENU_RACER, MENU_SHOOTER, MENU_BREAKOUT, MENU_LEVEL, MENU_THEREMIN, MENU_DICE, MENU_SOUND, MENU_BACK };
const int menuLength = 10;
int currentMenuItem = 0;
int topIndex = 0;
const int linesVisible = 3;
// Button Debouncing
bool lastUp = HIGH, lastDown = HIGH, lastOpen = HIGH, lastMenuExit = HIGH;
unsigned long upTs = 0, downTs = 0, openTs = 0, menuExitTs = 0;
const unsigned long debounceMs = 150;
// Frame Rate Control
unsigned long lastFrameTime = 0;
const int FRAME_DELAY = 16; // ~60 FPS
// Game Variables
float birdY = SCREEN_HEIGHT / 2;
float birdV = 0;
const float gravity = 0.25;
const float flapImpulse = -3.2;
int score = 0;
bool gameOver = false;
bool flappyOverSfxDone = false;
struct Pipe { int x; int gapY; int gapH; };
Pipe pipes[3];
int tttBoard[9];
int tttCursor = 4;
int tttTurn = 1;
bool tttOver = false;
int tttWinner = 0;
const int SLICES = 16;
const int horizonY = 10;
int roadCenter = SCREEN_WIDTH/2;
int centerOffset = 0;
int targetCenterOffset = 0;
int speed = 2;
bool racerOver = false;
unsigned long lastSpawn = 0;
int racerScore = 0;
struct Obj { int slice; int lane; int w; bool active; };
Obj objs[5];
int carW = 14;
int carH = 8;
int carY = SCREEN_HEIGHT - 8;
// Shooter Variables
struct Bullet { float x; float y; bool active; };
Bullet bullets[6];
struct Enemy { float x; float y; bool active; int type; };
Enemy enemies[6];
float shipX = SCREEN_WIDTH/2;
int shipW = 12;
bool shooterOver = false;
int shooterScore = 0;
unsigned long lastEnemySpawn = 0;
int enemySpawnRate = 1500;
// Breakout Variables
float padX = SCREEN_WIDTH/2;
int padW = 24;
float ballX, ballY, ballVX, ballVY;
bool ballActive = false;
bool breakoutOver = false;
int breakoutScore = 0;
bool bricks[4][10]; // 4 rows, 10 columns
int brickW = 12;
int brickH = 6;
int brickGap = 1;
int lives = 3;
// -------------------------------------------------------------------------
// HELPER FUNCTIONS
// -------------------------------------------------------------------------
void drawSpiral(int cx, int cy, bool clockwise) {
float t = (millis() % 1000) / 1000.0 * 2 * PI;
if (!clockwise) t = -t;
for (int r=0; r<14; r+=2) {
float ang = t + r * 0.5;
int px = cx + r * cos(ang);
int py = cy + r * sin(ang);
display.fillCircle(px, py, 1, BLACK);
// Second arm
float ang2 = ang + PI;
int px2 = cx + r * cos(ang2);
int py2 = cy + r * sin(ang2);
display.fillCircle(px2, py2, 1, BLACK);
}
}
int sliceY(int s) { return horizonY + (SCREEN_HEIGHT - horizonY - 2) * s / (SLICES - 1); }
int sliceWidth(int s) { int minW = 24; int maxW = 92; return minW + (maxW - minW) * s / (SLICES - 1); }
void buzzerInit(){ pinMode(BUZZER_PIN, OUTPUT); noTone(BUZZER_PIN); }
void buzzerTone(int f){ if (!soundEnabled) return; tone(BUZZER_PIN, f); }
void buzzerOff(){ noTone(BUZZER_PIN); }
void enqueueSfx(int type){ if (!soundEnabled) return; sfxType = type; sfxStep = 0; sfxTs = millis(); }
// -------------------------------------------------------------------------
// GAME LOGIC (FLAPPY)
// -------------------------------------------------------------------------
void startFlappy() {
birdY = SCREEN_HEIGHT / 2;
birdV = 0;
score = 0;
gameOver = false;
flappyOverSfxDone = false;
int startX = 150;
for (int i = 0; i < 3; i++) {
pipes[i].x = startX + i * 60;
pipes[i].gapH = 26;
pipes[i].gapY = random(12, SCREEN_HEIGHT - 12 - pipes[i].gapH);
}
}
void updateFlappy(bool flap) {
if (gameOver) return;
if (flap) { birdV = flapImpulse; enqueueSfx(10); }
birdV += gravity;
birdY += birdV;
if (birdY < 2) { birdY = 2; birdV = 0; }
if (birdY > SCREEN_HEIGHT - 2) { gameOver = true; }
for (int i = 0; i < 3; i++) {
pipes[i].x -= 2;
if (pipes[i].x + 16 < 0) {
int mx = max(pipes[(i+2)%3].x, pipes[(i+1)%3].x);
pipes[i].x = mx + 60;
pipes[i].gapH = 26;
pipes[i].gapY = random(12, SCREEN_HEIGHT - 12 - pipes[i].gapH);
}
}
for (int i = 0; i < 3; i++) {
int bx = 24;
int by = (int)birdY;
int px = pipes[i].x;
int pw = 16;
if (bx + 6 >= px && bx - 6 <= px + pw) {
if (by <= pipes[i].gapY || by >= pipes[i].gapY + pipes[i].gapH) gameOver = true;
}
if (px + pw == bx) { score++; enqueueSfx(11); }
}
if (gameOver && !flappyOverSfxDone) { enqueueSfx(12); flappyOverSfxDone = true; }
}
void drawFlappy() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0);
display.print("Score:");
display.print(score);
for (int i = 0; i < 3; i++) {
int px = pipes[i].x;
int pw = 16;
int gy = pipes[i].gapY;
int gh = pipes[i].gapH;
display.fillRect(px, 10, pw, gy - 10, SSD1306_WHITE);
display.fillRect(px, gy + gh, pw, SCREEN_HEIGHT - (gy + gh), SSD1306_WHITE);
}
display.fillCircle(24, (int)birdY, 4, SSD1306_WHITE);
if (gameOver) {
display.setTextSize(2);
display.setCursor(30, 24);
display.println("GAME");
display.setCursor(30, 40);
display.println("OVER");
}
}
// -------------------------------------------------------------------------
// GAME LOGIC (TIC TAC TOE)
// -------------------------------------------------------------------------
bool tttCheckWin(int p) {
int w[8][3] = {{0,1,2},{3,4,5},{6,7,8},{0,3,6},{1,4,7},{2,5,8},{0,4,8},{2,4,6}};
for (int i=0;i<8;i++) if (tttBoard[w[i][0]]==p && tttBoard[w[i][1]]==p && tttBoard[w[i][2]]==p) return true;
return false;
}
bool tttFull() { for (int i=0;i<9;i++) if (tttBoard[i]==0) return false; return true; }
int tttEval() { if (tttCheckWin(2)) return 10; if (tttCheckWin(1)) return -10; return 0; }
int tttMinimax(bool isMax, int depth) {
int sc = tttEval();
if (sc==10) return sc - depth;
if (sc==-10) return sc + depth;
if (tttFull()) return 0;
if (isMax) {
int best=-1000;
for (int i=0;i<9;i++) if (tttBoard[i]==0){ tttBoard[i]=2; int v=tttMinimax(false, depth+1); tttBoard[i]=0; if(v>best) best=v; }
return best;
} else {
int best=1000;
for (int i=0;i<9;i++) if (tttBoard[i]==0){ tttBoard[i]=1; int v=tttMinimax(true, depth+1); tttBoard[i]=0; if(v<best) best=v; }
return best;
}
}
void tttCpuMove() {
int bestVal=-1000, bestIdx=-1;
for (int i=0;i<9;i++) if (tttBoard[i]==0){ tttBoard[i]=2; int v=tttMinimax(false,0); tttBoard[i]=0; if(v>bestVal){bestVal=v; bestIdx=i;} }
if (bestIdx==-1) for (int i=0;i<9;i++) if (tttBoard[i]==0){bestIdx=i;break;}
if (bestIdx!=-1) tttBoard[bestIdx]=2;
}
void startTicTac() { for(int i=0;i<9;i++) tttBoard[i]=0; tttCursor=4; tttTurn=1; tttOver=false; tttWinner=0; }
void updateTicTac(bool upPress, bool downPress, bool openPress) {
if (tttOver){ if(openPress){ startTicTac(); enqueueSfx(20); } return; }
if (upPress){ tttCursor--; if(tttCursor<0) tttCursor=8; }
if (downPress){ tttCursor++; if(tttCursor>8) tttCursor=0; }
if (openPress && tttBoard[tttCursor]==0){
tttBoard[tttCursor]=1;
enqueueSfx(20);
if (tttCheckWin(1)){ tttOver=true; tttWinner=1; enqueueSfx(21); return; }
if (tttFull()){ tttOver=true; tttWinner=0; enqueueSfx(23); return; }
tttCpuMove();
if (tttCheckWin(2)){ tttOver=true; tttWinner=2; enqueueSfx(22); return; }
if (tttFull()){ tttOver=true; tttWinner=0; enqueueSfx(23); return; }
}
}
void drawTicTac() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0);
display.print("TicTac");
int gx=24, gy=10, cw=26, ch=16;
display.drawLine(gx+cw, gy, gx+cw, gy+3*ch, SSD1306_WHITE);
display.drawLine(gx+2*cw, gy, gx+2*cw, gy+3*ch, SSD1306_WHITE);
display.drawLine(gx, gy+ch, gx+3*cw, gy+ch, SSD1306_WHITE);
display.drawLine(gx, gy+2*ch, gx+3*cw, gy+2*ch, SSD1306_WHITE);
for (int i=0;i<9;i++) {
int r=i/3, c=i%3; int cx=gx+c*cw, cy=gy+r*ch;
if (i==tttCursor && !tttOver) display.drawRect(cx+1, cy+1, cw-2, ch-2, SSD1306_WHITE);
if (tttBoard[i]==1) {
display.drawLine(cx+4, cy+3, cx+cw-4, cy+ch-3, SSD1306_WHITE);
display.drawLine(cx+cw-4, cy+3, cx+4, cy+ch-3, SSD1306_WHITE);
} else if (tttBoard[i]==2) {
display.drawCircle(cx+cw/2, cy+ch/2, min(cw,ch)/2-5, SSD1306_WHITE);
}
}
display.setCursor(0,56);
if (!tttOver) display.print("You");
else if (tttWinner==1) display.print("You win");
else if (tttWinner==2) display.print("CPU win");
else display.print("Draw");
}
// -------------------------------------------------------------------------
// GAME LOGIC (RACER)
// -------------------------------------------------------------------------
void startRacer(){
racerOver = false;
racerScore = 0;
speed = 2;
centerOffset = 0;
targetCenterOffset = 0;
for (int i=0;i<5;i++){ objs[i].active=false; }
lastSpawn = millis();
}
void spawnObstacle(){
for (int i=0;i<5;i++){
if (!objs[i].active){
objs[i].active = true;
objs[i].slice = 2;
objs[i].lane = random(-80, 81);
objs[i].w = random(8, 14);
return;
}
}
}
void updateRacer(bool upPress, bool downPress, bool accelHeld){
if (racerOver) return;
int16_t ax, ay, az;
mpu.getAcceleration(&ax, &ay, &az);
targetCenterOffset = map(ax, -17000, 17000, -28, 28);
centerOffset = (centerOffset*85 + targetCenterOffset*15) / 100;
if (upPress) centerOffset -= 3;
if (downPress) centerOffset += 3;
if (centerOffset < -34) centerOffset = -34;
if (centerOffset > 34) centerOffset = 34;
if (accelHeld) { if (speed < 6) speed++; } else { if (speed > 2) speed--; }
unsigned long t = millis();
if (t - lastSpawn > (unsigned long)(900 - speed*100)) { spawnObstacle(); lastSpawn = t; }
for (int i=0;i<5;i++){
if (!objs[i].active) continue;
objs[i].slice += speed/2;
if (objs[i].slice >= SLICES){
int s = SLICES - 1;
int w = sliceWidth(s);
int ox = roadCenter + centerOffset + (objs[i].lane * (w/2)) / 100;
int ow = objs[i].w;
int carX1 = roadCenter + centerOffset - carW/2;
int carX2 = roadCenter + centerOffset + carW/2;
int ox1 = ox - ow/2, ox2 = ox + ow/2;
bool overlap = !(carX2 < ox1 || carX1 > ox2);
if (overlap) racerOver = true;
objs[i].active = false;
if (!racerOver) racerScore++;
}
}
roadCenter = SCREEN_WIDTH/2;
}
void drawRacer(){
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0);
display.print("Spd:");
display.print(speed);
display.setCursor(64,0);
display.print("Score:");
display.print(racerScore);
for (int s=0; s<SLICES; s++){
int w = sliceWidth(s);
int y = sliceY(s);
int left = roadCenter + centerOffset - w/2;
int right = roadCenter + centerOffset + w/2;
if (left < 0) left = 0;
if (right > SCREEN_WIDTH) right = SCREEN_WIDTH;
display.drawLine(left, y, right, y, SSD1306_WHITE);
if (s % 2 == 0) {
int mid = (left + right)/2;
display.drawPixel(mid, y, SSD1306_WHITE);
}
}
for (int i=0;i<5;i++){
if (!objs[i].active) continue;
int s = objs[i].slice; if (s < 0) s = 0; if (s >= SLICES) s = SLICES-1;
int w = sliceWidth(s);
int y = sliceY(s);
int ox = roadCenter + centerOffset + (objs[i].lane * (w/2)) / 100;
int ow = max(6, objs[i].w * (s+1) / SLICES);
int oh = max(3, ow/2);
int x1 = ox - ow/2; if (x1 < 0) x1 = 0;
int y1 = y - oh/2; if (y1 < horizonY) y1 = horizonY;
display.fillRect(x1, y1, ow, oh, SSD1306_WHITE);
}
int carX = roadCenter + centerOffset;
int carX1 = carX - carW/2;
int carY1 = carY - carH;
if (carX1 < 0) carX1 = 0;
display.fillRect(carX1, carY1, carW, carH, SSD1306_WHITE);
if (racerOver){
display.setTextSize(2);
display.setCursor(24, 24);
display.println("CRASH");
}
}
// -------------------------------------------------------------------------
// GAME LOGIC (SHOOTER)
// -------------------------------------------------------------------------
void startShooter() {
shooterOver = false;
shooterScore = 0;
shipX = SCREEN_WIDTH / 2;
lastEnemySpawn = millis();
for(int i=0; i<6; i++) { bullets[i].active = false; enemies[i].active = false; }
}
void spawnEnemy() {
for(int i=0; i<6; i++) {
if(!enemies[i].active) {
enemies[i].active = true;
enemies[i].x = random(0, SCREEN_WIDTH - 8);
enemies[i].y = -8;
enemies[i].type = random(0, 2); // 0=Asteroid, 1=Ship
return;
}
}
}
void fireBullet() {
for(int i=0; i<6; i++) {
if(!bullets[i].active) {
bullets[i].active = true;
bullets[i].x = shipX;
bullets[i].y = SCREEN_HEIGHT - 12;
enqueueSfx(10); // Reuse flap sound for shoot
return;
}
}
}
void updateShooter(bool openPress) {
if (shooterOver) return;
// Control Ship with Gyro
int16_t ax, ay, az;
mpu.getAcceleration(&ax, &ay, &az);
float tilt = map(ax, -17000, 17000, -5, 5);
shipX -= tilt;
if (shipX < 6) shipX = 6;
if (shipX > SCREEN_WIDTH - 6) shipX = SCREEN_WIDTH - 6;
// Shoot
if (openPress) fireBullet();
// Update Bullets
for(int i=0; i<6; i++) {
if(bullets[i].active) {
bullets[i].y -= 3;
if(bullets[i].y < -2) bullets[i].active = false;
}
}
// Spawn Enemies
if (millis() - lastEnemySpawn > enemySpawnRate) {
spawnEnemy();
lastEnemySpawn = millis();
if(enemySpawnRate > 500) enemySpawnRate -= 10;
}
// Update Enemies & Collision
for(int i=0; i<6; i++) {
if(enemies[i].active) {
enemies[i].y += (enemies[i].type == 0 ? 1.5 : 2.5);
if(enemies[i].y > SCREEN_HEIGHT) { enemies[i].active = false; }
// Check collision with ship
if (abs(enemies[i].x - shipX) < 8 && abs(enemies[i].y - (SCREEN_HEIGHT - 8)) < 8) {
shooterOver = true;
enqueueSfx(12);
}
// Check collision with bullets
for(int b=0; b<6; b++) {
if(bullets[b].active) {
if (abs(enemies[i].x - bullets[b].x) < 8 && abs(enemies[i].y - bullets[b].y) < 6) {
enemies[i].active = false;
bullets[b].active = false;
shooterScore += 10;
enqueueSfx(11);
}
}
}
}
}
}
void drawShooter() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0);
display.print("Score:");
display.print(shooterScore);
// Draw Ship
display.fillTriangle(shipX, SCREEN_HEIGHT-10, shipX-6, SCREEN_HEIGHT, shipX+6, SCREEN_HEIGHT, WHITE);
// Draw Bullets
for(int i=0; i<6; i++) {
if(bullets[i].active) display.drawFastVLine(bullets[i].x, bullets[i].y, 3, WHITE);
}
// Draw Enemies
for(int i=0; i<6; i++) {
if(enemies[i].active) {
if(enemies[i].type == 0) display.drawCircle(enemies[i].x, enemies[i].y, 4, WHITE); // Asteroid
else display.fillTriangle(enemies[i].x, enemies[i].y+4, enemies[i].x-4, enemies[i].y-4, enemies[i].x+4, enemies[i].y-4, WHITE); // Enemy Ship
}
}
if (shooterOver) {
display.setTextSize(2);
display.setCursor(30, 24);
display.println("GAME");
display.setCursor(30, 40);
display.println("OVER");
}
}
// -------------------------------------------------------------------------
// GAME LOGIC (BREAKOUT)
// -------------------------------------------------------------------------
void startBreakout() {
breakoutOver = false;
breakoutScore = 0;
lives = 3;
padX = SCREEN_WIDTH / 2;
ballActive = false;
// Reset Bricks
for(int r=0; r<4; r++) {
for(int c=0; c<10; c++) {
bricks[r][c] = true;
}
}
}
void updateBreakout(bool openPress) {
if (breakoutOver) return;
// Paddle Control (Gyro)
int16_t ax, ay, az;
mpu.getAcceleration(&ax, &ay, &az);
float tilt = map(ax, -17000, 17000, -6, 6);
padX -= tilt;
if (padX < padW/2) padX = padW/2;
if (padX > SCREEN_WIDTH - padW/2) padX = SCREEN_WIDTH - padW/2;
// Launch Ball
if (!ballActive) {
ballX = padX;
ballY = SCREEN_HEIGHT - 10;
if (openPress) {
ballActive = true;
ballVX = random(-2, 3);
if(ballVX == 0) ballVX = 1;
ballVY = -2.5;
}
} else {
ballX += ballVX;
ballY += ballVY;
// Wall Collisions
if (ballX <= 0 || ballX >= SCREEN_WIDTH) ballVX = -ballVX;
if (ballY <= 0) ballVY = -ballVY;
// Paddle Collision
if (ballY >= SCREEN_HEIGHT - 8 && ballY < SCREEN_HEIGHT - 2 && abs(ballX - padX) < padW/2 + 2) {
ballVY = -abs(ballVY); // Bounce up
// Add english based on where it hit paddle
ballVX += (ballX - padX) * 0.1;
enqueueSfx(20);
}
// Brick Collision
int r = (ballY - 10) / (brickH + brickGap); // Offset by 10px top margin
int c = ballX / (brickW + brickGap);
if (r >= 0 && r < 4 && c >= 0 && c < 10 && bricks[r][c]) {
bricks[r][c] = false;
ballVY = -ballVY;
breakoutScore += 10;
enqueueSfx(11);
}
// Die
if (ballY > SCREEN_HEIGHT) {
lives--;
ballActive = false;
enqueueSfx(12);
if (lives <= 0) breakoutOver = true;
}
}
}
void drawBreakout() {
display.clearDisplay();
// HUD
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0);
display.print("Lives:"); display.print(lives);
display.setCursor(64,0);
display.print("Score:"); display.print(breakoutScore);
// Paddle
display.fillRect(padX - padW/2, SCREEN_HEIGHT - 4, padW, 3, WHITE);
// Ball
if (ballActive || lives > 0) display.fillCircle(ballX, ballY, 2, WHITE);
// Bricks
for(int r=0; r<4; r++) {
for(int c=0; c<10; c++) {
if (bricks[r][c]) {
int bx = c * (brickW + brickGap) + 1;
int by = r * (brickH + brickGap) + 10;
display.fillRect(bx, by, brickW, brickH, WHITE);
}
}
}
if (breakoutOver) {
display.setTextSize(2);
display.setCursor(30, 30);
display.println("GAME OVER");
}
}
// -------------------------------------------------------------------------
// TOOLS (LEVEL, THEREMIN, DICE)
// -------------------------------------------------------------------------
// --- SPIRIT LEVEL ---
void drawLevel() {
int16_t ax, ay, az;
mpu.getAcceleration(&ax, &ay, &az);
// Map tilt to screen coordinates
// 16384 = 1g (approx)
int bx = map(ax, -16384, 16384, SCREEN_WIDTH-8, 8);
int by = map(ay, -16384, 16384, 8, SCREEN_HEIGHT-8);
// Clamp to screen
if(bx < 8) bx = 8; if(bx > SCREEN_WIDTH-8) bx = SCREEN_WIDTH-8;
if(by < 8) by = 8; if(by > SCREEN_HEIGHT-8) by = SCREEN_HEIGHT-8;
display.clearDisplay();
// Draw Crosshair
display.drawFastHLine(0, SCREEN_HEIGHT/2, SCREEN_WIDTH, WHITE);
display.drawFastVLine(SCREEN_WIDTH/2, 0, SCREEN_HEIGHT, WHITE);
display.drawCircle(SCREEN_WIDTH/2, SCREEN_HEIGHT/2, 10, WHITE);
// Draw Bubble
display.fillCircle(bx, by, 6, WHITE);
// Display Values
display.setTextSize(1);
display.setTextColor(WHITE);
display.setCursor(0,0);
display.print("X:"); display.print(ax);
display.setCursor(0,8);
display.print("Y:"); display.print(ay);
}
// --- THEREMIN ---
void updateTheremin() {
int16_t ax, ay, az;
mpu.getAcceleration(&ax, &ay, &az);
// Map X tilt to Pitch (200Hz - 2000Hz)
int pitch = map(ax, -17000, 17000, 200, 2000);
if (pitch < 100) pitch = 100;
// Map Y tilt to nothing for now, or maybe modulation
// Only play if sound enabled
if (soundEnabled) {
tone(BUZZER_PIN, pitch);
} else {
noTone(BUZZER_PIN);
}
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(WHITE);
display.setCursor(20, 20);
display.print(pitch); display.print(" Hz");
// Visual Bar
int barH = map(pitch, 200, 2000, 0, SCREEN_HEIGHT);
display.fillRect(SCREEN_WIDTH-10, SCREEN_HEIGHT-barH, 8, barH, WHITE);
}
// --- DICE ROLLER ---
int diceValue = 1;
bool rolling = false;
unsigned long rollStart = 0;
void startDice() {
diceValue = 1;
rolling = false;
}
void updateDice(bool openPress) {
int16_t ax, ay, az;
mpu.getAcceleration(&ax, &ay, &az);
// Shake to roll
static int16_t lastAxD=0, lastAyD=0;
if (!rolling && (abs(ax-lastAxD)>4000 || abs(ay-lastAyD)>4000 || openPress)) {
rolling = true;
rollStart = millis();
enqueueSfx(20);
}
lastAxD = ax; lastAyD = ay;
if (rolling) {
if (millis() - rollStart < 1000) {
if (millis() % 100 < 50) diceValue = random(1, 7);
} else {
rolling = false;
enqueueSfx(11);
}
}
}
void drawDice() {
display.clearDisplay();
int dx = SCREEN_WIDTH/2 - 16;
int dy = SCREEN_HEIGHT/2 - 16;
int ds = 32;
display.drawRoundRect(dx, dy, ds, ds, 4, WHITE);
// Dots
if (rolling) {
display.setTextSize(2);
display.setCursor(dx+10, dy+10);
display.print("?");
} else {
// Draw pips based on diceValue
int r = 3;
if(diceValue%2==1) display.fillCircle(dx+ds/2, dy+ds/2, r, WHITE); // Center
if(diceValue>1) { display.fillCircle(dx+8, dy+8, r, WHITE); display.fillCircle(dx+ds-8, dy+ds-8, r, WHITE); }
if(diceValue>3) { display.fillCircle(dx+ds-8, dy+8, r, WHITE); display.fillCircle(dx+8, dy+ds-8, r, WHITE); }
if(diceValue==6) { display.fillCircle(dx+8, dy+ds/2, r, WHITE); display.fillCircle(dx+ds-8, dy+ds/2, r, WHITE); }
}
display.setTextSize(1);
display.setCursor(0,0);
display.print("Shake to Roll");
}
// -------------------------------------------------------------------------
// ANIMATION & SFX
// -------------------------------------------------------------------------
void drawHeart(int x, int y){
// Normal Heart
display.fillCircle(x-2, y-2, 2, SSD1306_WHITE);
display.fillCircle(x+2, y-2, 2, SSD1306_WHITE);
display.fillTriangle(x-4, y-2, x+4, y-2, x, y+3, SSD1306_WHITE);
}
void drawBigHeart(int x, int y) {
// Bigger Heart for full screen effect
display.fillCircle(x-4, y-4, 4, SSD1306_WHITE);
display.fillCircle(x+4, y-4, 4, SSD1306_WHITE);
display.fillTriangle(x-8, y-4, x+8, y-4, x, y+8, SSD1306_WHITE);
}
void drawSparkle(int x, int y){
display.drawPixel(x, y-2, SSD1306_WHITE);
display.drawPixel(x, y+2, SSD1306_WHITE);
display.drawPixel(x-2, y, SSD1306_WHITE);
display.drawPixel(x+2, y, SSD1306_WHITE);
display.drawPixel(x-1, y-1, SSD1306_WHITE);
display.drawPixel(x+1, y-1, SSD1306_WHITE);
display.drawPixel(x-1, y+1, SSD1306_WHITE);
display.drawPixel(x+1, y+1, SSD1306_WHITE);
}
void drawNote(int x, int y){
display.drawCircle(x, y, 3, SSD1306_WHITE);
display.drawLine(x+3, y-6, x+3, y+2, SSD1306_WHITE);
display.drawLine(x+3, y-6, x+8, y-8, SSD1306_WHITE);
}
void drawBlush(){
int lx = leftEyeX + eyeWidth/2 - 8;
int ly = eyeY + eyeHeight + 2;
int rx = rightEyeX + eyeWidth/2 + 8;
int ry = ly;
display.drawCircle(lx, ly, 3, SSD1306_WHITE);
display.drawCircle(rx, ry, 3, SSD1306_WHITE);
}
void drawMouth(){
int mx = (leftEyeX + rightEyeX)/2;
int my = eyeY + eyeHeight + 6;
if (mouthType == 0) {
display.drawLine(mx-16, my, mx+16, my, SSD1306_WHITE);
} else if (mouthType == 1) {
for (int i=-16;i<=16;i++) { int y = my + (i*i)/64; display.drawPixel(mx+i, y, SSD1306_WHITE); }
} else if (mouthType == 2) {
display.drawLine(mx-10, my+2, mx+14, my, SSD1306_WHITE);
} else if (mouthType == 3) {
display.drawCircle(mx, my, 8, SSD1306_WHITE);
} else if (mouthType == 4) {
for (int i=-16;i<=16;i++) { int y = my - (i*i)/64; display.drawPixel(mx+i, y, SSD1306_WHITE); }
} else if (mouthType == 5) {
display.fillRect(mx-10, my-4, 20, 8, SSD1306_WHITE);
display.fillRect(mx-4, my, 8, 6, BLACK);
} else if (mouthType == 6) {
display.drawCircle(mx, my, 5, SSD1306_WHITE);
}
if (faceAnim == FACE_WHISTLE){
int nx = mx + 18;
int ny = my - 10 + (faceAnimFrame % 6 < 3 ? 0 : -1);
drawNote(nx, ny);
}
if (faceAnim == FACE_KISS){
int kx = mx + 16 + (faceAnimFrame % 10);
int ky = my - 8 - (faceAnimFrame % 10);
drawHeart(kx, ky);
}
}
// -------------------------------------------------------------------------
// MOCHI EYE SHAPES
// -------------------------------------------------------------------------
// 0=Normal, 1=Happy(^), 2=Sad(U), 3=Angry(>), 4=Cross(X), 5=Line(-), 6=Hollow(O), 7=Heart
void drawMochiEye(int x, int y, int w, int h, int type) {
int cx = x + w/2;
int cy = y + h/2;
if (type == 0) {
// Normal (handled elsewhere usually, but just in case)
display.fillRoundRect(x, y, w, h, 5, WHITE);
}
else if (type == 1) { // Happy ^
display.drawLine(cx-w/2, cy, cx, cy-h/2, WHITE);
display.drawLine(cx, cy-h/2, cx+w/2, cy, WHITE);
display.drawLine(cx-w/2, cy+1, cx, cy-h/2+1, WHITE); // Thicker
display.drawLine(cx, cy-h/2+1, cx+w/2, cy+1, WHITE);
}
else if (type == 2) { // Sad U
display.drawLine(cx-w/2, cy-h/4, cx-w/2, cy+h/4, WHITE);
display.drawLine(cx+w/2, cy-h/4, cx+w/2, cy+h/4, WHITE);
display.drawLine(cx-w/2, cy+h/4, cx+w/2, cy+h/4, WHITE);
}
else if (type == 3) { // Angry >
display.drawLine(cx-w/2, cy-h/2, cx+w/2, cy, WHITE);
display.drawLine(cx+w/2, cy, cx-w/2, cy+h/2, WHITE);
display.drawLine(cx-w/2, cy-h/2+1, cx+w/2, cy+1, WHITE); // Thicker
display.drawLine(cx+w/2, cy+1, cx-w/2, cy+h/2+1, WHITE);
}
else if (type == 4) { // Cross X
display.drawLine(cx-w/2, cy-h/2, cx+w/2, cy+h/2, WHITE);
display.drawLine(cx+w/2, cy-h/2, cx-w/2, cy+h/2, WHITE);
}
else if (type == 5) { // Line -
display.fillRect(cx-w/2, cy-2, w, 4, WHITE);
}
else if (type == 6) { // Hollow O
display.drawCircle(cx, cy, min(w,h)/2, WHITE);
display.drawCircle(cx, cy, min(w,h)/2-1, WHITE);
}
else if (type == 7) { // Heart
drawHeart(cx, cy);
}
else if (type == 8) { // Angry Left <
display.drawLine(cx+w/2, cy-h/2, cx-w/2, cy, WHITE);
display.drawLine(cx-w/2, cy, cx+w/2, cy+h/2, WHITE);
display.drawLine(cx+w/2, cy-h/2+1, cx-w/2, cy+1, WHITE);
display.drawLine(cx-w/2, cy+1, cx+w/2, cy+h/2+1, WHITE);
}
}
void drawBrows(){
int lx = leftEyeX + eyeWidth/2;
int rx = rightEyeX + eyeWidth/2;
int wigL = (faceAnim == FACE_EYEBROW_WIGGLE ? ((faceAnimFrame%10<5)?-1:1) : 0);
int wigR = (faceAnim == FACE_EYEBROW_WIGGLE ? ((faceAnimFrame%10<5)?1:-1) : 0);
int ly = eyeY - 6 - browLiftL + wigL;
int ry = eyeY - 6 - browLiftR + wigR;
display.drawLine(lx-12, ly, lx+12, ly, SSD1306_WHITE);
display.drawLine(rx-12, ry, rx+12, ry, SSD1306_WHITE);
}
void startSfxWhistle(unsigned long t){ enqueueSfx(30); }
void updateSfx(unsigned long t){
if (sfxType == 0) {
if (mode == MODE_ANIM && soundEnabled && t >= sfxNext) {
enqueueSfx(30);
sfxNext = t + random(7000, 11000);
}
return;
}
if (!soundEnabled) { buzzerOff(); sfxType = 0; return; }
if (sfxType == 10){
if (t >= sfxTs){
if (sfxStep==0){ buzzerTone(520); sfxTs=t+70; sfxStep=1; }
else { buzzerOff(); sfxType=0; }
}
} else if (sfxType == 11){
if (t >= sfxTs){
if (sfxStep==0){ buzzerTone(600); sfxTs=t+60; sfxStep=1; }
else { buzzerOff(); sfxType=0; }
}
} else if (sfxType == 12){
if (t >= sfxTs){
int seq[3]={480,420,360};
if (sfxStep<3){ buzzerTone(seq[sfxStep]); sfxTs=t+120; sfxStep++; }
else { buzzerOff(); sfxType=0; }
}
} else if (sfxType == 20){
if (t >= sfxTs){
if (sfxStep==0){ buzzerTone(480); sfxTs=t+50; sfxStep=1; }
else { buzzerOff(); sfxType=0; }
}
} else if (sfxType == 21){
if (t >= sfxTs){
int seq[3]={440,523,587};
if (sfxStep<3){ buzzerTone(seq[sfxStep]); sfxTs=t+120; sfxStep++; }
else { buzzerOff(); sfxType=0; }
}
} else if (sfxType == 22){
if (t >= sfxTs){
int seq[2]={494,440};
if (sfxStep<2){ buzzerTone(seq[sfxStep]); sfxTs=t+130; sfxStep++; }
else { buzzerOff(); sfxType=0; }
}
} else if (sfxType == 23){
if (t >= sfxTs){
if (sfxStep==0){ buzzerTone(520); sfxTs=t+80; sfxStep=1; }
else { buzzerOff(); sfxType=0; }
}
} else if (sfxType == 30){
if (t >= sfxTs){
int seq[2]={392,523};
if (sfxStep<2){ buzzerTone(seq[sfxStep]); sfxTs=t+100; sfxStep++; }
else { buzzerOff(); sfxType=0; }
}
} else if (sfxType == 31){
if (t >= sfxTs){
int seq[3]={392,523,659};
if (sfxStep<3){ buzzerTone(seq[sfxStep]); sfxTs=t+110; sfxStep++; }
else { buzzerOff(); sfxType=0; }
}
} else if (sfxType == 32){
if (t >= sfxTs){
int seq[2]={440,494};
if (sfxStep<2){ buzzerTone(seq[sfxStep]); sfxTs=t+120; sfxStep++; }
else { buzzerOff(); sfxType=0; }
}
} else if (sfxType == 33){
if (t >= sfxTs){
int seq[3]={494,440,392};
if (sfxStep<3){ buzzerTone(seq[sfxStep]); sfxTs=t+130; sfxStep++; }
else { buzzerOff(); sfxType=0; }
}
} else if (sfxType == 34){
if (t >= sfxTs){
int seq[2]={523,523};
if (sfxStep<2){ buzzerTone(seq[sfxStep]); sfxTs=t+90; sfxStep++; }
else { buzzerOff(); sfxType=0; }
}
} else if (sfxType == 40) { // Menu Navigation
if (t >= sfxTs){
if (sfxStep == 0){ buzzerTone(800); sfxTs = t + 30; sfxStep = 1; }
else { buzzerOff(); sfxType = 0; }
}
} else if (sfxType == 41) { // Menu Select
if (t >= sfxTs){
if (sfxStep == 0){ buzzerTone(1200); sfxTs = t + 50; sfxStep = 1; }
else if (sfxStep == 1){ buzzerTone(1600); sfxTs = t + 80; sfxStep = 2; }
else { buzzerOff(); sfxType = 0; }
}
} else if (sfxType == 50) { // Computer Chirp 1 (High fast bleeps)
if (t >= sfxTs) {
if (sfxStep < 6) {
buzzerTone(random(1000, 2500));
sfxTs = t + 40;
sfxStep++;
} else { buzzerOff(); sfxType = 0; }
}
} else if (sfxType == 51) { // Computer Chirp 2 (Low grumble)
if (t >= sfxTs) {
if (sfxStep < 5) {
buzzerTone(random(200, 500));
sfxTs = t + 60;
sfxStep++;
} else { buzzerOff(); sfxType = 0; }
}
} else if (sfxType == 52) { // Kiss Sound (Slide up)
if (t >= sfxTs) {
if (sfxStep == 0) { buzzerTone(400); sfxTs = t + 50; sfxStep=1; }
else if (sfxStep == 1) { buzzerTone(800); sfxTs = t + 50; sfxStep=2; }
else if (sfxStep == 2) { buzzerTone(1200); sfxTs = t + 100; sfxStep=3; }
else { buzzerOff(); sfxType = 0; }
}
} else if (sfxType == 53) { // Suspicious (Hmmmm)
if (t >= sfxTs) {
if (sfxStep == 0) { buzzerTone(300); sfxTs = t + 200; sfxStep=1; }
else if (sfxStep == 1) { buzzerTone(400); sfxTs = t + 300; sfxStep=2; }
else { buzzerOff(); sfxType = 0; }
}
} else if (sfxType == 60) { // Cat Purr (Low vibrating)
if (t >= sfxTs) {
// Alternating low tones rapidly
int f = (sfxStep % 2 == 0) ? 150 : 170;
buzzerTone(f);
sfxTs = t + 60;
sfxStep++;
if (sfxStep > 20) { buzzerOff(); sfxType = 0; } // Purr for ~1.2s
}
} else if (sfxType == 61) { // Cat Meow (Slide up-down)
if (t >= sfxTs) {
if (sfxStep == 0) { buzzerTone(800); sfxTs = t + 100; sfxStep=1; }
else if (sfxStep == 1) { buzzerTone(1200); sfxTs = t + 150; sfxStep=2; }
else if (sfxStep == 2) { buzzerTone(1000); sfxTs = t + 150; sfxStep=3; }
else { buzzerOff(); sfxType = 0; }
}
} else if (sfxType == 99) { // Startup Melody
if (t >= sfxTs){
int seq[4] = {523, 659, 784, 1046}; // C E G C
if (sfxStep < 4){ buzzerTone(seq[sfxStep]); sfxTs = t + 120; sfxStep++; }
else { buzzerOff(); sfxType = 0; }
}
}
}
void updateFaceAnim(unsigned long t, bool forcePet){
if (forcePet) {
// "Petting" the robot - always positive/cat-like
int r = random(0, 5);
if (r == 0) { faceAnim = FACE_HEARTS; faceAnimDur = 1500; enqueueSfx(52); } // Kiss
else if (r == 1) { faceAnim = FACE_CAT_HAPPY; faceAnimDur = 1500; enqueueSfx(61); } // Meow
else if (r == 2) { faceAnim = FACE_CAT_PURR; faceAnimDur = 2000; enqueueSfx(60); } // Purr
else if (r == 3) { faceAnim = FACE_CAT_BLINK; faceAnimDur = 1200; enqueueSfx(61); } // Slow blink
else { faceAnim = FACE_GIZMO_HAPPY; faceAnimDur = 1200; enqueueSfx(31); }
faceAnimStart = t;
faceAnimNext = t + random(4000, 8000);
faceAnimFrame = 0;
return;
}
if (faceAnim == FACE_NONE && t >= faceAnimNext){
int r = random(0,25); // Increased range
if (r == 0) { faceAnim = FACE_WINK_L; faceAnimDur = 400; }
else if (r == 1) { faceAnim = FACE_WINK_R; faceAnimDur = 400; }
else if (r == 2) { faceAnim = FACE_HEARTS; faceAnimDur = 900; }
else if (r == 3) { faceAnim = FACE_SPARKLE; faceAnimDur = 900; }
else if (r == 4) { faceAnim = FACE_CROSS; faceAnimDur = 800; }
else if (r == 5) { faceAnim = FACE_SLEEPY; faceAnimDur = 1400; }
else if (r == 6) { faceAnim = FACE_TONGUE; faceAnimDur = 900; }
else if (r == 7) { faceAnim = FACE_SURPRISE; faceAnimDur = 700; }
else if (r == 8) { faceAnim = FACE_WHISTLE; faceAnimDur = 900; enqueueSfx(30); }
else if (r == 9) { faceAnim = FACE_BLUSH; faceAnimDur = 900; }
else if (r == 10){ faceAnim = FACE_EYEBROW_WIGGLE; faceAnimDur = 900; }
else if (r == 11){ faceAnim = FACE_SERENE; faceAnimDur = 1200; }
else if (r == 12){ faceAnim = FACE_GIZMO_HAPPY; faceAnimDur = 1000; enqueueSfx(31); }
else if (r == 13){ faceAnim = FACE_GIZMO_CURIOUS; faceAnimDur = 900; enqueueSfx(32); }
else if (r == 14){ faceAnim = FACE_GIZMO_CONFUSED; faceAnimDur = 1000; enqueueSfx(34); }
else if (r == 15){ faceAnim = FACE_GIZMO_SAD; faceAnimDur = 1100; enqueueSfx(33); }
else if (r == 16){ faceAnim = FACE_LAUGH; faceAnimDur = 1000; enqueueSfx(50); } // Replaced 31 with 50 (chirp)
else if (r == 17){ faceAnim = FACE_ANGRY; faceAnimDur = 1200; enqueueSfx(51); }
else if (r == 18){ faceAnim = FACE_SKEPTICAL; faceAnimDur = 1100; enqueueSfx(53); }
else if (r == 19){ faceAnim = FACE_CRY; faceAnimDur = 1500; enqueueSfx(33); }
else if (r == 20){ faceAnim = FACE_TEARS; faceAnimDur = 1500; enqueueSfx(33); }
else if (r == 21){ faceAnim = FACE_KISS; faceAnimDur = 1200; enqueueSfx(52); }
else if (r == 22){ faceAnim = FACE_SUSPICIOUS; faceAnimDur = 1500; enqueueSfx(53); }
else if (r == 23){ faceAnim = FACE_CAT_HAPPY; faceAnimDur = 1500; enqueueSfx(61); }
else if (r == 24){ faceAnim = FACE_CAT_PURR; faceAnimDur = 1800; enqueueSfx(60); }
else { faceAnim = FACE_KISS; faceAnimDur = 1000; enqueueSfx(52); } // Fallback
faceAnimStart = t;
faceAnimNext = t + random(4000, 8000);
faceAnimFrame = 0;
} else if (faceAnim != FACE_NONE){
if (t - faceAnimStart >= faceAnimDur) { faceAnim = FACE_NONE; faceAnimFrame = 0; }
else { faceAnimFrame++; }
}
}
void updateFaceDynamics(unsigned long t){
if (t >= nextMicro){
microX = random(-1,2);
microY = random(-1,2);
microEnd = t + 80;
nextMicro = t + random(1200, 2200);
}
if (t >= microEnd){ microX = 0; microY = 0; }
if ((int)t % 2200 < 40) targetPupilR = random(4,7);
if (pupilR < targetPupilR) pupilR++;
else if (pupilR > targetPupilR) pupilR--;
if (faceAnim == FACE_SLEEPY || faceAnim == FACE_SERENE || faceAnim == FACE_GIZMO_SAD){
eyelidL = min(eyeHeight/2, eyelidL + 1);
eyelidR = min(eyeHeight/2, eyelidR + 1);
} else {
eyelidL = max(0, eyelidL - 1);
eyelidR = max(0, eyelidR - 1);
}
if (faceAnim == FACE_SURPRISE){ browLiftL = 2; browLiftR = 2; }
else if (faceAnim == FACE_SLEEPY || faceAnim == FACE_SERENE || faceAnim == FACE_GIZMO_SAD){ browLiftL = -1; browLiftR = -1; }
else if (faceAnim == FACE_GIZMO_CURIOUS || faceAnim == FACE_SUSPICIOUS){ browLiftL = 2; browLiftR = -1; }
else if (faceAnim == FACE_GIZMO_HAPPY || faceAnim == FACE_CAT_HAPPY){ browLiftL = 1; browLiftR = 1; }
else if (faceAnim == FACE_ANGRY){ browLiftL = -2; browLiftR = -2; }
else if (faceAnim == FACE_SKEPTICAL){ browLiftL = 2; browLiftR = 0; }
else if (faceAnim == FACE_CRY || faceAnim == FACE_TEARS){ browLiftL = 1; browLiftR = 1; }
else { browLiftL = 0; browLiftR = 0; }
if (faceAnim == FACE_TONGUE) mouthType = 5;
else if (faceAnim == FACE_SURPRISE) mouthType = 3;
else if (faceAnim == FACE_HEARTS || faceAnim == FACE_GIZMO_HAPPY || faceAnim == FACE_LAUGH || faceAnim == FACE_CAT_HAPPY) mouthType = 1;
else if (faceAnim == FACE_WHISTLE || faceAnim == FACE_GIZMO_CURIOUS || faceAnim == FACE_KISS) mouthType = 6;
else if (faceAnim == FACE_GIZMO_SAD || faceAnim == FACE_CRY || faceAnim == FACE_TEARS) mouthType = 4;
else if (faceAnim == FACE_ANGRY || faceAnim == FACE_SUSPICIOUS) mouthType = 2;
else if (faceAnim == FACE_SKEPTICAL) mouthType = 0;
else if (faceAnim == FACE_CAT_PURR) mouthType = 1;
else if ((int)t % 7000 < 80) mouthType = random(0,5);
if (faceAnim == FACE_GIZMO_HAPPY || faceAnim == FACE_CAT_HAPPY){
int amp = 2;
if ((faceAnimFrame/6)%2==0){ targetLeftPupilY = eyeY + eyeHeight/2 - amp; targetRightPupilY = targetLeftPupilY; }
else { targetLeftPupilY = eyeY + eyeHeight/2 + amp; targetRightPupilY = targetLeftPupilY; }
} else if (faceAnim == FACE_GIZMO_CURIOUS){
int amp = 2;
if ((faceAnimFrame/6)%2==0){ targetLeftPupilY = eyeY + eyeHeight/2 - amp; targetRightPupilY = targetLeftPupilY; }
else { targetLeftPupilY = eyeY + eyeHeight/2 + amp; targetRightPupilY = targetLeftPupilY; }
} else if (faceAnim == FACE_GIZMO_CURIOUS){
targetLeftEyeX = leftEyeX - 2;
targetRightEyeX = rightEyeX + 2;
targetEyeY = eyeY;
targetLeftPupilX = leftEyeX + eyeWidth/2 - 3;
targetRightPupilX = rightEyeX + eyeWidth/2 - 3;
targetLeftPupilY = eyeY + eyeHeight/2 - 2;
targetRightPupilY = eyeY + eyeHeight/2 - 2;
targetPupilR = 4;
} else if (faceAnim == FACE_GIZMO_CONFUSED){
int k = faceAnimFrame % 12;
int ox[12]={0,1,2,2,1,0,-1,-2,-2,-1,0,1};
int oy[12]={-2,-2,-1,0,1,2,2,2,1,0,-1,-2};
targetLeftPupilX = leftEyeX + eyeWidth/2 + ox[k];
targetRightPupilX = rightEyeX + eyeWidth/2 + ox[(k+6)%12];
targetLeftPupilY = eyeY + eyeHeight/2 + oy[k];
targetRightPupilY = eyeY + eyeHeight/2 + oy[(k+6)%12];
targetPupilR = 4;
} else if (faceAnim == FACE_GIZMO_SAD){
targetPupilR = 4;
}
}
void updateBlink(unsigned long currentTime){
if(currentTime - lastBlinkTime > blinkDelay && blinkState==0){ blinkState=1; lastBlinkTime=currentTime; }
else if(currentTime - lastBlinkTime > 400 && blinkState==1){ blinkState=0; lastBlinkTime=currentTime; }
}
void updateMPU(){
int16_t ax, ay, az;
mpu.getAcceleration(&ax, &ay, &az);
// DIZZY DETECTION LOGIC
// If we detect rapid changes in acceleration (shaking)
// Lowered threshold to 2500 (approx 0.15g change) and count to 3
if (faceAnim == FACE_NONE || faceAnim == FACE_DIZZY) {
if (abs(ax - lastAx) > 2500 || abs(ay - lastAy) > 2500) {
if (millis() - lastShakeTime < 500) {
shakeCount++;
} else {
shakeCount = 1;
}
lastShakeTime = millis();
}
lastAx = ax;
lastAy = ay;
// Trigger Dizzy Face if shaken enough
if (shakeCount > 3 && faceAnim != FACE_DIZZY) {
faceAnim = FACE_DIZZY;
faceAnimDur = 4000; // Stay dizzy for 4 seconds
faceAnimStart = millis();
shakeCount = 0;
enqueueSfx(33); // Play sound
}
}
// Normal Eye Movement (only if not dizzy)
if (faceAnim != FACE_DIZZY) {
targetLeftEyeX = map(ax, -17000, 17000, 20, 60);
targetRightEyeX = map(ax, -17000, 17000, 60, 100);
targetEyeY = map(ay, -17000, 17000, 10, 25);
int zTilt = az - 16384;
int pupilOffsetX = map(ax, -17000, 17000, -eyeWidth/4, eyeWidth/4);
int pupilOffsetY = map(zTilt, -8000, 8000, -eyeHeight/4, eyeHeight/4);
targetLeftPupilX = leftEyeX + eyeWidth/2 + pupilOffsetX;
targetLeftPupilY = eyeY + eyeHeight/2 + pupilOffsetY;
targetRightPupilX = rightEyeX + eyeWidth/2 + pupilOffsetX;
targetRightPupilY = eyeY + eyeHeight/2 + pupilOffsetY;
} else {
// Dizzy Eye Movement (Spinning)
// Center the eyes
targetLeftEyeX = 40;
targetRightEyeX = 80;
targetEyeY = 18;
// We don't need pupil coords for spiral, but keep variables sane
targetLeftPupilX = 40 + eyeWidth/2;
targetLeftPupilY = 18 + eyeHeight/2;
targetRightPupilX = 80 + eyeWidth/2;
targetRightPupilY = 18 + eyeHeight/2;
}
}
void smoothInterpolation(){
leftEyeX += (targetLeftEyeX - leftEyeX)/moveSpeed;
rightEyeX += (targetRightEyeX - rightEyeX)/moveSpeed;
eyeY += (targetEyeY - eyeY)/moveSpeed;
leftPupilX += (targetLeftPupilX - leftPupilX)/moveSpeed;
leftPupilY += (targetLeftPupilY - leftPupilY)/moveSpeed;
rightPupilX += (targetRightPupilX - rightPupilX)/moveSpeed;
rightPupilY += (targetRightPupilY - rightPupilY)/moveSpeed;
}
void drawExpression(int eyeX, int eyeY, int eyeWidth, int eyeHeight, int exp){
display.fillRoundRect(eyeX, eyeY, eyeWidth, eyeHeight, 5, WHITE);
if (exp==1) display.fillRect(eyeX+5, eyeY+18, eyeWidth-10,4,WHITE);
else if (exp==2) display.fillRect(eyeX+5, eyeY+eyeHeight-12, eyeWidth-10,4,WHITE);
else if (exp==3) display.fillRect(eyeX+5, eyeY+7, eyeWidth-10,4,WHITE);
}
void drawEyes(){
display.clearDisplay();
// DETERMINE MOCHI EYE TYPE
int leftType = 0; // 0=Normal
int rightType = 0;
if (faceAnim == FACE_LAUGH || faceAnim == FACE_GIZMO_HAPPY || faceAnim == FACE_HEARTS || faceAnim == FACE_CAT_HAPPY) { leftType=1; rightType=1; }
else if (faceAnim == FACE_SLEEPY || faceAnim == FACE_SERENE || faceAnim == FACE_CAT_PURR) { leftType=5; rightType=5; }
else if (faceAnim == FACE_ANGRY) { leftType=3; rightType=8; } // > <
else if (faceAnim == FACE_CROSS || faceAnim == FACE_GIZMO_CONFUSED) { leftType=4; rightType=4; }
else if (faceAnim == FACE_SURPRISE || faceAnim == FACE_GIZMO_CURIOUS) { leftType=6; rightType=6; }
else if (faceAnim == FACE_CRY || faceAnim == FACE_TEARS) { leftType=5; rightType=5; } // Or 2
else if (faceAnim == FACE_SKEPTICAL || faceAnim == FACE_SUSPICIOUS) { leftType=5; rightType=0; } // - O
else if (faceAnim == FACE_WINK_L) { leftType=1; rightType=0; } // ^ O
else if (faceAnim == FACE_WINK_R) { leftType=0; rightType=1; } // O ^
else if (faceAnim == FACE_KISS) { leftType=1; rightType=1; } // ^ ^
else if (faceAnim == FACE_CAT_BLINK) { leftType=5; rightType=5; } // - -
else if (faceAnim == FACE_DIZZY) { leftType=99; rightType=99; } // Spiral special
bool doBlink = (blinkState==1 && faceAnim==FACE_NONE);
// LEFT EYE
if (leftType == 0) { // Normal Eye
if(!doBlink){
drawExpression(leftEyeX, eyeY, eyeWidth, eyeHeight, expression);
int lpX = leftPupilX + microX;
int lpY = leftPupilY + microY;
display.fillCircle(lpX, lpY, pupilR, BLACK);
} else {
display.fillRect(leftEyeX, eyeY+eyeHeight/2-2, eyeWidth, 4, WHITE);
}
} else if (leftType == 99) { // Spiral
drawSpiral(leftEyeX + eyeWidth/2, eyeY + eyeHeight/2, true);
} else { // Mochi Shape
drawMochiEye(leftEyeX, eyeY, eyeWidth, eyeHeight, leftType);
}
// RIGHT EYE
if (rightType == 0) { // Normal Eye
if(!doBlink){
drawExpression(rightEyeX, eyeY, eyeWidth, eyeHeight, expression);
int rpX = rightPupilX + microX;
int rpY = rightPupilY + microY;
display.fillCircle(rpX, rpY, pupilR, BLACK);
} else {
display.fillRect(rightEyeX, eyeY+eyeHeight/2-2, eyeWidth, 4, WHITE);
}
} else if (rightType == 99) { // Spiral
drawSpiral(rightEyeX + eyeWidth/2, eyeY + eyeHeight/2, false);
} else { // Mochi Shape
drawMochiEye(rightEyeX, eyeY, eyeWidth, eyeHeight, rightType);
}
// OVERLAYS
if (eyelidL>0 && leftType==0) display.fillRect(leftEyeX, eyeY, eyeWidth, eyelidL, BLACK);
if (eyelidR>0 && rightType==0) display.fillRect(rightEyeX, eyeY, eyeWidth, eyelidR, BLACK);
if (faceAnim == FACE_HEARTS){
// Floating Hearts everywhere
drawBigHeart(10 + (faceAnimFrame*2)%100, 10 + (faceAnimFrame*3)%40);
drawBigHeart(110 - (faceAnimFrame*3)%100, 50 - (faceAnimFrame*2)%40);
drawHeart(leftEyeX + eyeWidth/2 - 8, eyeY - 2);
drawHeart(rightEyeX + eyeWidth/2 + 8, eyeY - 2);
}
if (faceAnim == FACE_SPARKLE){
// Sparkle Burst
int cx = SCREEN_WIDTH/2; int cy = SCREEN_HEIGHT/2;
int r = (faceAnimFrame * 5) % 60;
for(int i=0; i<8; i++) {
float a = i * PI / 4;
drawSparkle(cx + cos(a)*r, cy + sin(a)*r);
}
drawSparkle(leftEyeX + eyeWidth/2, eyeY + eyeHeight/2 - 6);
drawSparkle(rightEyeX + eyeWidth/2, eyeY + eyeHeight/2 - 6);
}
if (faceAnim == FACE_BLUSH || faceAnim == FACE_GIZMO_HAPPY || faceAnim == FACE_KISS) drawBlush();
if (faceAnim == FACE_CRY || faceAnim == FACE_TEARS) {
// Heavy Rain / Tears
int tearY = eyeY + eyeHeight + (faceAnimFrame % 10);
display.fillCircle(leftEyeX + eyeWidth/2, tearY, 2, WHITE);
display.fillCircle(rightEyeX + eyeWidth/2, tearY, 2, WHITE);
// Extra background rain
if (faceAnimFrame % 5 == 0) {
int rx = random(0, SCREEN_WIDTH);
int ry = random(0, SCREEN_HEIGHT);
display.drawFastVLine(rx, ry, 4, WHITE);
}
}
if (faceAnim == FACE_DIZZY) {
// Shaking effect handled by MPU, but lets add spinning stars
float angle = (millis() % 1000) / 1000.0 * 2 * PI;
int sx = SCREEN_WIDTH/2 + cos(angle) * 30;
int sy = SCREEN_HEIGHT/2 + sin(angle) * 10;
drawSparkle(sx, sy);
drawSparkle(SCREEN_WIDTH - sx, SCREEN_HEIGHT - sy);
}
drawBrows();
drawMouth();
// display.display() removed from here - called in loop
if(millis() % 5000 < 50 && faceAnim==FACE_NONE) expression = random(0,4);
}
void displayMenu(){
display.clearDisplay();
// Title Bar
display.fillRect(0, 0, 128, 14, SSD1306_WHITE);
display.setTextSize(1);
display.setTextColor(SSD1306_BLACK);
display.setCursor(35, 3);
display.print("MAIN MENU");
// Scroll Bar
int scrollHeight = 46 / menuLength;
int scrollY = 16 + (currentMenuItem * (46 - scrollHeight)) / (menuLength - 1);
display.drawRect(124, 16, 3, 46, SSD1306_WHITE);
display.fillRect(124, scrollY, 3, scrollHeight, SSD1306_WHITE);
// Menu Items
int y = 18;
for(int i=0; i<linesVisible; i++){
int idx = topIndex + i;
if (idx >= menuLength) break;
// Highlight Box
if(idx == currentMenuItem) {
display.fillRoundRect(2, y-2, 118, 15, 3, SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK);
display.setCursor(16, y+2);
} else {
display.setTextColor(SSD1306_WHITE);
display.setCursor(16, y+2);
}
// Icons (Simple 5x5 representations)
if(idx == MENU_FLAPPY) {
if(idx!=currentMenuItem) display.drawRect(6, y+2, 6, 6, SSD1306_WHITE);
else display.drawRect(6, y+2, 6, 6, SSD1306_BLACK);
display.print("Flappy Bird");
}
else if(idx == MENU_TICTAC) {
if(idx!=currentMenuItem) { display.drawLine(6,y+2,12,y+8,WHITE); display.drawLine(12,y+2,6,y+8,WHITE); }
else { display.drawLine(6,y+2,12,y+8,BLACK); display.drawLine(12,y+2,6,y+8,BLACK); }
display.print("Tic-Tac-Toe");
}
else if(idx == MENU_RACER) {
if(idx!=currentMenuItem) display.fillRect(6, y+4, 6, 4, SSD1306_WHITE);
else display.fillRect(6, y+4, 6, 4, SSD1306_BLACK);
display.print("Road Racer");
}
else if(idx == MENU_SHOOTER) {
if(idx!=currentMenuItem) display.fillTriangle(9, y+2, 6, y+8, 12, y+8, WHITE);
else display.fillTriangle(9, y+2, 6, y+8, 12, y+8, BLACK);
display.print("Space Shooter");
}
else if(idx == MENU_BREAKOUT) {
if(idx!=currentMenuItem) display.fillRect(6, y+2, 6, 3, WHITE);
else display.fillRect(6, y+2, 6, 3, BLACK);
display.print("Brick Breaker");
}
else if(idx == MENU_LEVEL) {
if(idx!=currentMenuItem) display.drawCircle(9, y+5, 3, WHITE);
else display.drawCircle(9, y+5, 3, BLACK);
display.print("Spirit Level");
}
else if(idx == MENU_THEREMIN) {
if(idx!=currentMenuItem) { display.drawPixel(6,y+8,WHITE); display.drawPixel(9,y+4,WHITE); display.drawPixel(12,y+2,WHITE); }
else { display.drawPixel(6,y+8,BLACK); display.drawPixel(9,y+4,BLACK); display.drawPixel(12,y+2,BLACK); }
display.print("Tilt Theremin");
}
else if(idx == MENU_DICE) {
if(idx!=currentMenuItem) display.drawRect(6, y+3, 5, 5, WHITE);
else display.drawRect(6, y+3, 5, 5, BLACK);
display.print("Dice Roller");
}
else if(idx == MENU_SOUND) {
display.print(soundEnabled ? "Sound: ON" : "Sound: OFF");
}
else if(idx == MENU_BACK) {
if(idx!=currentMenuItem) display.fillTriangle(6,y+5, 12,y+2, 12,y+8, WHITE);
else display.fillTriangle(6,y+5, 12,y+2, 12,y+8, BLACK);
display.print("Back to Face");
}
y += 16; // Item spacing
}
}
void setup() {
Serial.begin(115200);
// XIAO ESP32-C3 specific I2C
Wire.begin(PIN_SDA, PIN_SCL);
Wire.setClock(400000); // Set I2C to 400kHz for faster updates
if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) { for(;;); }
display.display();
delay(800);
// Initialize MPU6050
mpu.initialize();
// Check if MPU6050 connection was successful
if (!mpu.testConnection()) {
// If failed, show error on screen but continue so user knows
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0);
display.println("MPU6050 Error!");
display.println("Check Wiring");
display.display();
delay(2000);
}
pinMode(BUTTON_UP, INPUT_PULLUP);
pinMode(BUTTON_DOWN, INPUT_PULLUP);
pinMode(BUTTON_OPEN, INPUT_PULLUP);
pinMode(BUTTON_MENUEXIT, INPUT_PULLUP);
buzzerInit();
enqueueSfx(99); // Startup Melody
}
void loop() {
unsigned long t = millis();
// 1. POLLING (Always run to catch every press)
bool up = digitalRead(BUTTON_UP);
bool down = digitalRead(BUTTON_DOWN);
bool openB = digitalRead(BUTTON_OPEN);
bool menuExitB = digitalRead(BUTTON_MENUEXIT);
// Debounce Logic
bool upPress = (up == LOW && lastUp == HIGH && t - upTs > debounceMs);
bool downPress = (down == LOW && lastDown == HIGH && t - downTs > debounceMs);
bool openPress = (openB == LOW && lastOpen == HIGH && t - openTs > debounceMs);
bool menuExitPress = (menuExitB == LOW && lastMenuExit == HIGH && t - menuExitTs > debounceMs);
if (upPress) upTs = t;
if (downPress) downTs = t;
if (openPress) openTs = t;
if (menuExitPress) menuExitTs = t;
updateSfx(t);
// 2. FRAME LIMITER (Only update screen and game logic at 60 FPS)
if (t - lastFrameTime < FRAME_DELAY) {
// Just update button history and return
lastUp = up;
lastDown = down;
lastOpen = openB;
lastMenuExit = menuExitB;
return;
}
lastFrameTime = t;
if (mode == MODE_ANIM) {
if (menuExitPress) {
mode = MODE_MENU;
displayMenu();
display.display(); // Immediate update for responsiveness
lastUp=up; lastDown=down; lastOpen=openB; lastMenuExit=menuExitB;
return;
}
updateBlink(t);
updateMPU();
smoothInterpolation();
// New Button Logic: Random Face
if (openPress) {
updateFaceAnim(t, true);
} else {
updateFaceAnim(t, false);
}
updateFaceDynamics(t);
drawEyes();
} else if (mode == MODE_MENU) {
if (upPress) {
currentMenuItem--;
if(currentMenuItem < 0) currentMenuItem = menuLength - 1;
if (currentMenuItem < topIndex) topIndex = currentMenuItem;
enqueueSfx(40);
displayMenu();
}
if (downPress) {
currentMenuItem++;
if(currentMenuItem >= menuLength) currentMenuItem = 0;
if (currentMenuItem >= topIndex + linesVisible) topIndex = currentMenuItem - linesVisible + 1;
enqueueSfx(40);
displayMenu();
}
if (menuExitPress) {
mode = MODE_ANIM;
enqueueSfx(41);
display.clearDisplay();
lastUp=up; lastDown=down; lastOpen=openB; lastMenuExit=menuExitB;
return;
}
if (openPress) {
enqueueSfx(41);
int sel = currentMenuItem;
if (sel == MENU_FLAPPY) {
startFlappy();
mode = MODE_GAME_FLAPPY;
}
else if (sel == MENU_TICTAC) {
startTicTac();
mode = MODE_GAME_TTT;
}
else if (sel == MENU_RACER) {
startRacer();
mode = MODE_GAME_RACER;
}
else if (sel == MENU_SHOOTER) {
startShooter();
mode = MODE_GAME_SHOOTER;
}
else if (sel == MENU_BREAKOUT) {
startBreakout();
mode = MODE_GAME_BREAKOUT;
}
else if (sel == MENU_LEVEL) {
mode = MODE_TOOL_LEVEL;
}
else if (sel == MENU_THEREMIN) {
mode = MODE_TOOL_THEREMIN;
}
else if (sel == MENU_DICE) {
startDice();
mode = MODE_TOOL_DICE;
}
else if (sel == MENU_SOUND) {
soundEnabled = !soundEnabled;
if (!soundEnabled) { buzzerOff(); sfxType = 0; }
displayMenu();
}
else if (sel == MENU_BACK) {
mode = MODE_ANIM;
}
}
} else if (mode == MODE_GAME_FLAPPY) {
if (menuExitPress) {
mode = MODE_MENU;
displayMenu();
display.display(); // Immediate update
} else {
if (gameOver && openPress) {
startFlappy();
}
updateFlappy(upPress);
drawFlappy();
}
} else if (mode == MODE_GAME_TTT) {
if (menuExitPress) {
mode = MODE_MENU;
displayMenu();
display.display(); // Immediate update
} else {
updateTicTac(upPress, downPress, openPress);
drawTicTac();
}
} else if (mode == MODE_GAME_RACER) {
if (menuExitPress) {
mode = MODE_MENU;
displayMenu();
display.display(); // Immediate update
} else {
if (racerOver && openPress) {
startRacer();
}
updateRacer(upPress, downPress, (openB==LOW));
drawRacer();
}
} else if (mode == MODE_GAME_SHOOTER) {
if (menuExitPress) {
mode = MODE_MENU;
displayMenu();
display.display();
} else {
if (shooterOver && openPress) startShooter();
updateShooter(openPress);
drawShooter();
}
} else if (mode == MODE_GAME_BREAKOUT) {
if (menuExitPress) {
mode = MODE_MENU;
displayMenu();
display.display();
} else {
if (breakoutOver && openPress) startBreakout();
updateBreakout(openPress);
drawBreakout();
}
} else if (mode == MODE_TOOL_LEVEL) {
if (menuExitPress) {
mode = MODE_MENU;
displayMenu();
display.display();
} else {
drawLevel();
}
} else if (mode == MODE_TOOL_THEREMIN) {
if (menuExitPress) {
mode = MODE_MENU;
noTone(BUZZER_PIN); // Ensure sound stops
displayMenu();
display.display();
} else {
updateTheremin();
}
} else if (mode == MODE_TOOL_DICE) {
if (menuExitPress) {
mode = MODE_MENU;
displayMenu();
display.display();
} else {
updateDice(openPress);
drawDice();
}
}
// Single Render Call
display.display();
lastUp = up;
lastDown = down;
lastOpen = openB;
lastMenuExit = menuExitB;
}