#include <Arduino.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include <esp_system.h>
#include <math.h>
#include <Preferences.h>
Adafruit_ST7789 lcd(&SPI,14,15,21);
// Raster a 2x: trasferimento delle sole righe modificate, ritmo a 60 Hz.
constexpr int W=160,H=86,COUNT=80;
uint16_t frame[W*H],previous[W*H];
// Shared 32-colour palette: eyes, mouth, party effects and arcade games.
// Keep the original colours first so existing selections remain unchanged.
const uint16_t colors[]={
 0x07FF,0x07E0,0xFFE0,0xF81F,0xFFFF,0xFD20,0x867F,0xF9B0,0xF800,
 0x051F, // electric blue
 0x03FF, // azure
 0x05D5, // turquoise
 0x07EF, // spring green
 0x87F0, // mint
 0xBFE0, // lime
 0xDFF0, // lemon cream
 0xFEA0, // golden yellow
 0xFC00, // vivid orange
 0xFB08, // coral
 0xFAAA, // salmon
 0xFCB2, // peach
 0xF813, // raspberry
 0xF819, // hot pink
 0xFBB7, // rose
 0xD81F, // violet
 0xA21F, // purple
 0xB51F, // lavender
 0xC65F, // lilac
 0xAEFF, // ice blue
 0x7FF8, // aquamarine
 0xC7F9, // pale mint
 0xFF58  // warm cream
};
constexpr int COLOR_COUNT=sizeof(colors)/sizeof(colors[0]);
int mood=0,palette=0; uint32_t changeAt,blinkAt=2200,blinkStart=0,nextFrame=0;
bool rawDown=false,down=false,autoPlay=true;
uint32_t edgeAt=0,pressedAt=0,clickAt=0,reactionUntil=0,gazeAt=0;
uint32_t partyStarted=0,partyUntil=0;
uint32_t treatStarted=0,treatUntil=0;
uint32_t offendedUntil=0, offenseStarted=0;
int moodBeforeOffense=0;
int clicks=0,shuffleBag[COUNT],bagIndex=COUNT;
float gazeX=0,gazeY=0,targetX=0,targetY=0,smoothRX=21,smoothRY=25;
uint32_t transitionAt=0;
constexpr uint32_t FRAME_MS=16;
constexpr float GAME_STEP=16.f/33.f;
void selectMood(int next,uint32_t now){mood=next;transitionAt=now;changeAt=now+5500+esp_random()%3500;}
int randomMood(){
 if(bagIndex>=COUNT){for(int i=0;i<COUNT;i++)shuffleBag[i]=i;
 for(int i=COUNT-1;i>0;i--){int j=esp_random()%(i+1);int a=shuffleBag[i];shuffleBag[i]=shuffleBag[j];shuffleBag[j]=a;}bagIndex=0;}
 return shuffleBag[bagIndex++];
}
void pixel(int x,int y,uint16_t c){if(x>=0&&x<W&&y>=0&&y<H)frame[y*W+x]=c;}
void ellipse(int cx,int cy,int rx,int ry,uint16_t c){rx=max(1,rx);ry=max(1,ry);for(int y=-ry;y<=ry;y++){int span=(int)(rx*sqrtf(max(0.f,1.f-float(y*y)/float(ry*ry))));for(int x=-span;x<=span;x++)pixel(cx+x,cy+y,c);}}
void line(int x,int y,int xx,int yy,int r,uint16_t c){int n=max(abs(xx-x),abs(yy-y));for(int i=0;i<=n;i++)ellipse(x+(xx-x)*i/max(1,n),y+(yy-y)*i/max(1,n),r,r,c);}
void heart(int cx,int cy,uint16_t c){for(int y=-14;y<=15;y++)for(int x=-16;x<=16;x++){float a=x/16.f,b=-y/16.f;float q=a*a+b*b-1;if(q*q*q-a*a*b*b*b<=0)pixel(cx+x,cy+y,c);}}
void eye(int cx,int cy,int rx,int ry,int type,bool right,uint16_t c,float t){
 if(type>=64){
  float a=t*2.4f+(right?.55f:0);
  uint16_t accent=colors[(type+int(t*.6f))%COLOR_COUNT];
  switch(type){
   case 64: // Pianeti con anello inclinato.
    ellipse(cx,cy,14,14,c);
    for(int i=0;i<80;i++){float q=i*PI/40;pixel(cx+cosf(q)*24,cy+sinf(q)*7+cosf(q)*5,accent);}break;
   case 65: // Fuochi d'artificio a cicli sfalsati.
    for(int i=0;i<10;i++){float q=i*PI/5;float r=4+fmodf(a*8,21.f);line(cx+cosf(q)*r*.65f,cy+sinf(q)*r*.65f,cx+cosf(q)*r,cy+sinf(q)*r,1,i%2?c:accent);}break;
   case 66: // Doppia elica.
    for(int y=-22;y<=22;y+=3){int x=sin(a+y*.18f)*16;ellipse(cx+x,cy+y,2,2,c);ellipse(cx-x,cy+y,2,2,accent);if(y%2==0)line(cx-x,cy+y,cx+x,cy+y,0,0x3186);}break;
   case 67: // Onde concentriche.
    for(int y=-18;y<=18;y+=6)for(int x=-19;x<=19;x++)pixel(cx+x,cy+y+int(sinf(x*.18f+a+y*.1f)*3),y%12?accent:c);break;
   case 68: // Gemma sfaccettata.
    for(int i=0;i<6;i++){float q=i*PI/3+a*.2f;int x=cx+cosf(q)*20,y=cy+sinf(q)*22;line(cx,cy,x,y,1,i%2?c:accent);float b=q+PI/3;line(x,y,cx+cosf(b)*20,cy+sinf(b)*22,1,c);}break;
   case 69: // Sole con raggi mobili.
    ellipse(cx,cy,11,11,c);for(int i=0;i<8;i++){float q=a*.4f+i*PI/4;line(cx+cosf(q)*16,cy+sinf(q)*16,cx+cosf(q)*22,cy+sinf(q)*22,1,accent);}break;
   case 70: // Fiocchi di neve.
    for(int i=0;i<6;i++){float q=i*PI/3+a*.12f;int x=cx+cosf(q)*20,y=cy+sinf(q)*20;line(cx,cy,x,y,1,c);for(int k=-1;k<=1;k+=2)line(cx+cosf(q)*13,cy+sinf(q)*13,cx+cosf(q)*13+cosf(q+k*.8f)*7,cy+sinf(q)*13+sinf(q+k*.8f)*7,0,accent);}break;
   case 71: // Satellite e orbita.
    ellipse(cx,cy,19,22,c);ellipse(cx,cy,17,20,0);ellipse(cx+cosf(a)*19,cy+sinf(a)*22,4,4,accent);ellipse(cx,cy,5,6,c);break;
   case 72: // Occhi acquatici.
    ellipse(cx,cy,rx,ry,c);for(int x=-rx;x<=rx;x++)for(int y=-ry;y<int(sinf(x*.2f+a)*4);y++)pixel(cx+x,cy+y,0);break;
   case 73: // Pixel danzanti.
    for(int i=0;i<9;i++){int x=(i%3-1)*13,y=(i/3-1)*14+sin(a+i)*4;ellipse(cx+x,cy+y,2+int((sin(a+i)+1)*2),3,i%2?c:accent);}break;
   case 74: // Occhio fotografico.
    ellipse(cx,cy,22,24,c);ellipse(cx,cy,18,20,0);ellipse(cx,cy,10+sin(a)*4,12+sin(a)*4,accent);ellipse(cx,cy,5,6,0);ellipse(cx-5,cy-8,2,2,0xFFFF);break;
   case 75: // Nastri luminosi.
    for(int i=0;i<100;i++){float q=i*PI/50;pixel(cx+sin(q)*21,cy+sin(q*3+a)*18,c);pixel(cx+sin(q)*19,cy+cos(q*2+a)*16,accent);}break;
   case 76: // Occhi timidi.
    ellipse(cx,cy,rx,18,c);ellipse(cx+(right?-7:7),cy+5,8,10,0);ellipse(cx-6,cy-7,3,3,0xFFFF);break;
   case 77: // Occhi a molla.
    for(int x=-21;x<=21;x++)ellipse(cx+x,cy+sin(x*.3f+a)*14,1,2,c);break;
   case 78: // Petali doppi.
    for(int i=0;i<8;i++){float q=i*PI/4+a*.25f;ellipse(cx+cosf(q)*14,cy+sinf(q)*17,5,6,i%2?c:accent);}ellipse(cx,cy,5,5,0xFFE0);break;
   default: // Galassia a due bracci.
    for(int i=0;i<100;i++){float q=i*.13f+a,r=i*.22f;pixel(cx+cosf(q)*r,cy+sinf(q)*r,c);pixel(cx-cosf(q)*r,cy-sinf(q)*r,accent);}break;
  }
  return;
 }
 if(type>=48){
  float phase=t*3+(right?.7f:0);
  switch(type){
  case 48: // Fiore che respira.
   for(int i=0;i<6;i++){float a=phase*.4f+i*PI/3;ellipse(cx+cosf(a)*13,cy+sinf(a)*13,7,8,c);}
   ellipse(cx,cy,6,6,0);ellipse(cx,cy,3,3,c);break;
  case 49: // Radar con raggio rotante.
   ellipse(cx,cy,20,20,c);ellipse(cx,cy,17,17,0);
   line(cx,cy,cx+cosf(phase)*16,cy+sinf(phase)*16,1,c);ellipse(cx,cy,2,2,c);break;
  case 50: // Diamanti pulsanti.
   for(int i=0;i<4;i++){float a=i*PI/2+phase*.2f,b=a+PI/2;line(cx+cosf(a)*rx,cy+sinf(a)*ry,cx+cosf(b)*rx,cy+sinf(b)*ry,2,c);}break;
  case 51: // Farfalle.
   {int wing=8+int((sin(phase*2)+1)*4);ellipse(cx-wing/2,cy-7,wing,11,c);ellipse(cx+wing/2,cy-7,wing,11,c);ellipse(cx-6,cy+8,7,8,c);ellipse(cx+6,cy+8,7,8,c);line(cx,cy-12,cx,cy+14,1,0);}break;
  case 52: // Equalizzatore.
   for(int i=-2;i<=2;i++){int h=4+int((sinf(phase+i*.9f)+1)*9);line(cx+i*8,cy-h,cx+i*8,cy+h,2,c);}break;
  case 53: // Occhi robot con scansione.
   for(int y=-18;y<=18;y+=6)line(cx-17,cy+y,cx+17,cy+y,1,c);
   line(cx-18,cy+int(sinf(phase)*17),cx+18,cy+int(sinf(phase)*17),2,0xFFFF);break;
  case 54: // Comete orbitanti.
   for(int i=0;i<9;i++){float a=phase-i*.18f;ellipse(cx+cosf(a)*18,cy+sinf(a)*20,max(1,4-i/3),max(1,4-i/3),c);}break;
  case 55: // Mezzelune gemelle.
   ellipse(cx,cy,20,24,c);ellipse(cx+(right?-8:8),cy-5,18,22,0);break;
  case 56: // Risata con guance sollevate.
   for(int x=-20;x<=20;x++)ellipse(cx+x,cy-5+int(x*x*.025f),2,3,c);break;
  case 57: // Occhi curiosi con pupille mobili.
   ellipse(cx,cy,rx,ry,c);ellipse(cx+cosf(phase)*9,cy+sinf(phase)*10,8,10,0);ellipse(cx+cosf(phase)*9-2,cy+sinf(phase)*10-3,2,2,0xFFFF);break;
  case 58: // Portale pulsante.
   for(int i=0;i<3;i++){int r=5+(int(t*12)+i*7)%21;ellipse(cx,cy,r,r,c);ellipse(cx,cy,max(1,r-2),max(1,r-2),0);}break;
  case 59: // Pioggia digitale dentro gli occhi.
   for(int i=-2;i<=2;i++){int y=(int(t*22)+i*11+60)%35-17;line(cx+i*7,cy+y-5,cx+i*7,cy+y,1,c);}break;
  case 60: // Occhiali da sole.
   ellipse(cx,cy,24,16,c);ellipse(cx,cy,20,12,0);line(cx-11,cy+5,cx,cy-6,1,c);line(cx-3,cy+6,cx+8,cy-5,1,c);break;
  case 61: // Ventaglio di stelle.
   for(int i=0;i<8;i++){float a=phase*.5f+i*PI/4;line(cx+cosf(a)*7,cy+sinf(a)*7,cx+cosf(a)*22,cy+sinf(a)*22,2,c);}break;
  case 62: // Saltelli alternati.
   ellipse(cx,cy,rx,ry,c);ellipse(cx-5,cy-8,5,7,0);break;
  default: // Infinito in movimento.
   for(int i=0;i<72;i++){float a=i*PI/36+phase*.4f;pixel(cx+sin(a)*22,cy+sin(a*2)*13,c);pixel(cx+sin(a)*22,cy+sin(a*2)*13+1,c);}break;
  }
  return;
 }
 if(type==32){for(int i=0;i<10;i++){float a=i*PI/5-PI/2,b=(i+1)*PI/5-PI/2;int r=i%2?9:22,r2=i%2?22:9;line(cx+cosf(a)*r,cy+sinf(a)*r,cx+cosf(b)*r2,cy+sinf(b)*r2,2,c);}return;}
 if(type==33){ellipse(cx,cy,rx,ry,c);ellipse(cx+8,cy-6,rx-3,ry-2,0);return;}
 if(type==34){line(cx-13,cy-15,cx+9,cy-2,3,c);line(cx+9,cy-2,cx-8,cy+4,3,c);line(cx-8,cy+4,cx+12,cy+17,3,c);return;}
 if(type==35){ellipse(cx,cy,rx,ry,c);ellipse(cx+sin(t*3)*6,cy+cos(t*3)*8,rx-6,ry-6,0);return;}
 if(type==36){line(cx-12,cy-14,cx+12,cy,3,c);line(cx+12,cy,cx-12,cy+14,3,c);return;}
 if(type==37){for(int i=-2;i<=2;i++)line(cx+i*8,cy-12,cx+i*8,cy+12,2,c);return;}
 if(type==38){ellipse(cx,cy,rx,ry,c);ellipse(cx,cy,rx-4,ry-4,0);ellipse(cx+sin(t*4)*8,cy,4,8,c);return;}
 if(type==7){heart(cx,cy,c);return;}
 if(type==8){line(cx-12,cy-12,cx+12,cy+12,3,c);line(cx-12,cy+12,cx+12,cy-12,3,c);return;}
 if(type==9){for(int i=0;i<180;i++){float a=i*.095f,r=i*.085f;ellipse(cx+cosf(a+t*4)*r,cy+sinf(a+t*4)*r,1,1,c);}return;}
 if(type==10){line(cx-16,cy,cx+16,cy,2,c);line(cx,cy-19,cx,cy+19,2,c);line(cx-10,cy-10,cx+10,cy+10,1,c);line(cx-10,cy+10,cx+10,cy-10,1,c);return;}
 if(type==1||type==13||type==20){for(int x=-rx;x<=rx;x++){int y=(int)(float(x*x)/max(1,rx)*.55f);ellipse(cx+x,cy-6+y,2,3,c);}return;}
 if(type==6&&right){line(cx-rx,cy,cx+rx,cy-3,3,c);return;}
 ellipse(cx,cy,rx,ry,c);
 if(type==11||type==24)ellipse(cx,cy,max(1,rx-5),max(1,ry-5),0);
 if(type==2||type==3||type==17||type==18){for(int x=-rx;x<=rx;x++){int cut=(right?x:-x)/2;if(type==3||type==18)cut=-cut;for(int y=-ry;y<cut-ry/2;y++)pixel(cx+x,cy+y,0);}}
 if(type==4||type==16)for(int y=-ry;y<0;y++)for(int x=-rx;x<=rx;x++)pixel(cx+x,cy+y,0);
 if(type==21||type==22){ellipse(cx+(int)(sinf(t*2)*7),cy,5,ry-4,0);}
 if(type==25){line(cx-rx,cy,cx+rx,cy,2,0);line(cx,cy-ry,cx,cy+ry,2,0);}
}
// Bocca nel margine inferiore: stesso raster e orientamento degli occhi.
void mouth(int type,int dx,int dy,uint16_t c,float t){
 const int cx=80+dx/3,cy=75+constrain(dy/4,-2,2);
 // Ogni espressione ha una posa; il movimento resta continuo.
 const uint8_t poses[COUNT]={
  0,1,2,3,4,5,6,1,7,8,5,5,8,1,1,0,
  4,2,3,5,1,6,6,8,5,7,4,1,8,3,6,5,
  1,0,2,8,6,7,5,1,3,4,0,1,2,1,6,8,
  1,5,0,1,8,7,5,4,1,5,5,8,6,1,1,8,
  5,1,8,0,5,1,4,5,0,8,5,8,6,8,1,5};
 int pose=poses[type%COUNT];
 if(pose==5){ // O di sorpresa, pulsante.
  int r=3+int((sinf(t*4)+1)*1.3f);
  ellipse(cx,cy,4,r,c);ellipse(cx,cy,2,max(1,r-2),0);return;
 }
 if(pose==7){ // Espressione KO / denti serrati.
  line(cx-7,cy-2,cx+7,cy-2,1,c);line(cx-7,cy+2,cx+7,cy+2,1,c);
  line(cx-7,cy-2,cx-7,cy+2,1,c);line(cx+7,cy-2,cx+7,cy+2,1,c);
  for(int x=-3;x<=3;x+=3)line(cx+x,cy-1,cx+x,cy+1,0,c);return;
 }
 if(pose==8){ // Bocca ondulata per vertigini e facce buffe.
  for(int x=-8;x<=8;x++)ellipse(cx+x,cy+int(sinf(x*.55f+t*6)*2),1,1,c);return;
 }
 if(pose==4){ // Respiro lento durante sonno e stanchezza.
  ellipse(cx,cy,4,1+int((sinf(t*2)+1)*.6f),c);return;
 }
 for(int x=-8;x<=8;x++){
  float u=x/8.f;int y=0;
  if(pose==0)y=int(2*(1-u*u));
  else if(pose==1)y=int(4*(1-u*u))-2;
  else if(pose==2||pose==3)y=-int(3*(1-u*u))+1;
  else if(pose==6)y=x/3;
  ellipse(cx+x,cy+y,1,1,c);
 }
 if(pose==1){ // Piccolo sorriso aperto, animato.
  int opening=2+int((sinf(t*5)+1)*.6f);
  ellipse(cx,cy,5,opening,c);ellipse(cx,cy-1,3,1,0);
 }
}
// Timeline dedicata: movimenti continui, senza casualità fra due fotogrammi.
float easeEmotion(float v){v=constrain(v,0.f,1.f);return v*v*(3.f-2.f*v);}
uint16_t emotionColor(uint16_t a,uint16_t b,float f){
 f=constrain(f,0.f,1.f);
 int r=int(((a>>11)&31)*(1-f)+((b>>11)&31)*f);
 int g=int(((a>>5)&63)*(1-f)+((b>>5)&63)*f);
 int bl=int((a&31)*(1-f)+(b&31)*f);
 return (r<<11)|(g<<5)|bl;
}
void drawOffense(float age){
 if(age<1.2f){
  float rise=easeEmotion(age/.22f),settle=1-easeEmotion((age-.96f)/.24f);
  float energy=rise*settle;
  int dx=int(sinf(age*57)*2.5f*energy),dy=int(sinf(age*29)*1.6f*energy);
  uint16_t hot=emotionColor(0xFD20,0xF800,(.5f+.5f*sinf(age*12))*rise);
  // Bagliore scuro e scintille centrifughe, non lampi a tutto schermo.
  for(int side=-1;side<=1;side+=2){
   int cx=80+side*33+dx,cy=40+dy;
   int rx=22+int(2*energy),ry=23-int(7*rise)+int(sinf(age*16)*energy);
   ellipse(cx,cy,rx+3,ry+3,0x4000);
   ellipse(cx,cy,rx,ry,hot);
   // Taglio inclinato verso il naso e pupilla incandescente.
   for(int x=-rx-2;x<=rx+2;x++){
    int top=cy-ry+7-int(side*x*.37f*rise);
    for(int y=cy-ry-4;y<top;y++)pixel(cx+x,y,0);
   }
   ellipse(cx-side*3,cy+2,4,7,0x2000);
   ellipse(cx-side*3-1,cy,1,3,0xFFE0);
   line(cx-19,cy-24+side*6,cx+19,cy-24-side*6,2,hot);
   ellipse(cx+side*21,cy+17,5,2,0x8800);
   for(int i=0;i<3;i++){
    float q=fmodf(age*1.7f+i*.33f,1.f);
    int x=cx+side*int(26+q*11),y=cy-12-int(q*17)+i*8;
    line(x,y,x+side*3,y-3,0,emotionColor(0xFD20,0x4000,q));
   }
  }
  int mx=80+dx,my=72+dy;
  // Denti davvero serrati: contorno, smalto e separazioni scure.
  line(mx-10,my-3,mx+10,my-3,1,hot);
  line(mx-10,my+3,mx+10,my+3,1,hot);
  for(int y=-1;y<=1;y++)line(mx-9,my+y,mx+9,my+y,0,0xFF58);
  for(int x=-6;x<=6;x+=4)line(mx+x,my-2,mx+x,my+2,0,0x6000);
  int r=3+int(2*energy),cy=12+int(sinf(age*18)*energy);
  for(int side=-1;side<=1;side+=2){
   line(80+side*2,cy-2,80+side*(r+3),cy-r-3,1,hot);
   line(80+side*2,cy+2,80+side*(r+3),cy+r+3,1,hot);
  }
  return;
 }
 float t=age-1.2f;
 float turn=easeEmotion(t/1.25f);
 float back=easeEmotion((t-5.1f)/.9f);
 float retreat=turn*(1-back);
 float scale=1-.47f*retreat;
 float breath=sinf(t*2.5f);
 int cx=80+int(31*retreat),cy=42-int(8*retreat)+int(breath*1.5f);
 uint16_t c=emotionColor(0xFB08,0xB51F,easeEmotion(t/.8f));
 c=emotionColor(c,colors[palette],back);
 int spread=int(33*scale),width=max(6,int(20*scale));
 for(int side=-1;side<=1;side+=2){
  int ex=cx+side*spread;
  // Palpebre sempre sigillate: nessuna pupilla, neppure nel rientro.
  for(int x=-width;x<=width;x++){
   float u=float(x)/width;
   int y=cy+int((3*(1-u*u)+side*u*3*retreat)*scale);
   ellipse(ex+x,y,1,1,c);
  }
  line(ex-width,cy-int(13*scale)+side,ex+width,cy-int(16*scale)-side,1,c);
  // Guance discrete e ciglia rivolte lontano dal centro.
  ellipse(ex+side*int(8*scale),cy+int(12*scale),max(2,int(5*scale)),1,0x780C);
  line(ex+side*width,cy,ex+side*(width+3),cy-3,0,c);
 }
 int mx=cx+int(5*retreat),my=cy+int(29*scale);
 for(int x=-int(8*scale);x<=int(8*scale);x++){
  float u=x/(8*scale);
  ellipse(mx+x,my-int(3*(1-u*u)*scale),1,1,c);
 }
 ellipse(mx+int(3*scale),my+int(4*scale),max(2,int(4*scale)),1,0xF9B0);
 // Due sbuffi disegnati come piccoli anelli in dissolvenza.
 for(int i=0;i<2;i++){
  float q=(t-(.25f+i*.48f))/1.05f;
  if(q>0&&q<1){
   int x=cx-int(42*scale)-int(q*18),y=cy+14-int(q*11),r=2+int(q*4);
   uint16_t steam=emotionColor(0xC65F,0,q);
   ellipse(x,y,r,max(2,r-1),steam);ellipse(x,y,max(1,r-2),max(1,r-3),0);
  }
 }
 // Puntini di silenzio: compaiono uno alla volta, poi svaniscono.
 float dots=easeEmotion((t-1.8f)/.5f)*(1-easeEmotion((t-4.6f)/.5f));
 for(int i=0;i<3;i++)if(t>1.8f+i*.3f)
  ellipse(28+i*8,25+int(sinf(t*2-i*.6f)),1,1,emotionColor(0,0x867F,dots));
}
void present();
void arcadeFace(uint32_t now);
void draw(uint32_t now){
 static uint32_t lastDraw=0;
 const float dt=lastDraw?min(uint32_t(50),now-lastDraw)*.001f:.016f;
 lastDraw=now;
 memset(frame,0,sizeof(frame));float t=now*.001f;
 if((int32_t)(now-gazeAt)>=0){targetX=(int)(esp_random()%25)-12;targetY=(int)(esp_random()%13)-6;gazeAt=now+700+esp_random()%1800;}
 gazeX+=(targetX-gazeX)*(1.f-expf(-6.38f*dt));gazeY+=(targetY-gazeY)*(1.f-expf(-5.28f*dt));
 const bool sweet=treatUntil && (int32_t)(treatUntil-now)>0;
 const float treatTime=(now-treatStarted)*.001f;
 const bool offended=offendedUntil && (int32_t)(offendedUntil-now)>0;
 const uint32_t emotionAge=now-offenseStarted;
 const bool angry=offended && emotionAge<1200;
 const float emotionTime=emotionAge*.001f;
 int dx=gazeX,dy=gazeY+sin(t*2)*1.5f,rx=21,ry=25;
 switch(mood){
 case 48:dy=sinf(t*2)*3;break;
 case 49:dx=0;dy=0;break;
 case 50:rx=18+sinf(t*4)*4;ry=23+cosf(t*4)*3;break;
 case 51:dy=sinf(t*5)*5;break;
 case 52:dx=0;dy=0;break;
 case 53:dx=sinf(t)*8;break;
 case 54:dy=cosf(t*2)*4;break;
 case 55:dy=5;break;
 case 56:dy=-abs(int(sinf(t*6)*7));break;
 case 57:rx=24;ry=27;break;
 case 58:dx=0;dy=0;break;
 case 59:dy=sinf(t)*3;break;
 case 60:dx=sinf(t*2)*8;ry=16;break;
 case 61:dy=sinf(t*3)*4;break;
 case 62:ry=18+sin(t*6)*7;dy=-abs(int(sin(t*6)*8));break;
 case 63:dx=0;dy=sin(t*2)*4;break;
 case 4:ry=18;break;case 5:rx=15;ry=31;break;
 case 12:dx=sinf(t*12)*14;dy=cosf(t*9)*5;break;
 case 13:dy=8;break;case 14:rx=26;ry=14;dy=sinf(t*8)*7;break;
 case 15:dx=sinf(t*2)*21;ry=20;break;
 case 16:dy=7+sinf(t*2)*3;ry=14;break;
 case 17:dx=sinf(t*16)*3;break;case 18:dy=4;break;
 case 19:rx=18+sinf(t*5)*5;ry=24+cosf(t*5)*6;break;
 case 20:dy=-abs((int)(sinf(t*6)*12));break;
 case 21:dx=-12;break;case 22:dx=12;break;
 case 23:dx=cosf(t*3)*15;dy=sinf(t*3)*12;break;
 case 24:ry=29;rx=18;break;
 case 26:rx=25;ry=8;dy=6;break;
 case 27:dy=sinf(t*10)*9;break;
 case 28:dx=sinf(t*7)*16;dy=cosf(t*7)*8;break;
 case 29:rx=17;ry=18;dy=10;break;
 case 30:ry=22+sinf(t*4)*8;break;
 case 39:rx=24;ry=18;dy=sin(t*5)*6;break;
 case 40:rx=15;ry=30;dx=sin(t*9)*5;break;
 case 41:rx=27;ry=10;break;
 case 42:dx=sin(t*1.4f)*23;dy=cos(t*1.4f)*10;break;
 case 43:rx=20+sin(t*8)*5;ry=23+sin(t*8)*7;break;
 case 44:dx=sin(t*16)*2;ry=29;break;
 case 45:rx=25;ry=21;dy=sin(t*7)*7;break;
 case 46:rx=18;ry=18;dx=sin(t*4)*18;break;
 case 47:ry=12+abs((int)(sin(t*3)*18));dx=cos(t*3)*10;break;
 case 31:dx=0;ry=4+abs((int)(sinf(t*4)*25));break;
 }
 if(offended){
  if(angry){dx=int(sinf(emotionTime*48)*3);dy=int(sinf(emotionTime*24)*2);rx=23;ry=18+int(sinf(emotionTime*14)*3);}
  else { // Distoglie lo sguardo, respira e stringe lentamente le palpebre.
   dx=10+int(sinf(emotionTime*1.8f)*2);dy=3+int(sinf(emotionTime*3)*2);
   rx=22;ry=12+int(sinf(emotionTime*2.5f)*2);
  }
 }
 const float shapeBlend=1.f-expf(-7.92f*dt);
 smoothRX+=(rx-smoothRX)*shapeBlend;smoothRY+=(ry-smoothRY)*shapeBlend;
 rx=smoothRX;ry=smoothRY;
 uint32_t tr=now-transitionAt;
 if(tr<240)ry=max(2,(int)(ry*fabsf((float)tr-120)/120));
 if(blinkStart){uint32_t b=now-blinkStart;if(b<180)ry=max(2,(int)(ry*fabsf((float)b-90)/90));else blinkStart=0;}
 const bool party=(int32_t)(partyUntil-now)>0;
 uint16_t c=offended?(angry?(sinf(emotionTime*18)>0?0xF800:0xFA00):0xB51F):colors[party?((now-partyStarted)/240)%COLOR_COUNT:palette];
 if(mood>=64&&!offended&&!sweet&&!party)c=emotionColor(c,colors[(palette+5)%COLOR_COUNT],(.5f+.5f*sinf(t*.7f))*.45f);
 int shape=mood;
 if(mood==39||mood==45)shape=1;
 if(mood==40)shape=11;
 if(mood==41)shape=4;
 if(mood==43)shape=7;
 if(mood==44)shape=2;
 if(mood==46)shape=21;
 if(offended)shape=angry?2:4;
 if(sweet){
  // Stesso stile luminoso delle altre facce: occhi da risata e saltelli felici.
  dx=int(sinf(treatTime*2)*3);
  dy=-int(fabsf(sinf(treatTime*5))*3);
  eye(47+dx,42+dy,22,25,1,false,c,t);
  eye(113+dx,42+dy,22,25,1,true,c,t);
  ellipse(25+dx,60+dy,7,3,0xF9B0);
  ellipse(135+dx,60+dy,7,3,0xF9B0);
  if(treatTime>=1.f && treatTime<4.6f){
   // La bocca mastica in sincronia con i piccoli morsi.
   int opening=3+int((sinf((treatTime-1)*PI*5)+1)*2);
   ellipse(80,74,10,opening,c);
   ellipse(80,74,7,max(1,opening-2),0);
   ellipse(80,76,4,1,0xF9B0);
  }else mouth(1,dx,dy,c,t);
 }else if(!offended){
 eye(47+dx,42+dy,rx,ry,shape,false,c,t);
 eye(113+dx,42+(mood==62?-dy:dy),rx,mood==30?max(2,44-ry):ry,shape,true,c,t);
 if(mood==39||mood==45){ellipse(24+dx,64+dy,8,3,0xF9B0);ellipse(136+dx,64+dy,8,3,0xF9B0);}
 if(mood==76){ellipse(24+dx,63+dy,7,2,0xF9B0);ellipse(136+dx,63+dy,7,2,0xF9B0);}
 if(mood==40){ellipse(140,20+(now/70)%18,3,6,0x049F);}
 }
 if(sweet){}
 else if(!offended)mouth(mood,dx,dy,c,t);
 else drawOffense(emotionTime);

 if(sweet){
  // Il lecca-lecca raggiunge la bocca; ogni morso ne riduce la caramella.
  if(treatTime<5.3f){
   float enter=constrain(treatTime/1.f,0.f,1.f);
   enter=enter*enter*(3.f-2.f*enter);
   int bites=treatTime<1.f?0:min(6,int((treatTime-1.f)/.6f));
   int radius=max(0,11-bites*2);
   float exit=constrain((treatTime-4.6f)/.7f,0.f,1.f);
   int cx=109-int(enter*23),cy=104-int(enter*33)+int(exit*35);
   if(treatTime>=1.f&&treatTime<4.6f)cy+=int(sinf((treatTime-1)*PI*5)*2);
   line(cx+2,cy+4,cx+7,cy+24,1,0xFFFF);
   if(radius>0){
    ellipse(cx,cy,radius,radius,0xFFE0);
    for(int i=0;i<100;i++){
     float a=i*.19f+treatTime*2,r=i*.0095f*max(0,radius-1);
     ellipse(cx+int(cosf(a)*r),cy+int(sinf(a)*r),0,0,colors[(i/17+22)%COLOR_COUNT]);
    }
    // Impronta del morso sul lato rivolto alla bocca.
    if(bites>0)ellipse(cx-radius,cy-2,3,3,0);
    if(radius>4)ellipse(cx-2,cy-3,1,1,0xFFFF);
   }
  }
  // Cuoricini che salgono lungo i bordi senza coprire gli occhi.
  for(int i=0;i<4;i++){
   int x=i%2?148:12;
   int y=76-int(fmodf(treatTime*13+i*21,72.f));
   ellipse(x-2,y-1,2,2,0xF819);ellipse(x+2,y-1,2,2,0xF819);
   line(x-3,y,x,y+4,0,0xF819);line(x+3,y,x,y+4,0,0xF819);
  }
 }
 // Coriandoli deterministici: nessuna allocazione o attesa nel disegno.
 if(party){
  uint32_t elapsed=now-partyStarted;
  for(int i=0;i<24;i++){
   float speed=12.f+(i%7)*3.f;
   int y=int(elapsed*.001f*speed+i*17)%104-9;
   int x=(i*47+int(sinf(t*2+i)*5)+W)%W;
   // Lascia leggibili occhi e bocca: coriandoli sui bordi.
   if(y>15&&y<81&&x>17&&x<143)continue;
   uint16_t confetti=colors[i%COLOR_COUNT];
   line(x,y,x+int(sinf(t*5+i)*2),y+2,0,confetti);
  }
 }

 if((int32_t)(reactionUntil-now)>0){for(int i=0;i<9;i++){float a=i*6.283f/9+t;int r=25+(now/20+i*7)%55;int x=80+cosf(a)*r,y=43+sinf(a)*r*.55f;line(x-2,y,x+2,y,1,colors[i%COLOR_COUNT]);line(x,y-2,x,y+2,1,colors[i%COLOR_COUNT]);}}
 if(!autoPlay){ellipse(77,82,1,1,c);ellipse(82,82,1,1,c);}
 if(mood==3||mood==18){int drop=now/50%23;ellipse(69,51+drop,3,5,0x049F);ellipse(136,51+drop,3,5,0x049F);}
 if(mood==16){line(133,16,145,16,1,c);line(145,16,133,27,1,c);line(133,27,145,27,1,c);}
 if(mood==27||mood==28){for(int i=0;i<6;i++){int x=(i*29+now/35)%160;int y=(i*19+now/65)%86;ellipse(x,y,1,1,colors[(i+palette)%COLOR_COUNT]);}}
 present();
}

// Interfaccia e giochi sullo stesso raster degli occhi.
class FrameText: public Adafruit_GFX {
 public: FrameText():Adafruit_GFX(W,H){} void drawPixel(int16_t x,int16_t y,uint16_t c) override {pixel(x,y,c);}
} text;
enum Screen {EYES,MENU,SNAKE,DINO,FLAPPY,PADDLE,RELAX,METEORS,NEON,RPS};
Screen screen=EYES;
int menuIndex=0,score=0,bestSnake=0,bestDino=0,bestFlappy=0,bestPaddle=0;
bool dead=false,dirty=true,turnQueued=false;
uint32_t gameAt=0,gameStarted=0,renderAt=0;
int sx[240],sy[240],length=4,direction=0,foodX=0,foodY=0;
float dinoY=0,velocity=0,obstacleX=170;
float birdY=40,birdV=0,pipeX=165;
int gapY=40; bool pipePassed=false;
float paddleX=65,ballX=80,ballY=40,ballVX=1.5f,ballVY=1.6f;
int paddleDir=1;
Preferences records;
int jumps=0,obstacleHeight=17,obstacleWidth=9,combo=0;
bool bricks[3][10];
int stage=1; uint32_t readyUntil=0,flashUntil=0; bool savedDeath=false;
struct Spark {float x,y,vx,vy; int life; uint16_t color;};
Spark sparks[28];
void burst(float x,float y,uint16_t color){
 for(int i=0;i<28;i++){float a=i*6.283185f/28;float v=.5f+(esp_random()%20)*.08f;sparks[i]={x,y,cosf(a)*v,sinf(a)*v,24+int(esp_random()%20),color};}
}
void newBricks(){for(int y=0;y<3;y++)for(int x=0;x<10;x++)bricks[y][x]=true;}
void saveRecords(){records.putUInt("snake",bestSnake);records.putUInt("dino",bestDino);records.putUInt("fly",bestFlappy);records.putUInt("paddle",bestPaddle);}

// Poche scelte per pagina; tutti i giochi rimangono disponibili.
enum MenuPage {HOME_PAGE,GAMES_PAGE,STYLE_PAGE};
MenuPage menuPage=HOME_PAGE;
uint32_t menuFeedbackUntil=0;
int menuCount(){return menuPage==HOME_PAGE?4:menuPage==GAMES_PAGE?8:4;}
const char* homeNames[]={"OCCHI","GIOCHI","RELAX","STILE"};
const char* homeHelp[]={"80 facce animate","7 giochi da scoprire","Respira con la faccia","Colori e automatismo"};
const char* gameNames[]={"SNAKE","DINOSAURO","VOLATORE","RACCHETTA","METEORE","NEON RUN","SASSO CARTA","INDIETRO"};
const char* gameHelp[]={"Clic: svolta a destra","Clic: salto / doppio","Clic: batti le ali","Clic: inverti direzione","Clic: inverti la nave","Clic: cambia corsia","1 sasso 2 carta 3 forbici","Torna alle 4 sezioni"};
const char* styleNames[]={"COLORE","AUTOMATICO","CASUALE","INDIETRO"};
uint32_t relaxStarted=0;
int relaxScene=0;
uint32_t relaxSceneAt=0;
struct SpaceRock {float x,y,speed;int radius;bool gem;};
SpaceRock rocks[9];
float shipX=80; int shipDir=1,spaceBest=0,shield=1;
uint32_t spaceHit=0;
void resetSpace(){shipX=80;shipDir=1;shield=1;spaceHit=0;
 for(int i=0;i<9;i++)rocks[i]={float(8+esp_random()%144),float(-i*15-12),.4f+float(esp_random()%60)/100,3+int(esp_random()%3),i%4==0};
}

void label(int x,int y,const char* str,uint16_t c=0xFFFF,int size=1){text.setTextSize(size);text.setTextColor(c);text.setCursor(x,y);text.print(str);}
void arcadeFace(uint32_t now){
 text.fillRect(143,77,17,9,0);
 uint16_t c=dead?0xFB08:colors[palette];
 bool blink=now%2900<130;
 for(int x=0;x<2;x++){
  int cx=147+x*8;
  if(dead){line(cx-1,78,cx+1,80,0,c);line(cx+1,78,cx-1,80,0,c);}
  else ellipse(cx,79,1,blink?1:2,c);
 }
 for(int x=-3;x<=3;x++)pixel(151+x,(dead?84:82)+(dead?-1:1)*int(1-x*x/9.f),c);
}
void present(){
 // Piccoli rettangoli compatti: niente trasferimenti delle fasce invariate.
 // Mantiene esattamente il verso e il raster 2x esistenti.
 constexpr int BAND=8;
 static uint16_t output[320*BAND*2];
 for(int y0=0;y0<H;y0+=BAND){
  const int y1=min(H,y0+BAND);
  int left=W,right=-1,first=y1,last=-1;
  for(int y=y0;y<y1;y++){
   const int base=y*W;
   if(memcmp(frame+base,previous+base,W*sizeof(uint16_t))==0)continue;
   first=min(first,y);last=y;
   for(int x=0;x<W;x++)if(frame[base+x]!=previous[base+x]){
    left=min(left,x);right=max(right,x);
   }
  }
  if(right<left)continue;
  const int width=(right-left+1)*2;
  const int rows=(last-first+1)*2;
  for(int y=last;y>=first;y--){
   int k=(last-y)*2*width;
   for(int x=right;x>=left;x--){
    const uint16_t c=frame[y*W+x];
    output[k]=c;output[k+1]=c;
    output[k+width]=c;output[k+width+1]=c;k+=2;
   }
  }
  lcd.drawRGBBitmap((W-1-right)*2,(H-1-last)*2,output,width,rows);
  for(int y=first;y<=last;y++)
   memcpy(previous+y*W+left,frame+y*W+left,(right-left+1)*sizeof(uint16_t));
 }
}

void food(){
 bool occupied;
 do {foodX=esp_random()%26;foodY=esp_random()%10;occupied=false;
 for(int i=0;i<length;i++)if(sx[i]==foodX&&sy[i]==foodY)occupied=true;
 }while(occupied);
}
// NEON RUN: tre corsie, movimento graduale e simulazione indipendente dal rendering.
struct NeonItem {float y; int lane; bool gem;};
NeonItem neonItems[7];
int neonLane=1,neonBest=0,neonShield=1,neonChain=0;
float neonX=80; uint32_t neonTick=0,neonDamage=0;
void resetNeon(uint32_t now){
 neonLane=1;neonX=80;neonShield=1;neonChain=0;neonTick=now;neonDamage=0;
 for(int i=0;i<7;i++)neonItems[i]={float(-i*30-20),int(esp_random()%3),i%3==0};
}
void neonGame(uint32_t now){
 if(now-renderAt<FRAME_MS)return;
 float dt=min(uint32_t(40),now-neonTick)/1000.f;neonTick=now;renderAt=now;
 if(!dead&&now>=readyUntil){
  neonX+=(40+neonLane*40-neonX)*min(1.f,dt*18);
  for(auto &o:neonItems){
   o.y+=(24+min(34,score/2))*dt;
   if(o.y>61&&o.y<75&&fabsf(neonX-(40+o.lane*40))<11){
    if(o.gem){neonChain++;score+=3+min(7,neonChain);if(neonChain%4==0)neonShield=1;burst(neonX,65,0x07FF);}
    else if(now-neonDamage>1000){neonChain=0;neonDamage=now;if(neonShield){neonShield=0;burst(neonX,65,0xFFE0);}else{dead=true;burst(neonX,65,0xF800);}}
    o.y=-100; o.lane=esp_random()%3; o.gem=esp_random()%3==0;
   }
   if(o.y>89){if(!o.gem)score++;o.y=-100;o.lane=esp_random()%3;o.gem=esp_random()%3==0;}
  }
 }
 if(dead&&!savedDeath){savedDeath=true;neonBest=max(neonBest,score);records.putUInt("neon",neonBest);}
 memset(frame,0,sizeof(frame));
 for(int i=0;i<22;i++){int x=(i*73+11)%160;int y=14+(int(now/65)+i*17)%62;pixel(x,y,0x2104);}
 for(int lane=0;lane<4;lane++)for(int y=14;y<77;y+=8){int yy=14+(y-14+int(now/55))%63;line(20+lane*40,yy,20+lane*40,yy+3,0,0x3186);}
 for(auto &o:neonItems)if(o.y>17&&o.y<77){int x=40+o.lane*40,y=o.y;
  if(o.gem){text.fillTriangle(x,y-5,x-4,y,x,y+5,0x07FF);text.fillTriangle(x,y-5,x+4,y,x,y+5,0xFFFF);}
  else {text.fillRoundRect(x-8,y-4,16,8,2,0xF800);line(x-5,y-2,x+5,y-2,0,0xFD20);}
 }
 if(!dead&&(now-neonDamage>1000||now/90%2)){
  int x=neonX;uint16_t c=colors[palette];
  text.fillTriangle(x,59,x-7,72,x+7,72,c);ellipse(x-2,66,1,2,0);ellipse(x+2,66,1,2,0);
  line(x-3,74,x,76+now/70%3,1,0xFD20);
  if(neonShield)text.drawCircle(x,67,10,0x07FF);
 }
 for(auto &p:sparks)if(p.life>0){pixel(p.x,p.y,p.color);p.x+=p.vx;p.y+=p.vy;p.life--;}
 text.fillRect(0,0,W,13,0);char b[32];snprintf(b,sizeof(b),"NEON %d TOP %d",score,max(neonBest,score));label(2,2,b,colors[palette]);
 if(!dead&&now<readyUntil){label(46,31,"PRONTO?",0xFFFF);char n[4];snprintf(n,sizeof(n),"%d",int((readyUntil-now+499)/500));label(76,44,n,0xFFE0,2);}
 if(dead){text.fillRect(15,29,130,25,0);label(23,32,"PARTITA FINITA",0xF800);label(23,44,"CLIC: RIPROVA");}
 label(1,79,"CLIC:CORSIA LUNGO:MENU",0xBDF7);arcadeFace(now);present();
}
// Le sequenze di clic di questo gioco non attivano i gesti degli occhi.
int rpsClicks=0,rpsPlayer=-1,rpsBot=-1,rpsWins=0,rpsLosses=0,rpsOutcome=0;
uint32_t rpsClickAt=0,rpsRevealAt=0,rpsCountdownAt=0;
bool rpsCounting=false;
const char* rpsNames[]={"SASSO","CARTA","FORBICI"};
void resolveRps(uint32_t now){
 rpsCounting=false;
 // Rejection sampling: tre scelte equiprobabili, indipendenti dai clic.
 uint32_t n;do{n=esp_random();}while(n==UINT32_MAX);
 rpsBot=n%3;
 int result=(rpsPlayer-rpsBot+3)%3;
 rpsOutcome=result==0?0:result==1?1:-1;
 if(rpsOutcome>0)rpsWins++;else if(rpsOutcome<0)rpsLosses++;
 rpsRevealAt=now;
}
void rpsSymbol(int x,int y,int choice,uint16_t c){
 if(choice==0){text.fillRoundRect(x-9,y-6,18,13,3,c);for(int i=0;i<3;i++)line(x-5+i*5,y-5,x-5+i*5,y-1,0,0);}
 else if(choice==1){text.fillRect(x-8,y-9,16,19,c);for(int i=0;i<3;i++)line(x-5,y-5+i*5,x+5,y-5+i*5,0,0x4208);}
 else {text.drawCircle(x-5,y+5,4,c);text.drawCircle(x+5,y+5,4,c);line(x-3,y+2,x+7,y-9,1,c);line(x+3,y+2,x-7,y-9,1,c);}
}
void rpsGame(uint32_t now){
 if(rpsClicks&&!down&&now-rpsClickAt>550){
  rpsPlayer=rpsClicks-1;rpsClicks=0;rpsCounting=true;rpsCountdownAt=now;
 }
 if(rpsCounting&&now-rpsCountdownAt>=1800)resolveRps(now);
 if(now-renderAt<FRAME_MS)return;renderAt=now;
 memset(frame,0,sizeof(frame));
 bool result=rpsPlayer>=0&&!rpsClicks&&!rpsCounting;
 bool finished=rpsWins>=3||rpsLosses>=3;
 uint16_t c=result?(rpsOutcome>0?0x07E0:rpsOutcome<0?0xFD20:0x07FF):colors[palette];
 // Three illuminated gems make the match score readable at a glance.
 label(3,2,"TU",0xFFFF);label(72,2,"AMICO",c);
 for(int i=0;i<3;i++){
  ellipse(24+i*11,5,3,3,i<rpsWins?0x07E0:0x2104);
  ellipse(110+i*11,5,3,3,i<rpsLosses?0xFD20:0x2104);
 }
 bool blink=now%2800<110;int bob=int(sinf(now/220.f)*2);
 for(int x=120;x<=144;x+=24){
  if(result&&rpsOutcome>0)line(x-4,29+bob,x,26+bob,0,c),line(x,26+bob,x+4,29+bob,0,c);
  else {ellipse(x,28+bob,5,blink?1:7,c);if(!blink)ellipse(x-1,28+bob,2,3,0);}
 }
 for(int x=-7;x<=7;x++)pixel(132+x,43+bob+(result&&rpsOutcome<0?-1:1)*int((1-x*x/49.f)*3),c);
 if(rpsCounting){
  int beat=(now-rpsCountdownAt)/600;
  label(4,15,"SASSO CARTA FORBICI",0xBDF7);
  int bounce=int(sinf((now-rpsCountdownAt)%600/600.f*PI)*5);
  rpsSymbol(25,41-bounce,0,0xFFFF);rpsSymbol(78,41-bounce,0,c);
  char n[4];snprintf(n,sizeof(n),"%d",3-beat);label(48,32,n,0xFFE0,2);
  label(4,62,"PRONTO ALLA SFIDA?",c);
 }else if(result){
  label(5,15,"TU",0xBDF7);label(65,15,"AMICO",c);
  rpsSymbol(25,36,rpsPlayer,0xFFFF);rpsSymbol(79,36,rpsBot,c);
  label(2,50,rpsNames[rpsPlayer]);label(62,50,rpsNames[rpsBot],c);
  const char* reason=rpsOutcome==0?"STESSA SCELTA!":
    ((rpsOutcome>0?rpsPlayer:rpsBot)==0?"SASSO BATTE FORBICI":
     (rpsOutcome>0?rpsPlayer:rpsBot)==1?"CARTA BATTE SASSO":"FORBICI BATTE CARTA");
  label(2,62,finished?(rpsWins>=3?"SEI IL CAMPIONE!":"RIVINCITA?"):rpsOutcome>0?"PUNTO A TE!":rpsOutcome<0?"PUNTO ALL'AMICO!":"PAREGGIO!",c);
  label(1,78,now-rpsRevealAt<1400?reason:finished?"CLIC: NUOVA SFIDA":"CLIC: PROSSIMO TURNO",0xBDF7);
  if(rpsOutcome>0&&now-rpsRevealAt<1400){
   for(int i=0;i<12;i++){int x=(i*37+int((now-rpsRevealAt)/30))%158;
    int y=12+(i*13+int((now-rpsRevealAt)/20))%45;pixel(x,y,colors[i%COLOR_COUNT]);}
  }
 }else{
  label(2,14,"SCEGLI CON I CLIC",0xFFE0);
  for(int i=0;i<3;i++){
   int x=18+i*33;bool selected=rpsClicks==i+1;
   if(selected)text.drawRoundRect(x-14,24,29,29,3,c);
   rpsSymbol(x,38,i,selected?c:0xBDF7);
   char n[3];snprintf(n,sizeof(n),"%d",i+1);label(x-2,56,n,selected?c:0xBDF7);
  }
  if(rpsClicks){label(2,66,rpsNames[rpsClicks-1],c);
   text.fillRect(51,67,int(min(uint32_t(550),now-rpsClickAt)*56/550),3,c);}
  else label(2,66,"VINCE CHI FA 3 PUNTI",c);
  label(1,78,"1:SASSO 2:CARTA 3:FORBICI",0xBDF7);
 }
 // Same long-release gesture as the other games.
 if(down){text.fillRect(113,57,min(44,int((now-pressedAt)*44/700)),2,c);label(113,65,"MENU",0xBDF7);}
 present();
}
void startGame(Screen next,uint32_t now){
 if(next==METEORS)resetSpace();
 if(next==NEON)resetNeon(now);
 readyUntil=now+1500;savedDeath=false;stage=1;jumps=0;combo=0;newBricks();memset(sparks,0,sizeof(sparks));
 screen=next;score=0;dead=false;turnQueued=false;dirty=true;gameAt=now;gameStarted=now;
 if(next==SNAKE){length=4;direction=0;for(int i=0;i<length;i++){sx[i]=8-i;sy[i]=5;}food();}
 else if(next==PADDLE){paddleX=65;paddleDir=1;ballX=80;ballY=48;ballVX=1.5f;ballVY=1.6f;}
 else if(next==FLAPPY){birdY=40;birdV=-1.8f;pipeX=165;gapY=35+esp_random()%20;pipePassed=false;}
 else {dinoY=0;velocity=0;obstacleX=170;obstacleHeight=17;obstacleWidth=9;}
}
void shortPress(uint32_t now){
 if(screen==RPS){
  if(rpsCounting)return;
  if(rpsPlayer>=0&&!rpsClicks){
   if(now-rpsRevealAt<1400)return;
   if(rpsWins>=3||rpsLosses>=3){rpsWins=0;rpsLosses=0;}
   rpsPlayer=-1;rpsBot=-1;return;
  }
  rpsClicks=rpsClicks%3+1;rpsClickAt=now;
 }
 else if(screen==MENU){menuIndex=(menuIndex+1)%menuCount();dirty=true;}
 else if(screen==NEON){if(dead)startGame(NEON,now);else neonLane=(neonLane+1)%3;}
 else if(screen==METEORS){if(dead)startGame(METEORS,now);else shipDir=-shipDir;}
 else if(screen==RELAX){relaxScene=(relaxScene+1)%4;relaxSceneAt=now;}
 else if(screen==SNAKE){if(dead)startGame(SNAKE,now);else turnQueued=true;}
 else if(screen==PADDLE){if(dead)startGame(PADDLE,now);else paddleDir=-paddleDir;}
 else if(screen==DINO){if(dead)startGame(DINO,now);else if(jumps<2){velocity=jumps==0?5.5f:4.6f;jumps++;burst(25,65-dinoY,0xFFE0);}}
 else if(screen==FLAPPY){if(dead)startGame(FLAPPY,now);else birdV=-2.7f;}
 else {clicks++;clickAt=now;}
}
void longPress(uint32_t now){
 clicks=0;rpsClicks=0;
 if(screen!=MENU){
  Screen old=screen;screen=MENU;
  if(old==EYES){menuPage=HOME_PAGE;menuIndex=0;}
  else if(old==RELAX){menuPage=HOME_PAGE;menuIndex=2;}
  else {menuPage=GAMES_PAGE;menuIndex=old==SNAKE?0:old==DINO?1:old==FLAPPY?2:old==PADDLE?3:old==METEORS?4:old==NEON?5:6;}
  dirty=true;return;
 }
 if(menuPage==HOME_PAGE){
  if(menuIndex==0){screen=EYES;changeAt=now+6000;}
  else if(menuIndex==1){menuPage=GAMES_PAGE;menuIndex=0;}
  else if(menuIndex==2){screen=RELAX;relaxStarted=now;relaxSceneAt=now;nextFrame=now;}
  else {menuPage=STYLE_PAGE;menuIndex=0;}
 }else if(menuPage==GAMES_PAGE){
  if(menuIndex==7){menuPage=HOME_PAGE;menuIndex=1;}
  else if(menuIndex==6){screen=RPS;rpsCounting=false;rpsClicks=0;rpsPlayer=-1;rpsBot=-1;rpsWins=0;rpsLosses=0;rpsOutcome=0;}
  else {const Screen games[]={SNAKE,DINO,FLAPPY,PADDLE,METEORS,NEON};startGame(games[menuIndex],now);}
 }else{
  if(menuIndex==0)palette=(palette+1)%COLOR_COUNT;
  else if(menuIndex==1)autoPlay=!autoPlay;
  else if(menuIndex==2){palette=esp_random()%COLOR_COUNT;selectMood(randomMood(),now);}
  else {menuPage=HOME_PAGE;menuIndex=3;}
  menuFeedbackUntil=now+900;
 }
 dirty=true;
}
void button(uint32_t now){
 bool v=digitalRead(9)==LOW;
 if(v!=rawDown){rawDown=v;edgeAt=now;}
 if(now-edgeAt>=30&&down!=rawDown){down=rawDown;
 if(down)pressedAt=now;
 else if(now-pressedAt>=700)longPress(now);
 else shortPress(now);
 }
 if(screen==EYES&&clicks&&!down&&now-clickAt>280){
 if(clicks>=3){offendedUntil=0;partyUntil=0;reactionUntil=0;treatStarted=now;treatUntil=now+8400;selectMood(1,now);changeAt=treatUntil;}
 else if(clicks>=2){
  if(!offendedUntil)moodBeforeOffense=mood;
  treatUntil=0;offenseStarted=now;offendedUntil=now+1200+6000;partyUntil=0;reactionUntil=0;
  selectMood(2,now);changeAt=offendedUntil;
 }
 else if(!offendedUntil&&!treatUntil)selectMood((mood+1)%COUNT,now);
 clicks=0;
 }
}
void drawMenu(){
 memset(frame,0,sizeof(frame));
 uint32_t now=millis();uint16_t c=colors[palette];
 label(4,2,menuPage==HOME_PAGE?"SCEGLI":menuPage==GAMES_PAGE?"GIOCHI":"STILE",c);
 char counter[12];snprintf(counter,sizeof(counter),"%d / %d",menuIndex+1,menuCount());label(119,2,counter,0xBDF7);
 text.drawRoundRect(2,13,156,47,5,0x3186);
 // Faccia dedicata, lontana dal testo: resta animata in ogni sezione.
 int bob=int(sinf(now/270.f)*1.5f);bool blink=now%3100<120;
 for(int x=126;x<=144;x+=18){ellipse(x,28+bob,5,blink?1:6,c);if(!blink)ellipse(x+int(sinf(now/900.f)*2),28+bob,2,3,0);}
 for(int x=-5;x<=5;x++)pixel(135+x,41+bob+int(2*(1-x*x/25.f)),c);
 const char* name=menuPage==HOME_PAGE?homeNames[menuIndex]:menuPage==GAMES_PAGE?gameNames[menuIndex]:styleNames[menuIndex];
 label(8,21,name,c);
 const char* help=menuPage==HOME_PAGE?homeHelp[menuIndex]:menuPage==GAMES_PAGE?gameHelp[menuIndex]:menuIndex==0?"Conferma: nuovo colore":menuIndex==1?(autoPlay?"Attivo: cambia da solo":"In pausa: scegli tu"):menuIndex==2?"Sorteggia faccia e tinta":"Torna alle 4 sezioni";
 label(8,49,help,0xBDF7);
 for(int i=0;i<menuCount();i++)text.fillRoundRect(80-menuCount()*5+i*10,63,7,3,1,i==menuIndex?c:0x2104);
 bool ready=down&&now-pressedAt>=700;
 label(5,73,ready?"RILASCIA PER SCEGLIERE":now<menuFeedbackUntil?"FATTO! TIENI: RIPETI":"CLIC: AVANTI  TIENI: OK",ready?0x07E0:0xBDF7);
 if(down)text.fillRect(0,83,min(160,int((now-pressedAt)*160/700)),3,ready?0x07E0:c);
 present();dirty=false;
}
// Relax: ambienti silenziosi, senza lampeggi e senza pause del respiro.
uint16_t relaxMix(uint16_t a,uint16_t b,float u){
 u=constrain(u,0.f,1.f);
 int r=((a>>11)&31)*(1-u)+((b>>11)&31)*u;
 int g=((a>>5)&63)*(1-u)+((b>>5)&63)*u;
 int bl=(a&31)*(1-u)+(b&31)*u;
 return (r<<11)|(g<<5)|bl;
}
void drawRelax(uint32_t now){
 uint32_t elapsed=now-relaxStarted,phase=elapsed%10000;
 bool inhale=phase<4000;
 float u=inhale?phase/4000.f:1.f-(phase-4000)/6000.f;
 float ease=.5f-.5f*cosf(u*PI),t=elapsed/1000.f;
 const uint16_t sky[]={0x0804,0x0005,0x0023,0x1805};
 const uint16_t glow[]={0xC65F,0x7FF8,0xBFF0,0xFCB2};
 const uint16_t floorColor[]={0x210A,0x012A,0x1145,0x38C8};
 const char* names[]={"CIELO STELLATO","OCEANO CALMO","BOSCO MAGICO","TRAMONTO"};
 uint16_t c=glow[relaxScene],bg=sky[relaxScene];
 for(int y=0;y<H;y++){
  uint16_t shade=relaxMix(bg,floorColor[relaxScene],float(y)/H*.65f);
  for(int x=0;x<W;x++)frame[y*W+x]=shade;
 }
 // Piccoli punti di luce: pulsazioni lente, mai flash.
 for(int i=0;i<23;i++){
  int x=(i*47+11)%158+1,y=14+(i*19)%47;
  float shimmer=.18f+.22f*(.5f+.5f*sinf(t*.6f+i*2.f));
  if(relaxScene==2){x+=int(sinf(t*.22f+i)*3);y+=int(sinf(t*.35f+i)*3);}
  pixel(x,y,relaxMix(bg,c,shimmer));
 }
 if(relaxScene==0){
  ellipse(142,23,7,7,relaxMix(bg,c,.65f));ellipse(145,20,7,7,bg);
 }else if(relaxScene==3){
  ellipse(137,39,12,12,relaxMix(bg,c,.4f));
 }
 // Onde, colline e dune scivolano lentamente dietro alla faccia.
 for(int k=0;k<3;k++)for(int x=0;x<W;x++){
  int y=57+k*5+int(sinf(x*(relaxScene==2?.035f:.055f)+t*(.24f+k*.04f)+k*1.9f)*(2+k));
  pixel(x,y,relaxMix(bg,c,.12f+k*.07f));
 }
 // Un alone respira insieme alla faccina, con espansione sinusoidale.
 int cy=38-int(ease*2),radius=19+int(ease*7);
 for(int ring=0;ring<3;ring++){
  uint16_t rc=relaxMix(bg,c,.12f+ring*.12f);
  text.drawCircle(80,38,radius+6-ring*3,rc);
 }
 // Occhi chiusi, guance delicate e sorriso sincronizzato.
 uint16_t face=relaxMix(c,0xFFFF,.22f);
 int spread=11+int(ease*2);
 for(int side=-1;side<=1;side+=2){
  int cx=80+side*spread;
  for(int x=-5;x<=5;x++){
   int y=cy-3+int((1-x*x/25.f)*2);
   pixel(cx+x,y,face);pixel(cx+x,y+1,face);
  }
  ellipse(cx+side*2,cy+6,3,1,relaxMix(bg,0xF9B0,.38f));
 }
 for(int x=-6;x<=6;x++)pixel(80+x,cy+9+int((1-x*x/36.f)*(2+ease)),face);
 // Petali / gocce lungo il cerchio: indicano il movimento senza contatori urgenti.
 for(int i=0;i<8;i++){
  float a=i*PI/4-PI/2;
  int r=radius+5;
  ellipse(80+int(cosf(a)*r),38+int(sinf(a)*r),1,1,relaxMix(bg,c,.35f+.25f*ease));
 }
 label(inhale?49:52,2,inhale?"INSPIRA  4s":"ESPIRA  6s",face);
 // Barra che sale inspirando e scende espirando, non azzerata di colpo.
 text.fillRoundRect(48,67,64,3,1,relaxMix(bg,c,.15f));
 int width=int(ease*64);
 if(width)text.fillRoundRect(48,67,width,3,1,c);
 char info[28];
 if(now-relaxSceneAt<2400){
  snprintf(info,sizeof(info),"%d/4 %s",relaxScene+1,names[relaxScene]);
 }else if(elapsed<10000){snprintf(info,sizeof(info),"SENZA SFORZO, CON CALMA");}
 else {snprintf(info,sizeof(info),"%02lu:%02lu  %lu RESPIRI",(unsigned long)(elapsed/60000),(unsigned long)(elapsed/1000%60),(unsigned long)(elapsed/10000));}
 label((W-int(strlen(info))*6)/2,73,info,relaxMix(bg,c,.8f));
 // Aiuto iniziale, poi un invito discreto; tenere premuto mostra l'uscita.
 if(down){
  text.fillRect(0,73,W,13,bg);
  bool ready=now-pressedAt>=700;
  label(ready?23:32,75,ready?"RILASCIA: MENU":"TIENI PER USCIRE",face);
  text.fillRect(0,84,min(160,int((now-pressedAt)*160/700)),2,c);
 }else if(elapsed<5500){label(5,81,"CLIC:SCENA  TIENI:MENU",relaxMix(bg,c,.6f));}
 present();
}
// Gioco autonomo: passo a 16 ms, nessuna attesa o modifica al display.
void spaceGame(uint32_t now){
 if(down){gameAt=now;return;}
 if(now-gameAt<FRAME_MS)return;
 gameAt=now;
 if(!dead&&now>=readyUntil){
  shipX+=shipDir*1.15f;
  if(shipX<7){shipX=7;shipDir=1;}if(shipX>152){shipX=152;shipDir=-1;}
  for(auto &r:rocks){
   r.y+=r.speed+min(1.6f,score*.012f);
   if(fabsf(r.x-shipX)<r.radius+5&&fabsf(r.y-65)<r.radius+5){
    if(r.gem){score+=10;shield=1;burst(r.x,r.y,0x07FF);}
    else if(now-spaceHit>1300){
     burst(shipX,65,0xFD20);
     if(shield){shield=0;spaceHit=now;}else dead=true;
    }
    r.y=100;
   }
   if(r.y>85){if(!r.gem&&!dead)score++;r.x=8+esp_random()%144;r.y=-12-esp_random()%45;r.gem=esp_random()%5==0;}
  }
 }
 if(dead&&!savedDeath){savedDeath=true;spaceBest=max(score,spaceBest);records.putUInt("space",spaceBest);}
 memset(frame,0,sizeof(frame));
 for(int i=0;i<35;i++){int x=(i*47+13)%W,y=13+(i*23+now/(i%3?65:30))%62;pixel(x,y,i%3?0x4208:0xBDF7);}
 char info[32];snprintf(info,sizeof(info),"METEORE %d TOP %d",score,max(score,spaceBest));label(1,1,info,0x07FF);
 for(auto &r:rocks){int x=r.x,y=r.y;if(y<16||y>75)continue;
  if(r.gem){text.fillTriangle(x,y-4,x-4,y,x,y+4,0x07FF);text.fillTriangle(x,y-4,x+4,y,x,y+4,0x07FF);pixel(x,y-1,0xFFFF);}
  else{ellipse(x,y,r.radius,r.radius,0x8410);ellipse(x-1,y-1,2,1,0xBDF7);pixel(x+1,y+2,0x4208);}
 }
 int x=shipX;
 if(dead||now-spaceHit>1300||now/90%2){
 text.fillTriangle(x,58,x-6,71,x+6,71,colors[palette]);
 text.fillTriangle(x,61,x-2,66,x+2,66,0xFFFF);
 line(x-2,72,x,74+now/80%3,0,0xFD20);
 if(shield){text.drawCircle(x,65,10,0x03EF);}
 }
 for(auto &p:sparks)if(p.life>0){pixel(p.x,p.y,p.color);p.x+=p.vx*.5f;p.y+=p.vy*.5f;p.life--;}
 if(!dead&&now<readyUntil){label(28,33,"EVITA LE METEORE",0xFFE0);label(28,44,"PRENDI I DIAMANTI",0x07FF);}
 if(dead){text.fillRect(12,27,136,29,0);label(24,31,"PARTITA FINITA");label(20,44,"CLIC: RIPROVA");}
 label(1,78,"CLIC:GIRA LUNGO:MENU",0xBDF7);arcadeFace(now);present();
}
void games(uint32_t now){
 if(down){gameAt=now;return;} // Una pressione lunga non provoca mosse involontarie.
 uint32_t interval=screen==SNAKE?max(85,240-score*6):FRAME_MS;
 if(!dead&&now>=readyUntil&&now-gameAt>=interval){gameAt+=interval;if(now-gameAt>interval*3)gameAt=now;dirty=true;
 if(screen==SNAKE){
 if(turnQueued){direction=(direction+1)%4;turnQueued=false;}
 const int dx[]={1,0,-1,0},dy[]={0,1,0,-1};
 int x=sx[0]+dx[direction],y=sy[0]+dy[direction];
 bool eat=x==foodX&&y==foodY;
 if(x<0||x>=26||y<0||y>=10)dead=true;
 for(int i=0;i<length-(eat?0:1);i++)if(sx[i]==x&&sy[i]==y)dead=true;
 if(!dead){if(eat){length++;score++;bestSnake=max(score,bestSnake);burst(3+x*6,15+y*6,0xFFE0);flashUntil=now+130;}
 for(int i=length-1;i>0;i--){sx[i]=sx[i-1];sy[i]=sy[i-1];}sx[0]=x;sy[0]=y;
 if(length>=240)dead=true;else if(eat)food();}
 }else if(screen==PADDLE){
 paddleX=constrain(paddleX+paddleDir*2.5f*GAME_STEP,0.f,130.f);
 ballX+=ballVX*GAME_STEP;ballY+=ballVY*GAME_STEP;
 if(ballX<3){ballX=3;ballVX=fabsf(ballVX);}
 if(ballX>156){ballX=156;ballVX=-fabsf(ballVX);}
 if(ballY<15){ballY=15;ballVY=fabsf(ballVY);}
 bool hit=false;
 for(int r=0;r<3&&!hit;r++)for(int c=0;c<10&&!hit;c++)if(bricks[r][c]&&ballX+2>=c*16+1&&ballX-2<=c*16+14&&ballY+2>=18+r*7&&ballY-2<=23+r*7){
 bricks[r][c]=false;ballVY=-ballVY;score+=5;bestPaddle=max(score,bestPaddle);burst(ballX,ballY,colors[(r+stage)%COLOR_COUNT]);hit=true;}
 bool any=false;for(int r=0;r<3;r++)for(int c=0;c<10;c++)any|=bricks[r][c];
 if(!any){stage++;newBricks();ballX=80;ballY=49;ballVY=1.6f;burst(80,40,0xFFE0);}

 if(ballVY>0&&ballY>=67&&ballY<=73&&ballX>=paddleX-2&&ballX<=paddleX+32){
 ballY=67;score++;bestPaddle=max(score,bestPaddle);
 ballVY=-min(3.6f,1.6f+score*.1f);
 ballVX=constrain((ballX-(paddleX+15))*.17f,-2.7f,2.7f);
 }
 if(ballY>76)dead=true;
 }else if(screen==FLAPPY){
 birdV+=.19f*GAME_STEP;birdY+=birdV*GAME_STEP;
 pipeX-=(1.8f+min(1.2f,score*.06f))*GAME_STEP;
 if(!pipePassed&&pipeX+15<30){pipePassed=true;score++;bestFlappy=max(score,bestFlappy);burst(33,birdY,0xFFE0);}
 if(pipeX < -16){pipeX=165;gapY=32+esp_random()%24;pipePassed=false;}
 if(birdY-3<12||birdY+3>73)dead=true;
 if(pipeX<37&&pipeX+15>29&&(birdY-3<gapY-max(12,18-score/8)||birdY+3>gapY+max(12,18-score/8)))dead=true;
 }else{
 dinoY=max(0.f,dinoY+velocity*GAME_STEP);velocity-=.42f*GAME_STEP;if(dinoY==0){velocity=0;jumps=0;}
 obstacleX-=(2.4f+min(2.5f,score*.13f))*GAME_STEP;
 if(obstacleX < -12){obstacleX=165+esp_random()%65;obstacleHeight=12+esp_random()%12;obstacleWidth=7+esp_random()%6;score++;bestDino=max(score,bestDino);burst(25,55,0xFFE0);}
 if(obstacleX<34&&obstacleX+obstacleWidth>20&&dinoY<obstacleHeight-1)dead=true;
 }
 }
 if(!dead&&now<readyUntil)dirty=true;
 if(dead&&!savedDeath){savedDeath=true;saveRecords();burst(80,40,0xF800);}
 bool particles=false;for(auto &p:sparks)if(p.life>0)particles=true;
 if((particles||screen==SNAKE)&&!dead&&now-renderAt>=FRAME_MS)dirty=true;
 if(particles&&now-renderAt>=FRAME_MS)dirty=true;
 if(!dirty||now-renderAt<FRAME_MS)return;
 renderAt=now;
 memset(frame,0,sizeof(frame));char buf[32];snprintf(buf,sizeof(buf),"%s %d  TOP %d",screen==SNAKE?"SNAKE":screen==FLAPPY?"VOLO":screen==PADDLE?"RACC.":"DINO",score,screen==SNAKE?bestSnake:screen==FLAPPY?bestFlappy:screen==PADDLE?bestPaddle:bestDino);label(2,1,buf,colors[palette]);
 if(screen==SNAKE){
 text.drawRect(0,12,158,62,0x4208);
 for(int y=0;y<10;y++)for(int x=0;x<26;x++)if((x+y)%2==0)text.fillRect(1+x*6,13+y*6,6,6,0x0821);
 for(int i=length-1;i>=0;i--)text.fillRect(1+sx[i]*6,13+sy[i]*6,5,5,i==0?0xFFFF:colors[palette]);
 ellipse(3+foodX*6,15+foodY*6,2,2,now/200%2?0xF800:0xFD20);
 pixel(4+foodX*6,12+foodY*6,0x07E0);
 const int ex[]={1,0,-1,0},ey[]={0,1,0,-1};pixel(3+sx[0]*6+ex[direction],15+sy[0]*6+ey[direction],0);
 }else if(screen==PADDLE){
 text.drawRect(0,12,160,65,0x4208);
 for(int r=0;r<3;r++)for(int c=0;c<10;c++)if(bricks[r][c]){text.fillRoundRect(c*16+1,18+r*7,14,5,1,colors[(r+stage)%COLOR_COUNT]);text.drawFastHLine(c*16+3,18+r*7,9,0xFFFF);}
 line(ballX-ballVX*2,ballY-ballVY*2,ballX,ballY,0,0x8410);
 text.fillRoundRect((int)paddleX,70,30,4,2,colors[palette]);
 ellipse((int)ballX,(int)ballY,2,2,0xFFFF);
 }else if(screen==FLAPPY){
 text.drawFastHLine(0,12,160,0x4208);text.drawFastHLine(0,74,160,0x4208);
 for(int i=0;i<5;i++){int cx=(i*39+320-int(now/90)%195)%195-15;ellipse(cx,24+(i%3)*10,9,3,0x18C3);}
 int px=(int)pipeX; int gap=max(12,18-score/8);
 text.fillRect(px,13,15,max(0,gapY-gap-13),0x07E0);
 text.fillRect(px,gapY+max(12,18-score/8),15,max(0,74-gapY-gap),0x07E0);
 text.fillRect(px-2,gapY-gap-3,19,3,0x07E0);
 text.fillRect(px-2,gapY+max(12,18-score/8),19,3,0x07E0);
 ellipse(33,(int)birdY,4,3,colors[palette]);
 text.fillTriangle(37,birdY,41,birdY+1,37,birdY+2,0xFD20);pixel(35,(int)birdY-1,0);line(29,(int)birdY,31,(int)birdY+2,0,0xFFFF);
 }else{
 for(int i=0;i<4;i++){int cx=(i*51+320-int(now/120)%204)%204-18;ellipse(cx,24+i%2*9,10,3,0x2945);}
 line(0,68,159,68,0,0x7BEF);
 int y=66-(int)dinoY;
 text.fillRect(20,y-14,10,12,colors[palette]);text.fillRect(26,y-22,11,10,colors[palette]);pixel(33,y-20,0);
 line(20,y-9,15,y-13,1,colors[palette]);
 int stride=dead?0:(now/100)%2*3;
 text.fillRect(21,y-2,3,3+stride,colors[palette]);text.fillRect(28,y-2,3,6-stride,colors[palette]);
 text.fillRect((int)obstacleX,68-obstacleHeight,obstacleWidth,obstacleHeight,0x07E0);
 line(obstacleX-3,57,obstacleX+obstacleWidth+3,57,1,0x07E0);
 pixel(obstacleX+2,70-obstacleHeight,0xBFF7);
 for(int i=0;i<5;i++)pixel((i*37+320-(now/22)%160)%160,72,0x7BEF);
 }
 for(auto &p:sparks)if(p.life>0){pixel(p.x,p.y,p.color);p.x+=p.vx*GAME_STEP;p.y+=p.vy*GAME_STEP;p.vy+=.06f*GAME_STEP;p.life--;dirty=true;}
 if(!dead&&now<readyUntil){text.fillRoundRect(43,28,74,26,4,0);text.drawRoundRect(43,28,74,26,4,colors[palette]);char count[4];snprintf(count,sizeof(count),"%d",int((readyUntil-now+499)/500));label(75,33,count,0xFFE0,2);}
 if(dead){text.fillRect(12,27,136,29,0);text.drawRect(12,27,136,29,colors[palette]);label(24,31,"PARTITA FINITA");label(20,44,"CLIC: RIPROVA");}
 label(1,78,screen==SNAKE?"CLIC:DESTRA LUNGO:MENU":screen==PADDLE?"CLIC:GIRA LUNGO:MENU":screen==FLAPPY?"CLIC:VOLA LUNGO:MENU":"CLIC:2 SALTI LUNGO:MENU",0xBDF7);
 arcadeFace(now);present();dirty=false;
}

void setup(){
 // BOOT: ingresso soltanto, mai pilotare il pin di avvio.
 text.setTextWrap(false);
 records.begin("arcade",false);
 spaceBest=records.getUInt("space",0);
 neonBest=records.getUInt("neon",0);
 bestSnake=records.getUInt("snake",0);bestDino=records.getUInt("dino",0);bestFlappy=records.getUInt("fly",0);bestPaddle=records.getUInt("paddle",0);
 pinMode(9,INPUT_PULLUP);
 pinMode(22,OUTPUT);digitalWrite(22,LOW);
 pinMode(4,OUTPUT);digitalWrite(4,HIGH);
 SPI.begin(7,-1,6,14);
 lcd.init(172,320);lcd.setRotation(1);lcd.invertDisplay(true);lcd.fillScreen(0);
 memset(previous,0xFF,sizeof(previous));analogWrite(22,102);
 changeAt=millis()+3500;
}
void loop(){uint32_t now=millis();
 if(offendedUntil && (int32_t)(now-offendedUntil)>=0){offendedUntil=0;selectMood(moodBeforeOffense,now);}
 if(treatUntil && (int32_t)(now-treatUntil)>=0){treatUntil=0;selectMood(1,now);}
 button(now);
 if(screen==EYES){
 if(autoPlay&&!offendedUntil&&!treatUntil&&(int32_t)(now-changeAt)>=0&&!down&&!clicks)selectMood(randomMood(),now);
 if((int32_t)(now-blinkAt)>=0){blinkStart=now;blinkAt=now+2200+esp_random()%2600;}
 if((int32_t)(now-nextFrame)>=0){nextFrame+=FRAME_MS;if((int32_t)(now-nextFrame)>int32_t(FRAME_MS*3))nextFrame=now+FRAME_MS;draw(now);}
 }else if(screen==RELAX){if((int32_t)(now-nextFrame)>=0){nextFrame=now+FRAME_MS;drawRelax(now);}}
 else if(screen==MENU){if(dirty||now-renderAt>=FRAME_MS){renderAt=now;drawMenu();}}
 else if(screen==RPS)rpsGame(now);
 else if(screen==NEON)neonGame(now);
 else if(screen==METEORS)spaceGame(now);
 else games(now);
 delay(1);
}