/**** QPAD D21 Mini Mario (v12 clean build) - Fixes compilation (balanced braces), improves flag grab (slide guaranteed), and advances to next level. - Keeps your mapping: P3(5)=LEFT (idx 3), P4(6)=RIGHT (idx 4), P0(2)=JUMP (idx 0). - Touch driver remains unchanged (Adafruit_FreeTouch); we only interpret it in the game layer. - Includes: adaptive thresholds, robust collisions, unstuck, offscreen watchdog, coins, enemies, coyote, jump buffer. ****/ #include #include #include #include #include // ================= OLED ================= #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define SCREEN_ADDRESS 0x3C Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1, 1000000UL, 1000000UL); // ================= TOUCH (unchanged driver) ================= #define N_TOUCH 6 #define THRESHOLD 500 uint8_t touch_pins[N_TOUCH] = {2,3,4,5,6,7}; // P0..P5 -> pins (2,3,4,5,6,7) Adafruit_FreeTouch* touch_devices[N_TOUCH]; int touch_values[N_TOUCH] = {0,0,0,0,0,0}; bool pin_touched_now[N_TOUCH] = {false,false,false,false,false,false}; bool pin_touched_past[N_TOUCH] = {false,false,false,false,false,false}; void init_touch(){ for (int i=0;ibegin()){ // leave as 0 if not present } } } void update_touch(){ for (int i=0;imeasure(); touch_values[i] = v; pin_touched_now[i] = (v > THRESHOLD); } } // ---- Adaptive game thresholds (does not modify touch driver) ---- #define USE_ADAPTIVE_THRESH 1 #define CAL_SAMPLES 64 #define CAL_DELTA_DEFAULT 60 int16_t cal_delta[N_TOUCH] = {60, 60, 60, 60, 60, 60}; int16_t baseline[N_TOUCH] = {0,0,0,0,0,0}; int16_t game_thresh[N_TOUCH] = {0,0,0,0,0,0}; void calibrateTouch(){ for (int i=0;imeasure(); baseline[i] += v; } delay(5); } for (int i=0;i= worldHeightPixels()) return false; int16_t ty0 = y / TILE_H; if (ty0 < 0) ty0 = 0; int16_t ty1 = (y + h - 1) / TILE_H; if (ty1 >= MAP_H_TILES) ty1 = MAP_H_TILES - 1; int32_t t0 = (x0w >= 0) ? (x0w / TILE_W) : ((x0w - (TILE_W-1)) / TILE_W); int32_t t1 = (x1w >= 0) ? (x1w / TILE_W) : ((x1w - (TILE_W-1)) / TILE_W); for (int16_t ty = ty0; ty <= ty1; ++ty){ for (int32_t tx = t0; tx <= t1; ++tx){ int16_t tw = (int16_t)(tx % MAP_W_TILES); if (tw < 0) tw += MAP_W_TILES; if (world[ty][tw]) return true; } } return false; } void clampPlayer(){ if (px < 0) px = 0; if (px > SCREEN_WIDTH - PLAYER_W) px = SCREEN_WIDTH - PLAYER_W; if (py < -PLAYER_H) py = -PLAYER_H; if (py > SCREEN_HEIGHT + PLAYER_H) py = SCREEN_HEIGHT + PLAYER_H; } void safetyUnstuck(){ if (!rectCollidesSolid((int)px, (int)py, PLAYER_W, PLAYER_H)) return; const int8_t dxs[4] = {0, 1,-1, 0}; const int8_t dys[4] = {-1,0, 0, 1}; for (int dir=0; dir<4; ++dir){ float ox = px, oy = py; for (int s=0; s=0) coins_map[ty-1][tx]=1; } // Ground coins for (int tx=6; tx MAX_FALL_SPEED) e.vy = MAX_FALL_SPEED; // Horizontal patrol in world space; test in screen space { float remain = e.vx; int steps = (int)ceil(fabs(remain)); if (steps < 1) steps = 1; float step = remain / steps; for (int s=0; s= worldWidthPixels()) e.worldX -= worldWidthPixels(); } else { e.vx = -e.vx; // bounce back break; } } } // Vertical { float remain = e.vy; int steps = (int)ceil(fabs(remain)); if (steps < 1) steps = 1; float step = remain / steps; bool collided = false; for (int s=0; s 0){ for (int guard=0; guard 2*SCREEN_WIDTH) ex -= worldWidthPixels(); if (aabbPlayerEnemy(ex, (int16_t)e.y)){ if (vy > 0 && ((int)py + PLAYER_H - 1) <= ((int)e.y + ENEMY_H/2)){ e.alive = false; score += 200; vy = -3.6f; shakeTimer = SHAKE_TIME; } else { // lose life (declared later) // (we forward-declare here by placing function before usage in final code order) } } } } // ================= COINS ================= void collectCoins(int16_t xscr, int16_t y, int16_t w, int16_t h){ int32_t x0w = (int32_t)xscr + (int32_t)camX; int32_t x1w = (int32_t)xscr + (int32_t)w - 1 + (int32_t)camX; int16_t ty0 = y / TILE_H; if (ty0 < 0) ty0 = 0; int16_t ty1 = (y + h - 1) / TILE_H; if (ty1 >= MAP_H_TILES) ty1 = MAP_H_TILES - 1; int32_t t0 = (x0w >= 0) ? (x0w / TILE_W) : ((x0w - (TILE_W-1)) / TILE_W); int32_t t1 = (x1w >= 0) ? (x1w / TILE_W) : ((x1w - (TILE_W-1)) / TILE_W); for (int16_t ty = ty0; ty <= ty1; ++ty){ for (int32_t tx = t0; tx <= t1; ++tx){ int16_t tw = (int16_t)(tx % MAP_W_TILES); if (tw < 0) tw += MAP_W_TILES; if (coins_map[ty][tw]){ coins_map[ty][tw] = 0; coins += 1; score += 100; } } } } // ================= FLAG / LEVEL END ================= void tryGrabFlag(){ // Player center in world space vs flag (wrap-aware) int32_t wpx = worldWidthPixels(); int32_t pcx = (int32_t)px + (int32_t)camX + PLAYER_W/2; int32_t pole = (int32_t)flagWorldX() + TILE_W/2; bool near = false; for (int k=-1; k<=1; ++k){ int32_t f = pole + k * wpx; if (abs(pcx - f) <= 5){ near = true; break; } } if (!near) return; // Latch gameMode = MODE_FLAG_SLIDE; vx = 0.f; vy = 0.f; int16_t fxScr = (int16_t)(flagWorldX() - camX); px = fxScr - (PLAYER_W/2); if (px < 0) px = 0; if (px > SCREEN_WIDTH-PLAYER_W) px = SCREEN_WIDTH-PLAYER_W; } // Forward decls for life handling void resetGame(); void loseLifeAndRespawn(); // ================= RESET / LIVES ================= void hardResetToLevelStart(){ generateWorld(); spawnEnemies(); px = PLAYER_START_X; py = PLAYER_START_Y; vx = vy = 0; onGround = false; jumpFrames = 0; coyote = 0; jumpBuffer = 0; camX = 0; score = 0; coins = 0; animCounter = 0; shakeTimer = 0; offscreenFrames = 0; // lift out if inside solid for (int g=0; g0; ++g) py -= 1; } void resetGame(){ gameMode = MODE_PLAY; clearTimer = 0; hardResetToLevelStart(); lives = MAX_LIVES; } void loseLifeAndRespawn(){ gameMode = MODE_PLAY; offscreenFrames = 0; if (lives>0) lives--; px = PLAYER_START_X; py = PLAYER_START_Y; vx = vy = 0; onGround = false; jumpFrames = 0; coyote = 0; jumpBuffer = 0; camX = 0; shakeTimer = 6; // Relight enemies (soft respawn) spawnEnemies(); for (int g=0; g0; ++g) py -= 1; if (lives == 0){ // Game over -> restart level lives = MAX_LIVES; levelNo = 1; hardResetToLevelStart(); } } // ================= SETUP ================= void setup(){ Serial.begin(115200); delay(20); Wire.begin(); if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)){ while(true){} } display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE); display.setCursor(0,0); display.println("QPAD D21 Mini Mario v12"); display.println("Touch+OLED init..."); display.display(); init_touch(); calibrateTouch(); generateWorld(); spawnEnemies(); // lift out if inside solid for (int g=0; g0; ++g) py -= 1; } // ================= UPDATE ================= void handlePlayerEnemyCollisions2(){ for (int i=0;i 2*SCREEN_WIDTH) ex -= worldWidthPixels(); if (aabbPlayerEnemy(ex, (int16_t)e.y)){ if (vy > 0 && ((int)py + PLAYER_H - 1) <= ((int)e.y + ENEMY_H/2)){ e.alive = false; score += 200; vy = -3.6f; shakeTimer = SHAKE_TIME; } else { loseLifeAndRespawn(); return; } } } } void updateGame(){ update_touch(); if (gameMode == MODE_FLAG_SLIDE){ int16_t groundY = playerGroundTopAtTile(FLAG_TILE_X); vy = (py < groundY) ? 1.8f : 0.f; py += vy; camX = camX; // freeze if ((int)py >= groundY){ py = groundY; gameMode = MODE_CLEARED; clearTimer = 180; // 6s } return; } else if (gameMode == MODE_CLEARED){ if (clearTimer > 0) clearTimer--; return; } // Read touches -> buttons (adaptive thresholds) int vL = touch_values[IDX_LEFT]; int vR = touch_values[IDX_RIGHT]; int vJ = touch_values[IDX_JUMP]; #if USE_ADAPTIVE_THRESH bool btnLeft = (vL > game_thresh[IDX_LEFT]); bool btnRight = (vR > game_thresh[IDX_RIGHT]); bool btnJump = (vJ > game_thresh[IDX_JUMP]); #else bool btnLeft = (vL > (THRESHOLD + PRESS_MARGIN)); bool btnRight = (vR > (THRESHOLD + PRESS_MARGIN)); bool btnJump = (vJ > (THRESHOLD + PRESS_MARGIN)); #endif if (btnLeft && btnRight){ if (vL > vR + PRESS_MARGIN) btnRight = false; else if (vR > vL + PRESS_MARGIN) btnLeft = false; else { btnLeft = btnRight = false; } } // Jump buffer & coyote static uint8_t jumpRelease=1; if (!btnJump) jumpRelease=1; if (btnJump && jumpRelease) jumpBuffer = JUMP_BUFFER_FRAMES; else if (jumpBuffer>0) jumpBuffer--; // Horizontal accel float ax = 0.f; if (btnLeft && !btnRight) ax = -MOVE_SPEED; else if (btnRight && !btnLeft) ax = MOVE_SPEED; else ax = 0.f; vx = (INVERT_X ? -ax : ax); // Jump start if ((onGround || coyote > 0) && jumpBuffer > 0){ jumpRelease = 0; vy = JUMP_VELOCITY; onGround = false; jumpFrames = 0; jumpBuffer = 0; } // Hold for higher jump if (btnJump && vy < 0 && jumpFrames < MAX_JUMP_FRAMES){ vy -= 0.15f; jumpFrames++; } // Gravity vy += GRAVITY; if (vy > MAX_FALL_SPEED) vy = MAX_FALL_SPEED; // ---- Stepped horizontal move ---- { float remain = vx; int steps = (int)ceil(fabs(remain)); if (steps < 1) steps = 1; float step = remain / steps; for (int s=0; s0?1:-1), (int)py, PLAYER_W, PLAYER_H); ++guard) px += (step>0?1:-1); vx = 0.f; break; } } } // ---- Stepped vertical move ---- { float remain = vy; int steps = (int)ceil(fabs(remain)); if (steps < 1) steps = 1; float step = remain / steps; bool collided = false; for (int s=0; s 0){ for (int guard=0; guard 0) coyote = COYOTE_FRAMES; onGround = false; if (coyote>0) coyote--; } } // Camera scroll and safety camX = (int16_t)((camX + CAM_SCROLL_SPEED) % worldWidthPixels()); if (camX < 0) camX += worldWidthPixels(); clampPlayer(); if (rectCollidesSolid((int)px, (int)py, PLAYER_W, PLAYER_H)) safetyUnstuck(); // Coins collectCoins((int)px, (int)py, PLAYER_W, PLAYER_H); // Enemies updateEnemies(); handlePlayerEnemyCollisions2(); // Flag tryGrabFlag(); // Off-screen watchdog if ((px <= -PLAYER_W) || (px >= SCREEN_WIDTH)) { if (offscreenFrames < 255) offscreenFrames++; } else { offscreenFrames = 0; } if (offscreenFrames > MAX_OFFSCREEN_FRAMES) { loseLifeAndRespawn(); offscreenFrames = 0; } // Fall if ((int)py > SCREEN_HEIGHT){ loseLifeAndRespawn(); } score += 1; animCounter++; } // ================= RENDER ================= void renderGame(){ display.clearDisplay(); int16_t shakeX = 0, shakeY = 0; if (shakeTimer>0){ shakeX = (shakeTimer % 2) ? 1 : -1; shakeY = ((shakeTimer/2) % 2) ? 1 : -1; shakeTimer--; } // Tiles + coins int16_t camTile = camX / TILE_W; int16_t camOffX = camX % TILE_W; for (int ty=0; ty 2*SCREEN_WIDTH) fx -= worldWidthPixels(); if (fx > -8 && fx < SCREEN_WIDTH){ display.drawFastVLine(fx + (TILE_W/2), 0, SCREEN_HEIGHT, SSD1306_WHITE); int16_t ftop = 8 + ((animCounter/6)%2 ? 1 : 0); display.fillRect(fx + (TILE_W/2) + 1, ftop, 10, 6, SSD1306_WHITE); } int16_t cx = (int16_t)(castleWorldX() - camX) + shakeX; if (cx > -16 && cx < SCREEN_WIDTH){ display.fillRect(cx, SCREEN_HEIGHT-24, 16, 24, SSD1306_WHITE); display.fillRect(cx+3, SCREEN_HEIGHT-30, 10, 6, SSD1306_WHITE); display.fillRect(cx+6, SCREEN_HEIGHT-34, 4, 4, SSD1306_WHITE); display.fillRect(cx+5, SCREEN_HEIGHT-12, 6, 12, SSD1306_WHITE); // gate } // Enemies for (int i=0;i 2*SCREEN_WIDTH) ex -= worldWidthPixels(); if (ex > -ENEMY_W && ex < SCREEN_WIDTH){ display.drawBitmap(ex + shakeX, (int16_t)e.y + shakeY, enemy_sprite, ENEMY_W, ENEMY_H, SSD1306_WHITE); } } // Player const uint8_t* sprite = player_stand; if (onGround && (vx != 0.f)){ sprite = ((animCounter / 6) % 2) ? player_run1 : player_run2; } display.drawBitmap((int16_t)px + shakeX, (int16_t)py + shakeY, sprite, PLAYER_W, PLAYER_H, SSD1306_WHITE); // HUD display.setTextSize(1); display.setTextColor(SSD1306_WHITE); display.setCursor(0,0); display.print(F("S:")); display.print(score); display.print(F(" C:")); display.print(coins); display.print(F(" L:")); display.print(lives); display.setCursor(0,8); display.print(F("L:")); display.print(touch_values[IDX_LEFT]); display.print(F(" R:")); display.print(touch_values[IDX_RIGHT]); display.print(F(" J:")); display.print(touch_values[IDX_JUMP]); #if USE_ADAPTIVE_THRESH display.setCursor(0,16); display.print(F("Lt>")); display.print(game_thresh[IDX_LEFT]); display.print(F(" Rt>")); display.print(game_thresh[IDX_RIGHT]); display.print(F(" J>")); display.print(game_thresh[IDX_JUMP]); #endif if (gameMode == MODE_CLEARED){ display.setTextSize(2); display.setCursor(10, 30); display.print(F("LEVEL ")); display.print(levelNo); display.print(F(" CLEAR")); display.setTextSize(1); } display.display(); } // ================= LOOP ================= void loop(){ static uint32_t lastFrame = 0; uint32_t now = millis(); if (now - lastFrame < FRAME_MS) return; lastFrame = now; updateGame(); renderGame(); if (gameMode == MODE_CLEARED && clearTimer == 0){ levelNo++; if (levelNo > 99) levelNo = 1; resetGame(); } }