// FINAL VERSION AS OF TUESDAY NIGHT 9/29
// CIGARETTE SIMULATOR for XIAO RP2040 + SSD1306 (128x64) + 6 capacitive touch pads
// Explore the map, find 5 cigarettes, keep STRESS and CANCER both under 100.
// Controls: turn L/R, walk fwd/back, pickup pad = pick up, smoke pad = smoke (smoke pad also advances menus).

#include <U8g2lib.h> //OLED graphics library
#include <math.h>

// SSD1306 128x64 display, full frame buffer, hardware I2C, no reset pin. (referenced from Quentin's Tetris code)
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

#ifndef WHITE
#define WHITE 1
#endif
#ifndef BLACK
#define BLACK 0
#endif

#define SCREEN_W 128
#define SCREEN_H 64
#define VIEW_H   50      // height of the 3D view; bottom 14 px are the stress/cancer bars
#define HORIZON  25      // middle of the 3D view
float zbuf[SCREEN_W];    // wall distance for every screen column (sprites use this to hide behind walls)

// touchpad inputs
static const uint8_t PIN_LEFT    = D1;   // turn left
static const uint8_t PIN_RIGHT   = D7;   // turn right
static const uint8_t PIN_BACK    = D8;   // walk back
static const uint8_t PIN_FORWARD = D0;   // forward
static const uint8_t PIN_SMOKE   = D9;   // smoke
static const uint8_t PIN_PICKUP  = D10;  // pickup

// TOUCH (Quentin's code; adapted for variable names)

// Capacitive touch by RC timing: discharge the pin, then let the internal
// pull-up charge it and count how long it takes to read HIGH.
// A finger adds capacitance, so the charge time gets longer.
#define N_TOUCH 6
#define THRESHOLD 6     // <-- TUNE: higher value = less sensitive

// Pins listed in the same order as the T_* indices below
int touch_pins[N_TOUCH]   = { PIN_LEFT, PIN_RIGHT, PIN_BACK, PIN_FORWARD, PIN_SMOKE, PIN_PICKUP };
int touch_values[N_TOUCH] = { 0, 0, 0, 0, 0, 0 };   // Raw charge-time counts

// Touch state this frame and last frame (used to detect a fresh press)
bool pin_touched_now[N_TOUCH]  = { false, false, false, false, false, false };
bool pin_touched_past[N_TOUCH] = { false, false, false, false, false, false };

// Thin wrappers around digitalWrite/digitalRead.
// If your core has digitalWriteFast, you can swap it in here in one place.
static inline void DW(uint8_t p, uint8_t v) { digitalWrite(p, v); }
static inline int  DR(uint8_t p) { return digitalRead(p); }

// Measure all touch pads once and update their pressed/held state
void update_touch() {
  int t;
  const int t_max = 200;   // Cap on the measurement so a stuck pin can't hang the loop
  int p;

  for (int i = 0; i < N_TOUCH; i++) {
    p = touch_pins[i];

    // Step 1: drive the pin LOW to discharge the pad
    pinMode(p, OUTPUT);
    DW(p, LOW);

    // Step 2: give it a moment to fully discharge
    delayMicroseconds(25);

    // Step 3: switch to input with pull-up, which starts charging the pad
    pinMode(p, INPUT_PULLUP);

    // Step 4: count loop iterations until the pin reads HIGH (or we hit the cap)
    t = 0;
    while (!DR(p) && t < t_max) {
      t++;
    }
    touch_values[i] = t;

    // Step 5: remember last state, then compare against the threshold
    pin_touched_past[i] = pin_touched_now[i];
    pin_touched_now[i]  = (touch_values[i] > THRESHOLD);
  }
}

// True for as long as the pad is being touched
bool touchHeld(int idx) { return pin_touched_now[idx]; }
// True only on the single frame where the touch begins (rising edge)
bool touchPressed(int idx) { return pin_touched_now[idx] && !pin_touched_past[idx]; }

// Named indices into the touch arrays, for readability
enum {
  T_LEFT = 0,
  T_RIGHT = 1,
  T_BACK = 2,
  T_FORWARD = 3,
  T_SMOKE = 4,
  T_PICKUP = 5
};

//END QUENTIN'S CODE



// HELPER FUNCTIONS
// Small wrappers so the drawing code can say "draw this in WHITE / BLACK".

inline void gPx(int x, int y, uint8_t c)                    { u8g2.setDrawColor(c); u8g2.drawPixel(x, y); } // draw pixel at (x, y)
inline void gLine(int x0, int y0, int x1, int y1, uint8_t c){ u8g2.setDrawColor(c); u8g2.drawLine(x0, y0, x1, y1); } // draw line from (x0, y0) to (x1, y1)
inline void gFrame(int x, int y, int w, int h, uint8_t c)   { u8g2.setDrawColor(c); u8g2.drawFrame(x, y, w, h); } // empty rectangle with top left coords (x,y) and dimensions w,h
inline void gFill(int x, int y, int w, int h, uint8_t c)    { u8g2.setDrawColor(c); u8g2.drawBox(x, y, w, h); } //same as above but filled
inline void gCircle(int x, int y, int r, uint8_t c)         { u8g2.setDrawColor(c); u8g2.drawCircle(x, y, r); } // circle with center (x,y) and radius r

// Text. With setFontPosTop(), the y you pass is the TOP of the text.
void useSmallFont() { u8g2.setFont(u8g2_font_5x7_tr);   u8g2.setFontPosTop(); }   // normal text
void useBigFont()   { u8g2.setFont(u8g2_font_10x20_tr); u8g2.setFontPosTop(); }   // titles

void gText(int x, int y, const char* s) { u8g2.setDrawColor(1); u8g2.drawStr(x, y, s); }
void gTextCenter(const char* s, int y) {
  u8g2.setDrawColor(1);
  u8g2.drawStr((SCREEN_W - u8g2.getStrWidth(s)) / 2, y, s);
}



//THE MAP 

#define MAP_W 12
#define MAP_H 12

// 1 = wall, 0 = floor. This is a 2d view from above
// Keep the outer border solid so rays can never leave the map

const uint8_t worldMap[MAP_H][MAP_W] = {
  {1,1,1,1,1,1,1,1,1,1,1,1},
  {1,0,0,0,0,0,0,0,0,0,0,1},
  {1,0,0,0,0,0,0,0,0,0,0,1},
  {1,0,0,1,1,0,0,0,1,0,0,1},
  {1,0,0,1,0,0,0,0,1,0,0,1},
  {1,0,0,0,0,0,0,0,0,0,0,1},
  {1,0,0,0,0,1,1,0,0,0,0,1},
  {1,0,0,0,0,0,0,0,0,0,0,1},
  {1,0,1,0,0,0,0,0,0,1,0,1},
  {1,0,1,0,0,0,0,0,0,0,0,1},
  {1,0,0,0,0,0,0,0,0,0,0,1},
  {1,1,1,1,1,1,1,1,1,1,1,1}
};


// brick texture (8x8 pattern, bit7 = leftmost pixel, 1 = white, 0 = black)
const uint8_t TEX_BRICK[8] = {
  0x00,                    // horizontal mortar line
  0xEF, 0xEF, 0xEF,        // brick row (one thin vertical mortar line)
  0x00,                    // horizontal mortar line
  0xFE, 0xFE, 0xFE         // next brick row, offset by half a brick
};

// Cigarette sprite! (mask = drawn pixels, col = white pixels)
const uint8_t SPR_MASK[8] = {0x28, 0x28, 0x7E, 0x7E, 0x7E, 0x7E, 0x7E, 0x00};
const uint8_t SPR_COL[8]  = {0x28, 0x28, 0x7E, 0x42, 0x5A, 0x42, 0x7E, 0x00};



// PLAYER STATE
float posX = 2.5, posY = 2.5;   // spawn in the center of a block
float angle = 0;                // facing direction in radians

const float MOVE_SPEED = 2.0;   // walking speed (blocks/s)
const float TURN_SPEED = 2.0;   // turning speed (radians/s)



// GAME PARAMS
#define NUM_CIGS        5
#define STRESS_START    10.0f   // starting stress (0..100)
#define STRESS_RATE     1.0f    // stress gained per second
#define STRESS_PER_CIG  35.0f   // stress removed per cigarette
#define LUNG_PER_CIG    45.0f   // cancer meter added per cigarette
#define LUNG_DECAY      1.6f    // cancer meter recovered per second
#define SMOKE_MS        2800    // length of smoking animation
#define PICKUP_RANGE    1.1f    // how close you must be (in blocks)

// cig locations where x,y are map coordS
struct Cig { float x, y; bool got; };

Cig cigs[NUM_CIGS] = {
  {5.5,  4.5,  false},
  {9.5,  1.5,  false},
  {9.5,  6.5,  false},
  {3.5,  9.5,  false},
  {10.5, 10.5, false}
};


// stress/health meters and inventory params
float stress = STRESS_START;
float lung   = 0;
int carried = 0;   // cigarettes in your pocket
int found   = 0;   // total picked up so far
int smoked  = 0;   // total smoked so far

// popup messages to hint players
char msg[20] = "";
uint32_t msgUntil = 0;
void showMsg(const char* m) { strncpy(msg, m, 19); msg[19] = 0; msgUntil = millis() + 1200; }

// game states
enum { S_TITLE = 1, S_INSTR, S_PLAY, S_END_STRESS, S_END_CANCER, S_WIN }; // title, instructions, game, stress ending, cancer ending, win screen
int state = S_TITLE;
uint32_t stateSince = 0;   // when we entered the current state
uint32_t gameStart = 0;    // when the current run began
uint32_t endElapsed = 0;   // how long the run lasted
int pendingEnd = 0;        // upcoming ending
uint32_t pendingAt = 0;

// hand action right now (only appears while smoking)
enum Act { A_NONE, A_SMOKE };
Act act = A_NONE;
uint32_t actStart = 0;


// ANIMATIONS ANIMATIONS ANIMATIONS
// claude helped me design this... the animations were too complicated to do by hand :(
// cigarette smoke animations: multiple puffs
struct Puff { float x, y, r, age, life; bool on; };
Puff puffs[12]; // bunch o circles to represent smoke

void spawnPuff(float x, float y, float r0, float life) {
  for (int i = 0; i < 12; i++)
    if (!puffs[i].on) { puffs[i] = {x, y, r0, 0, life, true}; return; }
}
void updatePuffs(float dt) {
  for (int i = 0; i < 12; i++) {
    if (!puffs[i].on) continue;
    Puff &p = puffs[i];
    p.age += dt;
    if (p.age >= p.life || p.y < -10) { p.on = false; continue; }
    p.y -= (6.0f + p.r * 1.2f) * dt;
    p.x += sinf(p.age * 5.0f + i) * 6.0f * dt;
    if (p.r < 9) p.r += 5.0f * dt;
  }
}
void drawPuffs() {
  for (int i = 0; i < 12; i++) {
    if (!puffs[i].on) continue;
    if (puffs[i].r < 2) gPx((int)puffs[i].x, (int)puffs[i].y, WHITE);
    else gCircle((int)puffs[i].x, (int)puffs[i].y, (int)puffs[i].r, WHITE);
  }
}


// SPRITES FOR CIG PACKS
void renderSprites() {
  float dirX = cosf(angle), dirY = sinf(angle);
  const float FOV = 0.66f;
  float planeX = -dirY * FOV, planeY = dirX * FOV;
  float invDet = 1.0f / (planeX * dirY - dirX * planeY);

  for (int i = 0; i < NUM_CIGS; i++) {
    if (cigs[i].got) continue;                           // already picked up

    // Position relative to the player, then rotated into camera space
    float sx = cigs[i].x - posX, sy = cigs[i].y - posY;
    float tx = invDet * (dirY * sx - dirX * sy);         // sideways offset
    float ty = invDet * (-planeY * sx + planeX * sy);    // depth (distance ahead)
    if (ty < 0.15f) continue;                            // behind you or too close

    int screenX = (int)((SCREEN_W / 2) * (1.0f + tx / ty));
    int fullH = (int)(VIEW_H / ty);                      // height a wall would be at this depth
    int size = (int)(fullH * 0.32f);                     // sprites are small
    if (size < 3) size = 3;
    int bottom = HORIZON + fullH / 2 - 1;                // sits on the floor
    int top = bottom - size + 1;
    int left = screenX - size / 2;

    for (int dx = 0; dx < size; dx++) {
      int x = left + dx;
      if (x < 0 || x >= SCREEN_W || ty >= zbuf[x]) continue;   // off-screen or behind a wall
      int tc = dx * 8 / size;
      uint8_t bit = 0x80 >> tc;
      for (int dy = 0; dy < size; dy++) {
        int y = top + dy;
        if (y < 0 || y >= VIEW_H) continue;
        int tr = dy * 8 / size;
        if (!(SPR_MASK[tr] & bit)) continue;
        gPx(x, y, (SPR_COL[tr] & bit) ? WHITE : BLACK);
      }
    }
  }
}


// HAND
float smooth(float t) { if (t < 0) t = 0; if (t > 1) t = 1; return t * t * (3 - 2 * t); }
float lerpf(float a, float b, float t) { return a + (b - a) * t; }

void drawCigStick(int x0, int y0, int x1, int y1) {
  gLine(x0 - 1, y0, x1 - 1, y1, BLACK);
  gLine(x0 + 2, y0, x1 + 2, y1, BLACK);
  gLine(x0, y0, x1, y1, WHITE);
  gLine(x0 + 1, y0, x1 + 1, y1, WHITE);
}

// A fixed hand holding a cigarette. (hx, hy) is the hand's position on screen.
void drawHand(int hx, int hy) {
  // wrist and palm
  gFill(hx - 7, hy + 3, 16, 34, BLACK);
  gFrame(hx - 7, hy + 3, 16, 34, WHITE);
  gFill(hx - 10, hy - 8, 22, 16, BLACK);
  gFrame(hx - 10, hy - 8, 22, 16, WHITE);
  // three fingers
  for (int i = 0; i < 3; i++) {
    gFill(hx - 9 + i * 7, hy - 13, 6, 7, BLACK);
    gFrame(hx - 9 + i * 7, hy - 13, 6, 7, WHITE);
  }
  // thumb
  gFill(hx + 8, hy - 6, 6, 9, BLACK);
  gFrame(hx + 8, hy - 6, 6, 9, WHITE);
  // cigarette
  drawCigStick(hx - 9, hy - 8, hx - 18, hy - 22);
}

// Smoking: the hand rises from the bottom of the screen, holds, then lowers.
void drawAction(uint32_t now) {
  if (act != A_SMOKE) return;

  float t = (now - actStart) / (float)SMOKE_MS;
  int hx, hy;
  bool lit = false;
  if (t < 0.22f) {                         // rise
    float k = smooth(t / 0.22f);
    hx = (int)lerpf(88, 64, k); hy = (int)lerpf(80, 44, k);
  } else if (t < 0.85f) {                  // hold at the mouth
    hx = 64; hy = 44; lit = true;
  } else {                                 // lower
    float k = smooth((t - 0.85f) / 0.15f);
    hx = (int)lerpf(64, 88, k); hy = (int)lerpf(44, 80, k); lit = true;
  }

  drawHand(hx, hy);

  if (lit) {
    int tx = hx - 18, ty = hy - 22;        // tip of the cigarette emits smoke
    gCircle(tx, ty, 3, WHITE);
    static uint32_t lastWisp = 0;
    if (now - lastWisp > 300 && ty < VIEW_H) { spawnPuff(tx, ty - 2, 1, 1.1f); lastWisp = now; }
  }
}




// RAYCASTING!!!!

// dotted floor texture below the horizon for feeling of depth
void drawFloor(int x, int fromY) {
  for (int y = fromY; y < VIEW_H; y++) {
    int r = y - HORIZON;
    uint8_t m = r < 5 ? 7 : (r < 12 ? 3 : 1);
    if ((((x + y * 2) & m) == 0)) gPx(x, y, WHITE);
  }
}

void renderWalls() {
  // Facing direction as a vector
  float dirX = cos(angle), dirY = sin(angle);

  // camera plane is a vector perpendicular to direction. field of view (length) is set to 66 degrees for now. turn it up to get a higher fov
  float planeX = -dirY * 0.66, planeY = dirX * 0.66;

  // One ray per screen column
  for (int x = 0; x < 128; x++) {

    // build ray for this column
    // cameraX goes from left edge (-1) to right edge (+1)
    float cameraX = 2.0 * x / 128 - 1.0;
    // The ray direction is straight ahead plus a sideways offset from the plane.
    float rayDirX = dirX + planeX * cameraX;
    float rayDirY = dirY + planeY * cameraX;

    
    int mapX = (int)posX;   // this is the cell
    int mapY = (int)posY;   // you're standing in (x,y)
    // How far the ray travels to cross from one vertical/horizontal line to the next (on the grid)
    // line to the next. The 1e30 (infinity) avoids dividing by zero for rays that are paralll.
    float deltaDistX = (rayDirX == 0) ? 1e30 : fabs(1.0 / rayDirX); // if your line is nearly parallel to a vertical line, distX will be huge
    float deltaDistY = (rayDirY == 0) ? 1e30 : fabs(1.0 / rayDirY); // vice versa for parallel to horizontal line

    //step dir
    int stepX, stepY;               // which way the ray walks through the grid
    float sideDistX, sideDistY;     // distance from the player to the next x / y grid line
    if (rayDirX < 0) { stepX = -1; sideDistX = (posX - mapX) * deltaDistX; }
    else             { stepX =  1; sideDistX = (mapX + 1.0 - posX) * deltaDistX; }
    if (rayDirY < 0) { stepY = -1; sideDistY = (posY - mapY) * deltaDistY; }
    else             { stepY =  1; sideDistY = (mapY + 1.0 - posY) * deltaDistY; }

    // hop from grid line to grid line until we enter a wall
    int side = 0;         // 0 = we hit a vertical wall face, 1 = a horizontal one
    bool hit = false;
    while (!hit) {
      // Advance whichever grid line is closer.
      if (sideDistX < sideDistY) { sideDistX += deltaDistX; mapX += stepX; side = 0; }
      else                       { sideDistY += deltaDistY; mapY += stepY; side = 1; }
      // Did we just step into a wall cell?
      if (worldMap[mapY][mapX] > 0) hit = true;
    }

    // distance to the wall **** IMPORTANT **** THIS TOOK ME SO LONG
    // We measure distance along the direction we're FACING (perpendicular
    // distance), not the straight line to the wall. otherwise we get a crazy fisheye effect
    float perpDist = (side == 0) ? (sideDistX - deltaDistX)
                                 : (sideDistY - deltaDistY);
    if (perpDist < 0.05) perpDist = 0.05;   // avoid touching wall

    zbuf[x] = perpDist; // record distance so sprites can hide behind walls

    // height of wall
    int lineH = (int)(VIEW_H / perpDist);
    if (lineH < 1) lineH = 1;
    int top = HORIZON - lineH / 2;
    int y0 = top < 0 ? 0 : top;
    int y1 = top + lineH - 1;
    if (y1 > VIEW_H - 1) y1 = VIEW_H - 1;

    // brick texture: which texture did the ray land on
    float wallX = (side == 0) ? (posY + perpDist * rayDirY) : (posX + perpDist * rayDirX);
    wallX -= floorf(wallX);
    int texX = (int)(wallX * 8);
    if (texX > 7) texX = 7;
    if ((side == 0 && rayDirX > 0) || (side == 1 && rayDirY < 0)) texX = 7 - texX;

    // i honestly regret doing the texture because it made this part so much more complicated for walls that look kind of mid
    int32_t step = (8 << 16) / lineH;             // texture rows per screen pixel
    int32_t texPos = (int32_t)(y0 - top) * step;
    uint8_t mask = 0x80 >> texX;
    bool far = perpDist > 4.5f;

    for (int y = y0; y <= y1; y++) {
      int texY = (texPos >> 16) & 7;
      texPos += step;
      if (!(TEX_BRICK[texY] & mask)) continue;            // black mortar line
      if (side == 1 && ((x + y) & 1)) continue;           // dotted = darker side faces
      if (far && (((x + y) & 3) == 0)) continue;          // distance fade
      gPx(x, y, WHITE);
    }
    drawFloor(x, (y1 + 1 > HORIZON) ? y1 + 1 : HORIZON);
  }

}




// STATS BAR

void drawBar(int y, const char* label, float v) {
  gText(0, y, label);
  gFrame(38, y, 90, 6, WHITE);                        // bar outline
  int w = (int)(v / 100.0f * 88.0f);
  if (w > 88) w = 88;
  bool blink = v > 80 && ((millis() / 180) & 1);      // flash when dangerously high
  if (!blink && w > 0) gFill(39, y + 1, w, 4, WHITE);
}

void drawHUD() {
  useSmallFont();
  char buf[12];

  // top-left: cigarettes in inventory
  gFill(0, 0, 30, 10, BLACK);
  gLine(2, 4, 11, 4, WHITE);
  gLine(2, 5, 11, 5, WHITE);
  if ((millis() / 250) & 1) gPx(13, 4, WHITE);
  snprintf(buf, sizeof(buf), "x%d", carried);
  gText(17, 1, buf);

  // top-right: found count
  gFill(98, 0, 30, 10, BLACK);
  snprintf(buf, sizeof(buf), "%d/%d", found, NUM_CIGS);
  gText(104, 1, buf);

  // short message (e.g. "Nothing here")
  if (millis() < msgUntil) {
    int w = u8g2.getStrWidth(msg) + 4;
    gFill((SCREEN_W - w) / 2, 12, w, 10, BLACK);
    gText((SCREEN_W - w) / 2 + 2, 14, msg);
  }

  // bottom bars
  gFill(0, VIEW_H, SCREEN_W, SCREEN_H - VIEW_H, BLACK);
  drawBar(50, "STRESS", stress);
  drawBar(57, "CANCER", lung);
}


// SCREENS

void drawTitle(uint32_t now) { // TITLE SCREEN
  u8g2.clearBuffer();
  useBigFont();
  gTextCenter("CIGARETTE", 1); // title in two lines
  gTextCenter("SIMULATOR", 22);
  gFill(28, 45, 56, 4, WHITE);
  gFill(28, 45, 14, 4, BLACK); // cigarette
  gFrame(28, 45, 14, 4, WHITE);
  if ((now / 200) & 1) gFill(84, 44, 3, 6, WHITE); else gFill(84, 45, 2, 4, WHITE); // smoke effect
  static uint32_t lastP = 0;
  if (now - lastP > 260) { spawnPuff(86, 43, 1, 1.5f); lastP = now; }
  drawPuffs();
  useSmallFont();
  if ((now / 500) & 1) gTextCenter("Press Q1", 56); // blinking indicator
  u8g2.sendBuffer();
}

void drawInstructions(uint32_t now) { // INSTRUCTIONS SCREEN
  u8g2.clearBuffer();
  useSmallFont();
  gText(0, 0,  "You're feeling pretty");
  gText(0, 9,  "stressed today, maybe"); // flavor text
  gText(0, 18, "5 cigarettes would");
  gText(0, 27, "help.");
  gTextCenter("Q0 - pickup", 38); // controls
  gTextCenter("Q1 - smoke", 47);
  if ((now / 500) & 1) gText(0, 56, "Press Q1 to start"); // action item
  u8g2.sendBuffer();
}

void drawEnd(uint32_t now) { // GAME SCREEN!!
  u8g2.clearBuffer();
  char buf[28];
  int secs = endElapsed / 1000;
  bool blink = (now / 500) & 1;
  if (state == S_END_STRESS) { // die if you get too stressed
    useSmallFont();
    gTextCenter("YOU DIED OF", 2);
    useBigFont();
    gTextCenter("STRESS", 15);
    useSmallFont();
    snprintf(buf, sizeof(buf), "Smoked %d/%d  %d:%02d", smoked, NUM_CIGS, secs / 60, secs % 60); // states at end of game
    gTextCenter(buf, 42);
    if (blink) gTextCenter("Press Q1 to retry", 56);
  } else if (state == S_END_CANCER) { // died of cancer (smoked too fast)
    useSmallFont();
    gTextCenter("YOU DIED OF", 1);
    useBigFont();
    gTextCenter("LUNG", 11);
    gTextCenter("CANCER", 31);
    useSmallFont();
    if (blink) gTextCenter("Press Q1 to retry", 56);
  } else {
    useSmallFont();
    gTextCenter("CONGRATULATIONS!", 2); // win state
    useBigFont();
    gTextCenter("YOU WON", 13);
    useSmallFont();
    snprintf(buf, sizeof(buf), "Time %d:%02d", secs / 60, secs % 60);
    gTextCenter(buf, 37);
    if (blink) {
      gTextCenter("Press Q1", 46);
      gTextCenter("to play again", 55); // repeat
    }
  }
  u8g2.sendBuffer();
}


// GAME LOGIC

void startGame() {
  posX = 2.5; posY = 2.5; angle = 0;
  stress = STRESS_START; lung = 0;
  carried = 0; found = 0; smoked = 0;
  for (int i = 0; i < NUM_CIGS; i++) cigs[i].got = false;
  act = A_NONE; pendingEnd = 0;
  msg[0] = 0; msgUntil = 0;
  for (int i = 0; i < 12; i++) puffs[i].on = false;
  gameStart = millis();
  state = S_PLAY; stateSince = millis();
}

// Pick up the nearest cigarette in range (instant, no hand animation)
void tryPick() {
  for (int i = 0; i < NUM_CIGS; i++) {
    if (cigs[i].got) continue;
    float dx = cigs[i].x - posX, dy = cigs[i].y - posY;
    if (dx * dx + dy * dy < PICKUP_RANGE * PICKUP_RANGE) {
      cigs[i].got = true; carried++; found++;
      showMsg("Picked up cig x1");      // <-- add this line
      return;
    }
  }
  showMsg("Nothing here");
}

// Smoke one cigarette
void trySmoke(uint32_t now) {
  if (carried <= 0) { showMsg("No cigarettes!"); return; }
  carried--; smoked++;
  stress -= STRESS_PER_CIG;
  lung += LUNG_PER_CIG;
  if (lung >= 60 && lung < 100) showMsg("*cough*");   // warning: getting close to cancer
  act = A_SMOKE; actStart = now;
}

// End the animation when its time is up
void updateAction(uint32_t now) {
  if (act == A_SMOKE && now - actStart >= SMOKE_MS) act = A_NONE;
}

// dt = seconds since the last frame
void playFrame(uint32_t now, float dt) {
  // meters: stress always creeps up, lungs slowly recover
  if (!pendingEnd) {
    stress += STRESS_RATE * dt;
    lung -= LUNG_DECAY * dt;
    if (lung < 0) lung = 0;
  }

  // one action per touch (touchPressed), and only when the hand is free
  if (act == A_NONE && !pendingEnd) {
    if (touchPressed(T_PICKUP)) tryPick();
    else if (touchPressed(T_SMOKE)) trySmoke(now);
  }
  updateAction(now);

  // movement and collision (you can't walk during an animation)
  if (act == A_NONE && !pendingEnd) {
    // Turning
    if (touchHeld(T_LEFT))  angle -= TURN_SPEED * dt;
    if (touchHeld(T_RIGHT)) angle += TURN_SPEED * dt;

    // Walking
    float move = 0;
    if (touchHeld(T_FORWARD)) move += MOVE_SPEED * dt;
    if (touchHeld(T_BACK))    move -= MOVE_SPEED * dt;

    float newX = posX + cos(angle) * move;
    float newY = posY + sin(angle) * move;

    if (worldMap[(int)posY][(int)newX] == 0) posX = newX; // if it's 1 then you can't move into it
    if (worldMap[(int)newY][(int)posX] == 0) posY = newY;
  }

  if (stress < 0) stress = 0;
  if (stress > 100) stress = 100;
  if (lung > 100) lung = 100;

  // end conditions. Death checks come first so dying beats winning on the same frame
  if (!pendingEnd) {
    if (stress >= 100.0f)                         { pendingEnd = S_END_STRESS;  pendingAt = now + 800; }
    else if (lung >= 100.0f)                      { pendingEnd = S_END_CANCER;  pendingAt = now + 800; }
    else if (act == A_NONE && smoked >= NUM_CIGS) { pendingEnd = S_WIN;         pendingAt = now + 400; }
  }
  if (pendingEnd && now >= pendingAt) {
    endElapsed = now - gameStart;
    state = pendingEnd;
    stateSince = now;
    pendingEnd = 0;
  }
}


// MISC

void setup() {
  Serial.begin(115200);
  u8g2.begin();                 // start the display
  u8g2.setBusClock(400000);     // I2C speed. Try 1000000 later for more frames per second.
  state = S_TITLE;
  stateSince = millis();
}

void loop() {
  // Work out dt, the time since the last frame, in seconds.
  static uint32_t last = millis();
  uint32_t now = millis();
  float dt = (now - last) / 1000.0f;
  last = now;
  if (dt > 0.1f) dt = 0.1f;     // cap it so a hiccup doesn't cause a huge jump

  update_touch();               // read the pads once per frame
  updatePuffs(dt);

  switch (state) {
    case S_TITLE:
      drawTitle(now);
      if (touchPressed(T_SMOKE)) { state = S_INSTR; stateSince = now; }
      break;

    case S_INSTR:
      drawInstructions(now);
      if (touchPressed(T_SMOKE)) startGame();
      break;

    case S_PLAY:
      playFrame(now, dt);
      if (state == S_PLAY) {
        u8g2.clearBuffer();
        renderWalls();          // 3D brick walls + dotted floor
        renderSprites();        // cigarettes on the ground
        drawAction(now);        // hand animation
        drawPuffs();            // smoke
        drawHUD();              // bars and inventory
        u8g2.sendBuffer();
      }
      break;

    default:                    // end screens: stress death, cancer death, win
      drawEnd(now);
      // wait 1 second so an accidental touch doesn't skip the screen
      if (touchPressed(T_SMOKE) && now - stateSince > 1000) startGame();
      break;
  }
}