// main.cpp - Application orchestration (initialization, loop, UI states) // High-level only: delegates Tetris logic, scores, background, font rendering. #include #include #include #include #include #include #include #include #include #include #include #include #include "audio/Audio.h" #include "audio/SoundEffect.h" #include "gameplay/Game.h" #include "persistence/Scores.h" #include "graphics/Starfield.h" #include "Starfield3D.h" #include "graphics/Font.h" #include "gameplay/LineEffect.h" #include "states/State.h" #include "states/LoadingState.h" #include "states/MenuState.h" #include "states/PlayingState.h" #include "audio/MenuWrappers.h" // Debug logging removed: no-op in this build (previously LOG_DEBUG) // Font rendering now handled by FontAtlas // ---------- Game config ---------- static constexpr int LOGICAL_W = 1200; static constexpr int LOGICAL_H = 1000; static constexpr int WELL_W = Game::COLS * Game::TILE; static constexpr int WELL_H = Game::ROWS * Game::TILE; // Piece types now declared in Game.h // Scores now managed by ScoreManager // 4x4 shapes encoded as 16-bit bitmasks per rotation (row-major 4x4). // Bit 0 = (x=0,y=0), Bit 1 = (1,0) ... Bit 15 = (3,3) // Shapes & game logic now in Game.cpp // (removed inline shapes) // Piece struct now in Game.h // Game struct replaced by Game class static const std::array COLORS = {{ SDL_Color{20, 20, 26, 255}, // 0 empty SDL_Color{0, 255, 255, 255}, // I SDL_Color{255, 255, 0, 255}, // O SDL_Color{160, 0, 255, 255}, // T SDL_Color{0, 255, 0, 255}, // S SDL_Color{255, 0, 0, 255}, // Z SDL_Color{0, 0, 255, 255}, // J SDL_Color{255, 160, 0, 255}, // L }}; static void drawRect(SDL_Renderer *r, float x, float y, float w, float h, SDL_Color c) { SDL_SetRenderDrawColor(r, c.r, c.g, c.b, c.a); SDL_FRect fr{x, y, w, h}; SDL_RenderFillRect(r, &fr); } // ...existing code... // ----------------------------------------------------------------------------- // Enhanced Button Drawing // ----------------------------------------------------------------------------- static void drawEnhancedButton(SDL_Renderer* renderer, FontAtlas& font, float cx, float cy, float w, float h, const std::string& label, bool isHovered, bool isSelected = false) { SDL_Color bgColor = isHovered ? SDL_Color{120, 150, 240, 255} : SDL_Color{80, 110, 200, 255}; if (isSelected) bgColor = {160, 190, 255, 255}; float x = cx - w/2; float y = cy - h/2; // Draw button background with border drawRect(renderer, x-2, y-2, w+4, h+4, {60, 80, 140, 255}); // Border drawRect(renderer, x, y, w, h, bgColor); // Background // Draw button text centered float textScale = 1.5f; float textX = x + (w - label.length() * 12 * textScale) / 2; float textY = y + (h - 20 * textScale) / 2; font.draw(renderer, textX, textY, label, textScale, {255, 255, 255, 255}); } // External wrapper for enhanced button so other translation units can call it. void menu_drawEnhancedButton(SDL_Renderer* renderer, FontAtlas& font, float cx, float cy, float w, float h, const std::string& label, bool isHovered, bool isSelected) { drawEnhancedButton(renderer, font, cx, cy, w, h, label, isHovered, isSelected); } // Popup wrappers // Forward declarations for popup functions defined later in this file static void drawLevelSelectionPopup(SDL_Renderer* renderer, FontAtlas& font, SDL_Texture* bgTex, int selectedLevel); static void drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musicEnabled); void menu_drawLevelSelectionPopup(SDL_Renderer* renderer, FontAtlas& font, SDL_Texture* bgTex, int selectedLevel) { drawLevelSelectionPopup(renderer, font, bgTex, selectedLevel); } void menu_drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musicEnabled) { drawSettingsPopup(renderer, font, musicEnabled); } // Simple rounded menu button drawer used by MenuState (keeps visual parity with JS) void menu_drawMenuButton(SDL_Renderer* renderer, FontAtlas& font, float cx, float cy, float w, float h, const std::string& label, SDL_Color bgColor, SDL_Color borderColor) { float x = cx - w/2; float y = cy - h/2; drawRect(renderer, x-6, y-6, w+12, h+12, borderColor); drawRect(renderer, x-4, y-4, w+8, h+8, {255,255,255,255}); drawRect(renderer, x, y, w, h, bgColor); float textScale = 1.6f; float approxCharW = 12.0f * textScale; float textW = label.length() * approxCharW; float tx = x + (w - textW) / 2.0f; float ty = y + (h - 20.0f * textScale) / 2.0f; font.draw(renderer, tx+2, ty+2, label, textScale, {0,0,0,180}); font.draw(renderer, tx, ty, label, textScale, {255,255,255,255}); } // ----------------------------------------------------------------------------- // Block Drawing Functions // ----------------------------------------------------------------------------- static void drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType) { if (!blocksTex || blockType < 0 || blockType >= PIECE_COUNT) { // Debug: print why we're falling back if (!blocksTex) { static bool printed = false; if (!printed) { (void)0; printed = true; } } // Fallback to colored rectangle if texture isn't available SDL_Color color = (blockType >= 0 && blockType < PIECE_COUNT) ? COLORS[blockType + 1] : SDL_Color{128, 128, 128, 255}; drawRect(renderer, x, y, size-1, size-1, color); return; } // JavaScript uses: sx = type * spriteSize, sy = 0, with 2px padding // Each sprite is 90px wide in the horizontal sprite sheet const int SPRITE_SIZE = 90; float srcX = blockType * SPRITE_SIZE + 2; // Add 2px padding like JS float srcY = 2; // Add 2px padding from top like JS float srcW = SPRITE_SIZE - 4; // Subtract 4px total padding like JS float srcH = SPRITE_SIZE - 4; // Subtract 4px total padding like JS SDL_FRect srcRect = {srcX, srcY, srcW, srcH}; SDL_FRect dstRect = {x, y, size, size}; SDL_RenderTexture(renderer, blocksTex, &srcRect, &dstRect); } static void drawPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, const Game::Piece& piece, float ox, float oy, float tileSize, bool isGhost = false) { if (piece.type >= PIECE_COUNT) return; for (int cy = 0; cy < 4; ++cy) { for (int cx = 0; cx < 4; ++cx) { if (Game::cellFilled(piece, cx, cy)) { float px = ox + (piece.x + cx) * tileSize; float py = oy + (piece.y + cy) * tileSize; if (isGhost) { SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); // Draw ghost piece as barely visible gray outline SDL_SetRenderDrawColor(renderer, 180, 180, 180, 20); // Very faint gray SDL_FRect rect = {px + 2, py + 2, tileSize - 4, tileSize - 4}; SDL_RenderFillRect(renderer, &rect); // Draw thin gray border SDL_SetRenderDrawColor(renderer, 180, 180, 180, 30); SDL_FRect border = {px + 1, py + 1, tileSize - 2, tileSize - 2}; SDL_RenderRect(renderer, &border); } else { drawBlockTexture(renderer, blocksTex, px, py, tileSize, piece.type); } } } } } static void drawSmallPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, PieceType pieceType, float x, float y, float tileSize) { if (pieceType >= PIECE_COUNT) return; // Use the first rotation (index 0) for preview Game::Piece previewPiece; previewPiece.type = pieceType; previewPiece.rot = 0; previewPiece.x = 0; previewPiece.y = 0; // Center the piece in the preview area float offsetX = 0, offsetY = 0; if (pieceType == I) { offsetX = tileSize * 0.5f; } // I-piece centering else if (pieceType == O) { offsetX = tileSize * 0.5f; } // O-piece centering // Use semi-transparent alpha for preview blocks Uint8 previewAlpha = 180; // Change this value for more/less transparency SDL_SetTextureAlphaMod(blocksTex, previewAlpha); for (int cy = 0; cy < 4; ++cy) { for (int cx = 0; cx < 4; ++cx) { if (Game::cellFilled(previewPiece, cx, cy)) { float px = x + offsetX + cx * tileSize; float py = y + offsetY + cy * tileSize; drawBlockTexture(renderer, blocksTex, px, py, tileSize, pieceType); } } } SDL_SetTextureAlphaMod(blocksTex, 255); // Reset alpha after drawing } // ----------------------------------------------------------------------------- // Popup Drawing Functions // ----------------------------------------------------------------------------- static void drawLevelSelectionPopup(SDL_Renderer* renderer, FontAtlas& font, SDL_Texture* bgTex, int selectedLevel) { // Popup dims scale with logical size for responsiveness float popupW = std::min(760.0f, LOGICAL_W * 0.75f); float popupH = std::min(520.0f, LOGICAL_H * 0.7f); float popupX = (LOGICAL_W - popupW) / 2.0f; float popupY = (LOGICAL_H - popupH) / 2.0f; // Draw the background picture stretched to full logical viewport if available if (bgTex) { // Dim the background by rendering it then overlaying a semi-transparent black rect SDL_FRect dst{0, 0, (float)LOGICAL_W, (float)LOGICAL_H}; SDL_RenderTexture(renderer, bgTex, nullptr, &dst); SDL_SetRenderDrawColor(renderer, 0, 0, 0, 160); SDL_FRect dim{0,0,(float)LOGICAL_W,(float)LOGICAL_H}; SDL_RenderFillRect(renderer, &dim); } else { // Fallback to semi-transparent overlay SDL_SetRenderDrawColor(renderer, 0, 0, 0, 180); SDL_FRect overlay{0, 0, (float)LOGICAL_W, (float)LOGICAL_H}; SDL_RenderFillRect(renderer, &overlay); } // Popup panel with border and subtle background drawRect(renderer, popupX-6, popupY-6, popupW+12, popupH+12, {90, 110, 140, 200}); // outer border drawRect(renderer, popupX-3, popupY-3, popupW+6, popupH+6, {30, 38, 60, 220}); // inner border drawRect(renderer, popupX, popupY, popupW, popupH, {18, 22, 34, 235}); // panel // Title (use retro pixel font) font.draw(renderer, popupX + 28, popupY + 18, "SELECT STARTING LEVEL", 2.4f, {255, 220, 0, 255}); // Grid layout for levels: 4 columns x 5 rows int cols = 4, rows = 5; float padding = 24.0f; float gridW = popupW - padding * 2; float gridH = popupH - 120.0f; // leave space for title and instructions float cellW = gridW / cols; float cellH = std::min(80.0f, gridH / rows - 12.0f); float gridStartX = popupX + padding; float gridStartY = popupY + 70; for (int level = 0; level < 20; ++level) { int row = level / cols; int col = level % cols; float cx = gridStartX + col * cellW; float cy = gridStartY + row * (cellH + 12.0f); bool isSelected = (level == selectedLevel); SDL_Color bg = isSelected ? SDL_Color{255, 220, 0, 255} : SDL_Color{70, 85, 120, 240}; SDL_Color fg = isSelected ? SDL_Color{0, 0, 0, 255} : SDL_Color{240, 240, 245, 255}; // Button background drawRect(renderer, cx + 8, cy, cellW - 16, cellH, bg); // Level label centered char levelStr[8]; snprintf(levelStr, sizeof(levelStr), "%d", level); float tx = cx + (cellW / 2.0f) - (6.0f * 1.8f); // rough centering float ty = cy + (cellH / 2.0f) - 10.0f; font.draw(renderer, tx, ty, levelStr, 1.8f, fg); } // Instructions under grid font.draw(renderer, popupX + 28, popupY + popupH - 40, "CLICK A LEVEL TO SELECT • ESC = CANCEL", 1.0f, {200,200,220,255}); } static void drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musicEnabled) { float popupW = 350, popupH = 260; float popupX = (LOGICAL_W - popupW) / 2; float popupY = (LOGICAL_H - popupH) / 2; // Semi-transparent overlay SDL_SetRenderDrawColor(renderer, 0, 0, 0, 128); SDL_FRect overlay{0, 0, LOGICAL_W, LOGICAL_H}; SDL_RenderFillRect(renderer, &overlay); // Popup background drawRect(renderer, popupX-4, popupY-4, popupW+8, popupH+8, {100, 120, 160, 255}); // Border drawRect(renderer, popupX, popupY, popupW, popupH, {40, 50, 70, 255}); // Background // Title font.draw(renderer, popupX + 20, popupY + 20, "SETTINGS", 2.0f, {255, 220, 0, 255}); // Music toggle font.draw(renderer, popupX + 20, popupY + 70, "MUSIC:", 1.5f, {255, 255, 255, 255}); const char* musicStatus = musicEnabled ? "ON" : "OFF"; SDL_Color musicColor = musicEnabled ? SDL_Color{0, 255, 0, 255} : SDL_Color{255, 0, 0, 255}; font.draw(renderer, popupX + 120, popupY + 70, musicStatus, 1.5f, musicColor); // Sound effects toggle font.draw(renderer, popupX + 20, popupY + 100, "SOUND FX:", 1.5f, {255, 255, 255, 255}); const char* soundStatus = SoundEffectManager::instance().isEnabled() ? "ON" : "OFF"; SDL_Color soundColor = SoundEffectManager::instance().isEnabled() ? SDL_Color{0, 255, 0, 255} : SDL_Color{255, 0, 0, 255}; font.draw(renderer, popupX + 140, popupY + 100, soundStatus, 1.5f, soundColor); // Instructions font.draw(renderer, popupX + 20, popupY + 150, "M = TOGGLE MUSIC", 1.0f, {200, 200, 220, 255}); font.draw(renderer, popupX + 20, popupY + 170, "S = TOGGLE SOUND FX", 1.0f, {200, 200, 220, 255}); font.draw(renderer, popupX + 20, popupY + 190, "N = PLAY LETS_GO", 1.0f, {200, 200, 220, 255}); font.draw(renderer, popupX + 20, popupY + 210, "ESC = CLOSE", 1.0f, {200, 200, 220, 255}); } // ----------------------------------------------------------------------------- // Starfield effect for background // ----------------------------------------------------------------------------- // Starfield now managed by Starfield class // State manager integration (scaffolded in StateManager.h) #include "core/StateManager.h" // ----------------------------------------------------------------------------- // Intro/Menu state variables // ----------------------------------------------------------------------------- static double logoAnimCounter = 0.0; static bool showLevelPopup = false; static bool showSettingsPopup = false; static bool showExitConfirmPopup = false; static bool musicEnabled = true; static int hoveredButton = -1; // -1 = none, 0 = play, 1 = level, 2 = settings // ----------------------------------------------------------------------------- // Tetris Block Fireworks for intro animation (block particles) // Forward declare block render helper used by particles static void drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType); // ----------------------------------------------------------------------------- struct BlockParticle { float x, y, vx, vy, size, alpha, decay; int blockType; // 0..6 BlockParticle(float sx, float sy) : x(sx), y(sy) { float angle = (rand() % 628) / 100.0f; // 0..2pi float speed = 1.5f + (rand() % 350) / 100.0f; // ~1.5..5.0 vx = std::cos(angle) * speed; vy = std::sin(angle) * speed; size = 6.0f + (rand() % 50) / 10.0f; // 6..11 px alpha = 1.0f; decay = 0.012f + (rand() % 200) / 10000.0f; // 0.012..0.032 blockType = rand() % 7; // choose a tetris color } bool update() { vx *= 0.985f; // friction vy = vy * 0.985f + 0.07f; // gravity x += vx; y += vy; alpha -= decay; size = std::max(2.0f, size - 0.04f); return alpha > 0.02f; } }; struct TetrisFirework { std::vector particles; int mode = 0; // 0=random,1=red,2=green,3=palette TetrisFirework(float x, float y) { mode = rand() % 4; int particleCount = 30 + rand() % 25; // 30-55 particles particles.reserve(particleCount); for (int i = 0; i < particleCount; ++i) particles.emplace_back(x, y); } bool update() { for (auto it = particles.begin(); it != particles.end();) { if (!it->update()) it = particles.erase(it); else ++it; } return !particles.empty(); } // Drawing is handled by drawFireworks_impl which accepts the texture to use. }; static std::vector fireworks; static Uint64 lastFireworkTime = 0; // ----------------------------------------------------------------------------- // Fireworks Management // ----------------------------------------------------------------------------- static void updateFireworks(double frameMs) { Uint64 now = SDL_GetTicks(); // Randomly spawn new block fireworks (2% chance per frame), bias to lower-right if (fireworks.size() < 5 && (rand() % 100) < 2) { float x = LOGICAL_W * 0.55f + float(rand() % int(LOGICAL_W * 0.35f)); float y = LOGICAL_H * 0.80f + float(rand() % int(LOGICAL_H * 0.15f)); fireworks.emplace_back(x, y); lastFireworkTime = now; } // Update existing fireworks for (auto it = fireworks.begin(); it != fireworks.end();) { if (!it->update()) { it = fireworks.erase(it); } else { ++it; } } } // Primary implementation that accepts a texture pointer static void drawFireworks_impl(SDL_Renderer* renderer, SDL_Texture* blocksTexture) { for (auto& f : fireworks) { // Particle draw uses the texture pointer passed into drawBlockTexture calls from f.draw // We'll set a thread-local-ish variable by passing the texture as an argument to draw // routines or using the provided texture in the particle's draw path. // For simplicity, the particle draw function below will reference a global symbol // via an argument — we adapt by providing the texture when calling drawBlockTexture. // Implementation: call a small lambda that temporarily binds the texture for drawBlockTexture. struct Drawer { SDL_Renderer* r; SDL_Texture* tex; void drawParticle(struct BlockParticle& p) { if (tex) { Uint8 prevA = 255; SDL_GetTextureAlphaMod(tex, &prevA); Uint8 setA = Uint8(std::max(0.0f, std::min(1.0f, p.alpha)) * 255.0f); SDL_SetTextureAlphaMod(tex, setA); // Note: color modulation will be applied by callers of drawBlockTexture where needed // but we mimic behavior from previous implementation by leaving color mod as default. drawBlockTexture(r, tex, p.x - p.size * 0.5f, p.y - p.size * 0.5f, p.size, p.blockType); SDL_SetTextureAlphaMod(tex, prevA); SDL_SetTextureColorMod(tex, 255, 255, 255); } else { SDL_SetRenderDrawColor(r, 255, 255, 255, Uint8(p.alpha * 255)); SDL_FRect rect{p.x - p.size/2, p.y - p.size/2, p.size, p.size}; SDL_RenderFillRect(r, &rect); } } } drawer{renderer, blocksTexture}; for (auto &p : f.particles) { drawer.drawParticle(p); } } } // External wrappers for use by other translation units (MenuState) // Expect callers to pass the blocks texture via StateContext so we avoid globals. void menu_drawFireworks(SDL_Renderer* renderer, SDL_Texture* blocksTex) { drawFireworks_impl(renderer, blocksTex); } void menu_updateFireworks(double frameMs) { updateFireworks(frameMs); } double menu_getLogoAnimCounter() { return logoAnimCounter; } int menu_getHoveredButton() { return hoveredButton; } int main(int, char **) { // Initialize random seed for fireworks srand(static_cast(SDL_GetTicks())); int sdlInitRes = SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO); if (sdlInitRes < 0) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_Init failed: %s", SDL_GetError()); return 1; } int ttfInitRes = TTF_Init(); if (ttfInitRes < 0) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "TTF_Init failed"); SDL_Quit(); return 1; } SDL_Window *window = SDL_CreateWindow("Tetris (SDL3)", LOGICAL_W, LOGICAL_H, SDL_WINDOW_RESIZABLE); if (!window) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateWindow failed: %s", SDL_GetError()); TTF_Quit(); SDL_Quit(); return 1; } SDL_Renderer *renderer = SDL_CreateRenderer(window, nullptr); if (!renderer) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateRenderer failed: %s", SDL_GetError()); SDL_DestroyWindow(window); TTF_Quit(); SDL_Quit(); return 1; } SDL_SetRenderVSync(renderer, 1); FontAtlas font; font.init("FreeSans.ttf", 24); // Load PressStart2P font for loading screen and retro UI elements FontAtlas pixelFont; pixelFont.init("assets/fonts/PressStart2P-Regular.ttf", 16); ScoreManager scores; scores.load(); Starfield starfield; starfield.init(200, LOGICAL_W, LOGICAL_H); Starfield3D starfield3D; starfield3D.init(LOGICAL_W, LOGICAL_H, 200); // Initialize line clearing effects LineEffect lineEffect; lineEffect.init(renderer); // Load logo using native SDL BMP loading SDL_Texture *logoTex = nullptr; SDL_Surface* logoSurface = SDL_LoadBMP("assets/images/logo.bmp"); if (logoSurface) { (void)0; logoTex = SDL_CreateTextureFromSurface(renderer, logoSurface); SDL_DestroySurface(logoSurface); } else { (void)0; } // Load small logo (used by Menu to show whole logo) SDL_Texture *logoSmallTex = nullptr; SDL_Surface* logoSmallSurface = SDL_LoadBMP("assets/images/logo_small.bmp"); int logoSmallW = 0, logoSmallH = 0; if (logoSmallSurface) { // capture surface size before creating the texture (avoids SDL_QueryTexture) logoSmallW = logoSmallSurface->w; logoSmallH = logoSmallSurface->h; logoSmallTex = SDL_CreateTextureFromSurface(renderer, logoSmallSurface); SDL_DestroySurface(logoSmallSurface); } else { // fallback: leave logoSmallTex null so MenuState will use large logo (void)0; } // Load background using native SDL BMP loading SDL_Texture *backgroundTex = nullptr; SDL_Surface* backgroundSurface = SDL_LoadBMP("assets/images/main_background.bmp"); if (backgroundSurface) { (void)0; backgroundTex = SDL_CreateTextureFromSurface(renderer, backgroundSurface); SDL_DestroySurface(backgroundSurface); } else { (void)0; } // Note: `backgroundTex` is owned by main and passed into `StateContext::backgroundTex` below. // States should render using `ctx.backgroundTex` rather than accessing globals. // Level background caching system SDL_Texture *levelBackgroundTex = nullptr; SDL_Texture *nextLevelBackgroundTex = nullptr; // used during fade transitions int cachedLevel = -1; // Track which level background is currently cached float levelFadeAlpha = 0.0f; // 0..1 blend factor where 1 means next fully visible const float LEVEL_FADE_DURATION = 3500.0f; // ms for fade transition (3.5s) float levelFadeElapsed = 0.0f; // Load blocks texture using native SDL BMP loading SDL_Texture *blocksTex = nullptr; SDL_Surface* blocksSurface = SDL_LoadBMP("assets/images/blocks90px_001.bmp"); if (blocksSurface) { (void)0; blocksTex = SDL_CreateTextureFromSurface(renderer, blocksSurface); SDL_DestroySurface(blocksSurface); } else { (void)0; } // No global exposure of blocksTex; states receive textures via StateContext. if (!blocksTex) { (void)0; // Create a 630x90 texture (7 blocks * 90px each) blocksTex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, 630, 90); if (blocksTex) { // Set texture as render target and draw colored blocks SDL_SetRenderTarget(renderer, blocksTex); SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0); SDL_RenderClear(renderer); // Draw each block type with its color for (int i = 0; i < PIECE_COUNT; ++i) { SDL_Color color = COLORS[i + 1]; SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, 255); SDL_FRect rect = {float(i * 90 + 4), 4, 82, 82}; // 4px padding, 82x82 block SDL_RenderFillRect(renderer, &rect); // Add a highlight effect SDL_SetRenderDrawColor(renderer, (Uint8)std::min(255, color.r + 30), (Uint8)std::min(255, color.g + 30), (Uint8)std::min(255, color.b + 30), 255); SDL_FRect highlight = {float(i * 90 + 4), 4, 82, 20}; SDL_RenderFillRect(renderer, &highlight); } // Reset render target SDL_SetRenderTarget(renderer, nullptr); (void)0; } else { std::fprintf(stderr, "Failed to create programmatic texture: %s\n", SDL_GetError()); } } else { (void)0; } // Provide the blocks sheet to the fireworks system through StateContext (no globals). // Default start level selection: 0 int startLevelSelection = 0; Game game(startLevelSelection); // Initialize sound effects system SoundEffectManager::instance().init(); // Load sound effects SoundEffectManager::instance().loadSound("clear_line", "assets/music/clear_line.wav"); // Load voice lines for line clears using WAV files (with MP3 fallback) std::vector doubleSounds = {"nice_combo", "you_fire", "well_played", "keep_that_ryhtm"}; std::vector tripleSounds = {"great_move", "smooth_clear", "impressive", "triple_strike"}; std::vector tetrisSounds = {"amazing", "you_re_unstoppable", "boom_tetris", "wonderful"}; // Helper function to load sound with WAV/MP3 fallback and file existence check auto loadSoundWithFallback = [&](const std::string& id, const std::string& baseName) { std::string wavPath = "assets/music/" + baseName + ".wav"; std::string mp3Path = "assets/music/" + baseName + ".mp3"; // Check if WAV file exists first SDL_IOStream* wavFile = SDL_IOFromFile(wavPath.c_str(), "rb"); if (wavFile) { SDL_CloseIO(wavFile); if (SoundEffectManager::instance().loadSound(id, wavPath)) { (void)0; return; } } // Fallback to MP3 if WAV doesn't exist or fails to load SDL_IOStream* mp3File = SDL_IOFromFile(mp3Path.c_str(), "rb"); if (mp3File) { SDL_CloseIO(mp3File); if (SoundEffectManager::instance().loadSound(id, mp3Path)) { (void)0; return; } } SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load sound: %s (tried both WAV and MP3)", id.c_str()); }; loadSoundWithFallback("nice_combo", "nice_combo"); loadSoundWithFallback("you_fire", "you_fire"); loadSoundWithFallback("well_played", "well_played"); loadSoundWithFallback("keep_that_ryhtm", "keep_that_ryhtm"); loadSoundWithFallback("great_move", "great_move"); loadSoundWithFallback("smooth_clear", "smooth_clear"); loadSoundWithFallback("impressive", "impressive"); loadSoundWithFallback("triple_strike", "triple_strike"); loadSoundWithFallback("amazing", "amazing"); loadSoundWithFallback("you_re_unstoppable", "you_re_unstoppable"); loadSoundWithFallback("boom_tetris", "boom_tetris"); loadSoundWithFallback("wonderful", "wonderful"); loadSoundWithFallback("lets_go", "lets_go"); // For level up // Set up sound effect callbacks game.setSoundCallback([&](int linesCleared) { // Play basic line clear sound first SoundEffectManager::instance().playSound("clear_line", 1.0f); // Increased volume // Then play voice line based on number of lines cleared if (linesCleared == 2) { SoundEffectManager::instance().playRandomSound(doubleSounds, 1.0f); // Increased volume } else if (linesCleared == 3) { SoundEffectManager::instance().playRandomSound(tripleSounds, 1.0f); // Increased volume } else if (linesCleared == 4) { SoundEffectManager::instance().playRandomSound(tetrisSounds, 1.0f); // Increased volume } // Single line clears just play the basic clear sound (no voice in JS version) }); game.setLevelUpCallback([&](int newLevel) { // Play level up sound SoundEffectManager::instance().playSound("lets_go", 1.0f); // Increased volume }); AppState state = AppState::Loading; double loadingProgress = 0.0; Uint64 loadStart = SDL_GetTicks(); bool running = true, isFullscreen = false; bool leftHeld = false, rightHeld = false; double moveTimerMs = 0; const double DAS = 170.0, ARR = 40.0; SDL_Rect logicalVP{0, 0, LOGICAL_W, LOGICAL_H}; float logicalScale = 1.f; Uint64 lastMs = SDL_GetTicks(); bool musicStarted = false; bool musicLoaded = false; int currentTrackLoading = 0; int totalTracks = 0; // Will be set dynamically based on actual files // Instantiate state manager StateManager stateMgr(state); // Prepare shared context for states StateContext ctx{}; // Allow states to access the state manager for transitions ctx.stateManager = &stateMgr; ctx.game = &game; ctx.scores = &scores; ctx.starfield = &starfield; ctx.starfield3D = &starfield3D; ctx.font = &font; ctx.pixelFont = &pixelFont; ctx.lineEffect = &lineEffect; ctx.logoTex = logoTex; ctx.logoSmallTex = logoSmallTex; ctx.logoSmallW = logoSmallW; ctx.logoSmallH = logoSmallH; ctx.backgroundTex = backgroundTex; ctx.blocksTex = blocksTex; ctx.musicEnabled = &musicEnabled; ctx.startLevelSelection = &startLevelSelection; ctx.hoveredButton = &hoveredButton; ctx.showLevelPopup = &showLevelPopup; ctx.showSettingsPopup = &showSettingsPopup; ctx.showExitConfirmPopup = &showExitConfirmPopup; // Instantiate state objects auto loadingState = std::make_unique(ctx); auto menuState = std::make_unique(ctx); auto playingState = std::make_unique(ctx); // Register handlers and lifecycle hooks stateMgr.registerHandler(AppState::Loading, [&](const SDL_Event& e){ loadingState->handleEvent(e); }); stateMgr.registerOnEnter(AppState::Loading, [&](){ loadingState->onEnter(); }); stateMgr.registerOnExit(AppState::Loading, [&](){ loadingState->onExit(); }); stateMgr.registerHandler(AppState::Menu, [&](const SDL_Event& e){ menuState->handleEvent(e); }); stateMgr.registerOnEnter(AppState::Menu, [&](){ menuState->onEnter(); }); stateMgr.registerOnExit(AppState::Menu, [&](){ menuState->onExit(); }); // Combined Playing state handler: run playingState handler and inline gameplay mapping stateMgr.registerHandler(AppState::Playing, [&](const SDL_Event& e){ // First give the PlayingState a chance to handle the event playingState->handleEvent(e); // Then perform inline gameplay mappings (gravity/rotation/hard-drop/hold) if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) { if (!game.isPaused()) { if (e.key.scancode == SDL_SCANCODE_SPACE) { game.hardDrop(); } else if (e.key.scancode == SDL_SCANCODE_UP) { game.rotate(+1); } else if (e.key.scancode == SDL_SCANCODE_Z || (e.key.mod & SDL_KMOD_SHIFT)) { game.rotate(-1); } else if (e.key.scancode == SDL_SCANCODE_C || (e.key.mod & SDL_KMOD_CTRL)) { game.holdCurrent(); } } } }); stateMgr.registerOnEnter(AppState::Playing, [&](){ playingState->onEnter(); }); stateMgr.registerOnExit(AppState::Playing, [&](){ playingState->onExit(); }); // Playing, LevelSelect and GameOver currently use inline logic in main; we'll migrate later while (running) { int winW = 0, winH = 0; SDL_GetWindowSize(window, &winW, &winH); // Use the full window for the viewport, scale to fit content logicalScale = std::min(winW / (float)LOGICAL_W, winH / (float)LOGICAL_H); if (logicalScale <= 0) logicalScale = 1.f; // Fill the entire window with our viewport logicalVP.w = winW; logicalVP.h = winH; logicalVP.x = 0; logicalVP.y = 0; // --- Events --- SDL_Event e; while (SDL_PollEvent(&e)) { if (e.type == SDL_EVENT_QUIT) running = false; else { // Route event to state manager handlers for per-state logic stateMgr.handleEvent(e); // Global key toggles (applies regardless of state) if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) { if (e.key.scancode == SDL_SCANCODE_M) { Audio::instance().toggleMute(); musicEnabled = !musicEnabled; } if (e.key.scancode == SDL_SCANCODE_S) { // Toggle sound effects SoundEffectManager::instance().setEnabled(!SoundEffectManager::instance().isEnabled()); } if (e.key.scancode == SDL_SCANCODE_N) { // Test sound effects - play lets_go.wav specifically SoundEffectManager::instance().playSound("lets_go", 1.0f); } if (e.key.key == SDLK_F11 || (e.key.key == SDLK_RETURN && (e.key.mod & SDL_KMOD_ALT))) { isFullscreen = !isFullscreen; SDL_SetWindowFullscreen(window, isFullscreen ? SDL_WINDOW_FULLSCREEN : 0); } } // Mouse handling remains in main loop for UI interactions if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN) { float mx = (float)e.button.x, my = (float)e.button.y; if (mx >= logicalVP.x && my >= logicalVP.y && mx <= logicalVP.x + logicalVP.w && my <= logicalVP.y + logicalVP.h) { float lx = (mx - logicalVP.x) / logicalScale, ly = (my - logicalVP.y) / logicalScale; if (state == AppState::Menu) { // Compute content offsets (match MenuState centering) float contentW = LOGICAL_W * logicalScale; float contentH = LOGICAL_H * logicalScale; float contentOffsetX = (winW - contentW) * 0.5f / logicalScale; float contentOffsetY = (winH - contentH) * 0.5f / logicalScale; if (showLevelPopup) { // Handle level selection popup clicks float popupW = 400, popupH = 300; float popupX = (LOGICAL_W - popupW) / 2; float popupY = (LOGICAL_H - popupH) / 2; if (lx >= popupX && lx <= popupX + popupW && ly >= popupY && ly <= popupY + popupH) { // Click inside popup - check level grid float gridStartX = popupX + 50; float gridStartY = popupY + 70; float cellW = 70, cellH = 35; if (lx >= gridStartX && ly >= gridStartY) { int col = int((lx - gridStartX) / cellW); int row = int((ly - gridStartY) / cellH); if (col >= 0 && col < 4 && row >= 0 && row < 5) { int selectedLevel = row * 4 + col; if (selectedLevel < 20) { startLevelSelection = selectedLevel; showLevelPopup = false; } } } } else { // Click outside popup - close it showLevelPopup = false; } } else if (showSettingsPopup) { // Click anywhere closes settings popup showSettingsPopup = false; } else { // Responsive Main menu buttons (match MenuState layout) bool isSmall = ((LOGICAL_W * logicalScale) < 700.0f); float btnW = isSmall ? (LOGICAL_W * 0.4f) : 300.0f; float btnH = isSmall ? 60.0f : 70.0f; float btnCX = LOGICAL_W * 0.5f + contentOffsetX; const float btnYOffset = 40.0f; // must match MenuState offset float btnCY = LOGICAL_H * 0.86f + contentOffsetY + btnYOffset; SDL_FRect playBtn{btnCX - btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH}; SDL_FRect levelBtn{btnCX + btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH}; if (lx >= playBtn.x && lx <= playBtn.x + playBtn.w && ly >= playBtn.y && ly <= playBtn.y + playBtn.h) { // Reset the game first with the chosen start level so HUD and // Playing state see the correct 0-based level immediately. game.reset(startLevelSelection); state = AppState::Playing; stateMgr.setState(state); } else if (lx >= levelBtn.x && lx <= levelBtn.x + levelBtn.w && ly >= levelBtn.y && ly <= levelBtn.y + levelBtn.h) { showLevelPopup = true; } // Settings button (gear icon area - top right) SDL_FRect settingsBtn{LOGICAL_W - 60, 10, 50, 30}; if (lx >= settingsBtn.x && lx <= settingsBtn.x + settingsBtn.w && ly >= settingsBtn.y && ly <= settingsBtn.y + settingsBtn.h) { showSettingsPopup = true; } } } else if (state == AppState::LevelSelect) startLevelSelection = (startLevelSelection + 1) % 20; else if (state == AppState::GameOver) { state = AppState::Menu; stateMgr.setState(state); } else if (state == AppState::Playing && showExitConfirmPopup) { // Convert mouse to logical coordinates and to content-local coords float lx = (mx - logicalVP.x) / logicalScale; float ly = (my - logicalVP.y) / logicalScale; // Compute content offsets (same as in render path) float contentW = LOGICAL_W * logicalScale; float contentH = LOGICAL_H * logicalScale; float contentOffsetX = (winW - contentW) * 0.5f / logicalScale; float contentOffsetY = (winH - contentH) * 0.5f / logicalScale; // Map to content-local logical coords (what drawing code uses) float localX = lx - contentOffsetX; float localY = ly - contentOffsetY; // Popup rect in logical coordinates (content-local) float popupW = 420, popupH = 180; float popupX = (LOGICAL_W - popupW) / 2; float popupY = (LOGICAL_H - popupH) / 2; if (localX >= popupX && localX <= popupX + popupW && localY >= popupY && localY <= popupY + popupH) { // Inside popup: two buttons Yes / No float btnW = 140, btnH = 46; float yesX = popupX + popupW * 0.25f - btnW/2.0f; float noX = popupX + popupW * 0.75f - btnW/2.0f; float btnY = popupY + popupH - 60; if (localX >= yesX && localX <= yesX + btnW && localY >= btnY && localY <= btnY + btnH) { // Yes -> go back to menu showExitConfirmPopup = false; game.reset(startLevelSelection); state = AppState::Menu; stateMgr.setState(state); } else if (localX >= noX && localX <= noX + btnW && localY >= btnY && localY <= btnY + btnH) { // No -> close popup and resume showExitConfirmPopup = false; game.setPaused(false); } } else { // Click outside popup: cancel showExitConfirmPopup = false; game.setPaused(false); } } } } else if (e.type == SDL_EVENT_MOUSE_MOTION) { float mx = (float)e.motion.x, my = (float)e.motion.y; if (mx >= logicalVP.x && my >= logicalVP.y && mx <= logicalVP.x + logicalVP.w && my <= logicalVP.y + logicalVP.h) { float lx = (mx - logicalVP.x) / logicalScale, ly = (my - logicalVP.y) / logicalScale; if (state == AppState::Menu && !showLevelPopup && !showSettingsPopup) { // Compute content offsets and responsive buttons (match MenuState) float contentW = LOGICAL_W * logicalScale; float contentH = LOGICAL_H * logicalScale; float contentOffsetX = (winW - contentW) * 0.5f / logicalScale; float contentOffsetY = (winH - contentH) * 0.5f / logicalScale; bool isSmall = ((LOGICAL_W * logicalScale) < 700.0f); float btnW = isSmall ? (LOGICAL_W * 0.4f) : 300.0f; float btnH = isSmall ? 60.0f : 70.0f; float btnCX = LOGICAL_W * 0.5f + contentOffsetX; const float btnYOffset = 40.0f; // must match MenuState offset float btnCY = LOGICAL_H * 0.86f + contentOffsetY + btnYOffset; SDL_FRect playBtn{btnCX - btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH}; SDL_FRect levelBtn{btnCX + btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH}; hoveredButton = -1; if (lx >= playBtn.x && lx <= playBtn.x + playBtn.w && ly >= playBtn.y && ly <= playBtn.y + playBtn.h) hoveredButton = 0; else if (lx >= levelBtn.x && lx <= levelBtn.x + levelBtn.w && ly >= levelBtn.y && ly <= levelBtn.y + levelBtn.h) hoveredButton = 1; } } } } } // --- Timing --- Uint64 now = SDL_GetTicks(); double frameMs = double(now - lastMs); lastMs = now; const bool *ks = SDL_GetKeyboardState(nullptr); bool left = state == AppState::Playing && ks[SDL_SCANCODE_LEFT]; bool right = state == AppState::Playing && ks[SDL_SCANCODE_RIGHT]; bool down = state == AppState::Playing && ks[SDL_SCANCODE_DOWN]; // Inform game about soft-drop state for scoring parity (1 point per cell when holding Down) if (state == AppState::Playing) game.setSoftDropping(down && !game.isPaused()); else game.setSoftDropping(false); // Handle DAS/ARR int moveDir = 0; if (left && !right) moveDir = -1; else if (right && !left) moveDir = +1; if (moveDir != 0 && !game.isPaused()) { if ((moveDir == -1 && leftHeld == false) || (moveDir == +1 && rightHeld == false)) { game.move(moveDir); moveTimerMs = DAS; } else { moveTimerMs -= frameMs; if (moveTimerMs <= 0) { game.move(moveDir); moveTimerMs += ARR; } } } else moveTimerMs = 0; leftHeld = left; rightHeld = right; if (down && !game.isPaused()) game.softDropBoost(frameMs); if (state == AppState::Playing) { if (!game.isPaused()) { game.tickGravity(frameMs); game.addElapsed(frameMs); // Update line effect and clear lines when animation completes if (lineEffect.isActive()) { if (lineEffect.update(frameMs / 1000.0f)) { // Effect is complete, now actually clear the lines game.clearCompletedLines(); } } } if (game.isGameOver()) { scores.submit(game.score(), game.lines(), game.level(), game.elapsed()); state = AppState::GameOver; stateMgr.setState(state); } } else if (state == AppState::Loading) { // Initialize audio system and start background loading on first frame if (!musicLoaded && currentTrackLoading == 0) { Audio::instance().init(); // Count actual music files first totalTracks = 0; for (int i = 1; i <= 100; ++i) { // Check up to 100 files char buf[64]; std::snprintf(buf, sizeof(buf), "assets/music/music%03d.mp3", i); // Check if file exists SDL_IOStream* file = SDL_IOFromFile(buf, "rb"); if (file) { SDL_CloseIO(file); totalTracks++; } else { break; // No more consecutive files } } // Add all found tracks to the background loading queue for (int i = 1; i <= totalTracks; ++i) { char buf[64]; std::snprintf(buf, sizeof(buf), "assets/music/music%03d.mp3", i); Audio::instance().addTrackAsync(buf); } // Start background loading thread Audio::instance().startBackgroundLoading(); currentTrackLoading = 1; // Mark as started } // Update progress based on background loading if (currentTrackLoading > 0 && !musicLoaded) { currentTrackLoading = Audio::instance().getLoadedTrackCount(); if (Audio::instance().isLoadingComplete()) { Audio::instance().shuffle(); // Shuffle once all tracks are loaded musicLoaded = true; } } // Calculate comprehensive loading progress // Phase 1: Initial assets (textures, fonts) - 20% double assetProgress = 0.2; // Assets are loaded at startup // Phase 2: Music loading - 70% double musicProgress = 0.0; if (totalTracks > 0) { musicProgress = musicLoaded ? 0.7 : std::min(0.7, (double)currentTrackLoading / totalTracks * 0.7); } // Phase 3: Final initialization - 10% double timeProgress = std::min(0.1, (now - loadStart) / 500.0); // Faster final phase loadingProgress = assetProgress + musicProgress + timeProgress; // Ensure we never exceed 100% and reach exactly 100% when everything is loaded loadingProgress = std::min(1.0, loadingProgress); if (musicLoaded && timeProgress >= 0.1) { loadingProgress = 1.0; } if (loadingProgress >= 1.0 && musicLoaded) { state = AppState::Menu; stateMgr.setState(state); } } if (state == AppState::Menu || state == AppState::Playing) { if (!musicStarted && musicLoaded) { // Music tracks are already loaded during loading screen, just start playback Audio::instance().start(); musicStarted = true; } } // Update starfields based on current state if (state == AppState::Loading) { starfield3D.update(float(frameMs / 1000.0f)); starfield3D.resize(logicalVP.w, logicalVP.h); // Update for window resize } else { starfield.update(float(frameMs / 1000.0f), logicalVP.x * 2 + logicalVP.w, logicalVP.y * 2 + logicalVP.h); } // Advance level background fade if a next texture is queued if (nextLevelBackgroundTex) { levelFadeElapsed += float(frameMs); levelFadeAlpha = std::min(1.0f, levelFadeElapsed / LEVEL_FADE_DURATION); } // Update intro animations if (state == AppState::Menu) { logoAnimCounter += frameMs * 0.0008; // Animation speed updateFireworks(frameMs); } // --- Per-state update hooks (allow states to manage logic incrementally) switch (stateMgr.getState()) { case AppState::Loading: loadingState->update(frameMs); break; case AppState::Menu: menuState->update(frameMs); break; case AppState::Playing: playingState->update(frameMs); break; default: break; } // --- Render --- SDL_SetRenderViewport(renderer, nullptr); SDL_SetRenderDrawColor(renderer, 12, 12, 16, 255); SDL_RenderClear(renderer); // Draw level-based background for gameplay, starfield for other states if (state == AppState::Playing) { // Use level-based background for gameplay with caching int currentLevel = game.level(); int bgLevel = (currentLevel > 32) ? 32 : currentLevel; // Cap at level 32 // Only load new background if level changed if (cachedLevel != bgLevel) { // Load new level background into nextLevelBackgroundTex if (nextLevelBackgroundTex) { SDL_DestroyTexture(nextLevelBackgroundTex); nextLevelBackgroundTex = nullptr; } char bgPath[256]; snprintf(bgPath, sizeof(bgPath), "assets/images/tetris_main_back_level%d.bmp", bgLevel); SDL_Surface* levelBgSurface = SDL_LoadBMP(bgPath); if (levelBgSurface) { nextLevelBackgroundTex = SDL_CreateTextureFromSurface(renderer, levelBgSurface); SDL_DestroySurface(levelBgSurface); // start fade transition levelFadeAlpha = 0.0f; levelFadeElapsed = 0.0f; cachedLevel = bgLevel; } else { // don't change textures if file missing cachedLevel = -1; } } // Draw blended backgrounds if needed if (levelBackgroundTex || nextLevelBackgroundTex) { SDL_FRect fullRect = { 0, 0, (float)logicalVP.w, (float)logicalVP.h }; // if fade in progress if (nextLevelBackgroundTex && levelFadeAlpha < 1.0f && levelBackgroundTex) { // draw current with inverse alpha SDL_SetTextureAlphaMod(levelBackgroundTex, Uint8((1.0f - levelFadeAlpha) * 255)); SDL_RenderTexture(renderer, levelBackgroundTex, nullptr, &fullRect); SDL_SetTextureAlphaMod(nextLevelBackgroundTex, Uint8(levelFadeAlpha * 255)); SDL_RenderTexture(renderer, nextLevelBackgroundTex, nullptr, &fullRect); // reset mods SDL_SetTextureAlphaMod(levelBackgroundTex, 255); SDL_SetTextureAlphaMod(nextLevelBackgroundTex, 255); } else if (nextLevelBackgroundTex && (!levelBackgroundTex || levelFadeAlpha >= 1.0f)) { // finalise swap if (levelBackgroundTex) { SDL_DestroyTexture(levelBackgroundTex); } levelBackgroundTex = nextLevelBackgroundTex; nextLevelBackgroundTex = nullptr; levelFadeAlpha = 0.0f; SDL_RenderTexture(renderer, levelBackgroundTex, nullptr, &fullRect); } else if (levelBackgroundTex) { SDL_RenderTexture(renderer, levelBackgroundTex, nullptr, &fullRect); } } } else if (state == AppState::Loading) { // Use 3D starfield for loading screen (full screen) starfield3D.draw(renderer); } else if (state == AppState::Menu) { // Use static background for menu, stretched to window; no starfield on sides if (backgroundTex) { SDL_FRect fullRect = { 0, 0, (float)logicalVP.w, (float)logicalVP.h }; SDL_RenderTexture(renderer, backgroundTex, nullptr, &fullRect); } } else { // Use regular starfield for other states (not gameplay) starfield.draw(renderer); } SDL_SetRenderViewport(renderer, &logicalVP); SDL_SetRenderScale(renderer, logicalScale, logicalScale); switch (state) { case AppState::Loading: { // Calculate actual content area (centered within the window) float contentScale = logicalScale; float contentW = LOGICAL_W * contentScale; float contentH = LOGICAL_H * contentScale; float contentOffsetX = (winW - contentW) * 0.5f / contentScale; float contentOffsetY = (winH - contentH) * 0.5f / contentScale; auto drawRect = [&](float x, float y, float w, float h, SDL_Color c) { SDL_SetRenderDrawColor(renderer,c.r,c.g,c.b,c.a); SDL_FRect fr{x + contentOffsetX, y + contentOffsetY, w, h}; SDL_RenderFillRect(renderer,&fr); }; // Calculate dimensions for perfect centering (like JavaScript version) const bool isLimitedHeight = LOGICAL_H < 450; const float logoHeight = logoTex ? (isLimitedHeight ? LOGICAL_H * 0.25f : LOGICAL_H * 0.4f) : 0; const float loadingTextHeight = 20; // Height of "LOADING" text (match JS) const float barHeight = 20; // Loading bar height (match JS) const float barPaddingVertical = isLimitedHeight ? 15 : 35; const float percentTextHeight = 24; // Height of percentage text const float spacingBetweenElements = isLimitedHeight ? 5 : 15; // Total content height const float totalContentHeight = logoHeight + (logoHeight > 0 ? spacingBetweenElements : 0) + loadingTextHeight + barPaddingVertical + barHeight + spacingBetweenElements + percentTextHeight; // Start Y position for perfect vertical centering float currentY = (LOGICAL_H - totalContentHeight) / 2.0f; // Draw logo (centered, static like JavaScript version) if (logoTex) { // Use the same original large logo dimensions as JS (we used a half-size BMP previously) const int lw = 872, lh = 273; // Cap logo width similar to JS UI.MAX_LOGO_WIDTH (600) and available screen space const float maxLogoWidth = std::min(LOGICAL_W * 0.9f, 600.0f); const float availableHeight = isLimitedHeight ? LOGICAL_H * 0.25f : LOGICAL_H * 0.4f; const float availableWidth = maxLogoWidth; const float scaleFactorWidth = availableWidth / static_cast(lw); const float scaleFactorHeight = availableHeight / static_cast(lh); const float scaleFactor = std::min(scaleFactorWidth, scaleFactorHeight); const float displayWidth = lw * scaleFactor; const float displayHeight = lh * scaleFactor; const float logoX = (LOGICAL_W - displayWidth) / 2.0f; SDL_FRect dst{logoX + contentOffsetX, currentY + contentOffsetY, displayWidth, displayHeight}; SDL_RenderTexture(renderer, logoTex, nullptr, &dst); currentY += displayHeight + spacingBetweenElements; } // Draw "LOADING" text (centered, using pixel font) const char* loadingText = "LOADING"; float textWidth = strlen(loadingText) * 12.0f; // Approximate width for pixel font float textX = (LOGICAL_W - textWidth) / 2.0f; pixelFont.draw(renderer, textX + contentOffsetX, currentY + contentOffsetY, loadingText, 1.0f, {255, 204, 0, 255}); currentY += loadingTextHeight + barPaddingVertical; // Draw loading bar (like JavaScript version) const int barW = 400, barH = 20; const int bx = (LOGICAL_W - barW) / 2; // Bar border (dark gray) - using drawRect which adds content offset drawRect(bx - 3, currentY - 3, barW + 6, barH + 6, {68, 68, 80, 255}); // Bar background (darker gray) drawRect(bx, currentY, barW, barH, {34, 34, 34, 255}); // Progress bar (gold color) drawRect(bx, currentY, int(barW * loadingProgress), barH, {255, 204, 0, 255}); currentY += barH + spacingBetweenElements; // Draw percentage text (centered, using pixel font) int percentage = int(loadingProgress * 100); char percentText[16]; std::snprintf(percentText, sizeof(percentText), "%d%%", percentage); float percentWidth = strlen(percentText) * 12.0f; // Approximate width for pixel font float percentX = (LOGICAL_W - percentWidth) / 2.0f; pixelFont.draw(renderer, percentX + contentOffsetX, currentY + contentOffsetY, percentText, 1.5f, {255, 204, 0, 255}); } break; case AppState::Menu: // Delegate full menu rendering to MenuState object now menuState->render(renderer, logicalScale, logicalVP); break; case AppState::LevelSelect: font.draw(renderer, LOGICAL_W * 0.5f - 120, 80, "SELECT LEVEL", 2.5f, SDL_Color{255, 220, 0, 255}); { char buf[64]; std::snprintf(buf, sizeof(buf), "LEVEL: %d", startLevelSelection); font.draw(renderer, LOGICAL_W * 0.5f - 80, 180, buf, 2.0f, SDL_Color{200, 240, 255, 255}); } font.draw(renderer, LOGICAL_W * 0.5f - 180, 260, "ARROWS CHANGE ENTER=OK ESC=BACK", 1.2f, SDL_Color{200, 200, 220, 255}); break; case AppState::Playing: { // Calculate actual content area (centered within the window) float contentScale = logicalScale; float contentW = LOGICAL_W * contentScale; float contentH = LOGICAL_H * contentScale; float contentOffsetX = (winW - contentW) * 0.5f / contentScale; float contentOffsetY = (winH - contentH) * 0.5f / contentScale; // Draw the game with layout matching the JavaScript version auto drawRect = [&](float x, float y, float w, float h, SDL_Color c) { SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a); SDL_FRect fr{x + contentOffsetX, y + contentOffsetY, w, h}; SDL_RenderFillRect(renderer, &fr); }; // Responsive layout that scales with window size while maintaining margins // Calculate available space considering UI panels and margins const float MIN_MARGIN = 40.0f; // Minimum margin from edges const float TOP_MARGIN = 60.0f; // Extra top margin for better spacing const float PANEL_WIDTH = 180.0f; // Width of side panels const float PANEL_SPACING = 30.0f; // Space between grid and panels const float NEXT_PIECE_HEIGHT = 120.0f; // Space reserved for next piece preview (increased) const float BOTTOM_MARGIN = 60.0f; // Space for controls text at bottom // Available width = Total width - margins - left panel - right panel - spacing const float availableWidth = LOGICAL_W - (MIN_MARGIN * 2) - (PANEL_WIDTH * 2) - (PANEL_SPACING * 2); const float availableHeight = LOGICAL_H - TOP_MARGIN - BOTTOM_MARGIN - NEXT_PIECE_HEIGHT; // Calculate block size based on available space (maintain 10:20 aspect ratio) const float maxBlockSizeW = availableWidth / Game::COLS; const float maxBlockSizeH = availableHeight / Game::ROWS; const float BLOCK_SIZE = std::min(maxBlockSizeW, maxBlockSizeH); // Ensure minimum and maximum block sizes const float finalBlockSize = std::max(20.0f, std::min(BLOCK_SIZE, 40.0f)); const float GRID_W = Game::COLS * finalBlockSize; const float GRID_H = Game::ROWS * finalBlockSize; // Calculate vertical position with proper top margin const float totalContentHeight = NEXT_PIECE_HEIGHT + GRID_H; const float availableVerticalSpace = LOGICAL_H - TOP_MARGIN - BOTTOM_MARGIN; const float verticalCenterOffset = (availableVerticalSpace - totalContentHeight) * 0.5f; const float contentStartY = TOP_MARGIN + verticalCenterOffset; // Perfect horizontal centering - center the entire layout (grid + panels) in the window const float totalLayoutWidth = PANEL_WIDTH + PANEL_SPACING + GRID_W + PANEL_SPACING + PANEL_WIDTH; const float layoutStartX = (LOGICAL_W - totalLayoutWidth) * 0.5f; // Calculate panel and grid positions from the centered layout const float statsX = layoutStartX + contentOffsetX; const float gridX = layoutStartX + PANEL_WIDTH + PANEL_SPACING + contentOffsetX; const float scoreX = layoutStartX + PANEL_WIDTH + PANEL_SPACING + GRID_W + PANEL_SPACING + contentOffsetX; // Position grid with proper top spacing const float gridY = contentStartY + NEXT_PIECE_HEIGHT + contentOffsetY; // Panel dimensions and positions const float statsY = gridY; const float statsW = PANEL_WIDTH; const float statsH = GRID_H; const float scoreY = gridY; const float scoreW = PANEL_WIDTH; // Next piece preview (above grid, centered) const float nextW = finalBlockSize * 4 + 20; const float nextH = finalBlockSize * 2 + 20; const float nextX = gridX + (GRID_W - nextW) * 0.5f; const float nextY = contentStartY + contentOffsetY; // Handle line clearing effects (now that we have grid coordinates) if (game.hasCompletedLines() && !lineEffect.isActive()) { auto completedLines = game.getCompletedLines(); lineEffect.startLineClear(completedLines, static_cast(gridX), static_cast(gridY), static_cast(finalBlockSize)); } // Draw panels with borders (like JS version) // Game grid border drawRect(gridX - 3 - contentOffsetX, gridY - 3 - contentOffsetY, GRID_W + 6, GRID_H + 6, {100, 120, 200, 255}); // Outer border drawRect(gridX - 1 - contentOffsetX, gridY - 1 - contentOffsetY, GRID_W + 2, GRID_H + 2, {60, 80, 160, 255}); // Inner border drawRect(gridX - contentOffsetX, gridY - contentOffsetY, GRID_W, GRID_H, {20, 25, 35, 255}); // Background // Left panel background (BLOCKS panel) - translucent, slightly shorter height { SDL_SetRenderDrawColor(renderer, 10, 15, 25, 160); SDL_FRect lbg{statsX - 16, gridY - 10, statsW + 32, GRID_H + 20}; SDL_RenderFillRect(renderer, &lbg); } // Right panel background (SCORE/LINES/LEVEL etc) - translucent { SDL_SetRenderDrawColor(renderer, 10, 15, 25, 160); SDL_FRect rbg{scoreX - 16, gridY - 16, scoreW + 32, GRID_H + 32}; SDL_RenderFillRect(renderer, &rbg); } // Draw grid lines (subtle lines to show cell boundaries) SDL_SetRenderDrawColor(renderer, 40, 45, 60, 255); // Slightly lighter than background // Vertical grid lines for (int x = 1; x < Game::COLS; ++x) { float lineX = gridX + x * finalBlockSize; // Remove duplicate contentOffsetX SDL_RenderLine(renderer, lineX, gridY, lineX, gridY + GRID_H); } // Horizontal grid lines for (int y = 1; y < Game::ROWS; ++y) { float lineY = gridY + y * finalBlockSize; // Remove duplicate contentOffsetY SDL_RenderLine(renderer, gridX, lineY, gridX + GRID_W, lineY); } // Block statistics panel drawRect(statsX - 3 - contentOffsetX, statsY - 3 - contentOffsetY, statsW + 6, statsH + 6, {100, 120, 200, 255}); drawRect(statsX - contentOffsetX, statsY - contentOffsetY, statsW, statsH, {30, 35, 50, 255}); // Next piece preview panel drawRect(nextX - 3 - contentOffsetX, nextY - 3 - contentOffsetY, nextW + 6, nextH + 6, {100, 120, 200, 255}); drawRect(nextX - contentOffsetX, nextY - contentOffsetY, nextW, nextH, {30, 35, 50, 255}); // Draw the game board const auto &board = game.boardRef(); for (int y = 0; y < Game::ROWS; ++y) { for (int x = 0; x < Game::COLS; ++x) { int v = board[y * Game::COLS + x]; if (v > 0) { float bx = gridX + x * finalBlockSize; float by = gridY + y * finalBlockSize; drawBlockTexture(renderer, blocksTex, bx, by, finalBlockSize, v - 1); } } } // Draw ghost piece (where current piece will land) if (!game.isPaused()) { Game::Piece ghostPiece = game.current(); // Find landing position while (true) { Game::Piece testPiece = ghostPiece; testPiece.y++; bool collision = false; // Simple collision check for (int cy = 0; cy < 4; ++cy) { for (int cx = 0; cx < 4; ++cx) { if (Game::cellFilled(testPiece, cx, cy)) { int gx = testPiece.x + cx; int gy = testPiece.y + cy; if (gy >= Game::ROWS || gx < 0 || gx >= Game::COLS || (gy >= 0 && board[gy * Game::COLS + gx] != 0)) { collision = true; break; } } } if (collision) break; } if (collision) break; ghostPiece = testPiece; } // Draw ghost piece drawPiece(renderer, blocksTex, ghostPiece, gridX, gridY, finalBlockSize, true); } // Draw the falling piece if (!game.isPaused()) { drawPiece(renderer, blocksTex, game.current(), gridX, gridY, finalBlockSize, false); } // Draw line clearing effects if (lineEffect.isActive()) { lineEffect.render(renderer, static_cast(gridX), static_cast(gridY), static_cast(finalBlockSize)); } // Draw next piece preview pixelFont.draw(renderer, nextX + 10, nextY - 20, "NEXT", 1.0f, {255, 220, 0, 255}); if (game.next().type < PIECE_COUNT) { drawSmallPiece(renderer, blocksTex, game.next().type, nextX + 10, nextY + 10, finalBlockSize * 0.6f); } // Draw block statistics (left panel) pixelFont.draw(renderer, statsX + 10, statsY + 10, "BLOCKS", 1.0f, {255, 220, 0, 255}); const auto& blockCounts = game.getBlockCounts(); int totalBlocks = 0; for (int i = 0; i < PIECE_COUNT; ++i) totalBlocks += blockCounts[i]; const char* pieceNames[] = {"I", "O", "T", "S", "Z", "J", "L"}; // Dynamic vertical cursor so bars sit below blocks cleanly float yCursor = statsY + 52; for (int i = 0; i < PIECE_COUNT; ++i) { // Baseline for this entry float py = yCursor; // Draw small piece icon (top of entry) float previewSize = finalBlockSize * 0.55f; drawSmallPiece(renderer, blocksTex, static_cast(i), statsX + 18, py, previewSize); // Compute preview height in tiles (rotation 0) int maxCy = -1; { Game::Piece prev; prev.type = static_cast(i); prev.rot = 0; prev.x = 0; prev.y = 0; for (int cy = 0; cy < 4; ++cy) { for (int cx = 0; cx < 4; ++cx) { if (Game::cellFilled(prev, cx, cy)) maxCy = std::max(maxCy, cy); } } } int tilesHigh = (maxCy >= 0 ? maxCy + 1 : 1); float previewHeight = tilesHigh * previewSize; // Count on the right, near the top (aligned with blocks) int count = blockCounts[i]; char countStr[16]; snprintf(countStr, sizeof(countStr), "%d", count); pixelFont.draw(renderer, statsX + statsW - 20, py + 6, countStr, 1.1f, {240, 240, 245, 255}); // Percentage and bar BELOW the blocks int perc = (totalBlocks > 0) ? int(std::round(100.0 * double(count) / double(totalBlocks))) : 0; char percStr[16]; snprintf(percStr, sizeof(percStr), "%d%%", perc); float barX = statsX + 12; float barY = py + previewHeight + 18.0f; float barW = statsW - 24; float barH = 6; // Percent text just above the bar (left) pixelFont.draw(renderer, barX, barY - 16, percStr, 0.8f, {230, 230, 235, 255}); // Track SDL_SetRenderDrawColor(renderer, 170, 170, 175, 200); SDL_FRect track{barX, barY, barW, barH}; SDL_RenderFillRect(renderer, &track); // Fill (piece color) SDL_Color pc = COLORS[i + 1]; SDL_SetRenderDrawColor(renderer, pc.r, pc.g, pc.b, 230); float fillW = barW * (perc / 100.0f); if (fillW < 0) fillW = 0; if (fillW > barW) fillW = barW; SDL_FRect fill{barX, barY, fillW, barH}; SDL_RenderFillRect(renderer, &fill); // Advance cursor: bar bottom + spacing yCursor = barY + barH + 18.0f; } // Draw score panel (right side), centered vertically in grid // Compute content vertical centering based on known offsets const float contentTopOffset = 0.0f; const float contentBottomOffset = 290.0f; // last line (time value) const float contentPad = 36.0f; float scoreContentH = (contentBottomOffset - contentTopOffset) + contentPad; float baseY = gridY + (GRID_H - scoreContentH) * 0.5f; pixelFont.draw(renderer, scoreX, baseY + 0, "SCORE", 1.0f, {255, 220, 0, 255}); char scoreStr[32]; snprintf(scoreStr, sizeof(scoreStr), "%d", game.score()); pixelFont.draw(renderer, scoreX, baseY + 25, scoreStr, 0.9f, {255, 255, 255, 255}); pixelFont.draw(renderer, scoreX, baseY + 70, "LINES", 1.0f, {255, 220, 0, 255}); char linesStr[16]; snprintf(linesStr, sizeof(linesStr), "%03d", game.lines()); pixelFont.draw(renderer, scoreX, baseY + 95, linesStr, 0.9f, {255, 255, 255, 255}); pixelFont.draw(renderer, scoreX, baseY + 140, "LEVEL", 1.0f, {255, 220, 0, 255}); char levelStr[16]; snprintf(levelStr, sizeof(levelStr), "%02d", game.level()); pixelFont.draw(renderer, scoreX, baseY + 165, levelStr, 0.9f, {255, 255, 255, 255}); // Next level progress // JS rules: first threshold = (startLevel+1)*10; afterwards every +10 int startLv = game.startLevelBase(); // 0-based int firstThreshold = (startLv + 1) * 10; int linesDone = game.lines(); int nextThreshold = 0; if (linesDone < firstThreshold) { nextThreshold = firstThreshold; } else { int blocksPast = linesDone - firstThreshold; nextThreshold = firstThreshold + ((blocksPast / 10) + 1) * 10; } int linesForNext = std::max(0, nextThreshold - linesDone); pixelFont.draw(renderer, scoreX, baseY + 200, "NEXT LVL", 1.0f, {255, 220, 0, 255}); char nextStr[32]; snprintf(nextStr, sizeof(nextStr), "%d LINES", linesForNext); pixelFont.draw(renderer, scoreX, baseY + 225, nextStr, 0.9f, {80, 255, 120, 255}); // Time pixelFont.draw(renderer, scoreX, baseY + 265, "TIME", 1.0f, {255, 220, 0, 255}); int totalSecs = static_cast(game.elapsed()); int mins = totalSecs / 60; int secs = totalSecs % 60; char timeStr[16]; snprintf(timeStr, sizeof(timeStr), "%02d:%02d", mins, secs); pixelFont.draw(renderer, scoreX, baseY + 290, timeStr, 0.9f, {255, 255, 255, 255}); // --- Gravity HUD: show current gravity in ms and equivalent fps (top-right) --- { char gms[64]; double gms_val = game.getGravityMs(); double gfps = gms_val > 0.0 ? (1000.0 / gms_val) : 0.0; snprintf(gms, sizeof(gms), "GRAV: %.0f ms (%.2f fps)", gms_val, gfps); pixelFont.draw(renderer, LOGICAL_W - 260, 10, gms, 0.9f, {200, 200, 220, 255}); } // Hold piece (if implemented) if (game.held().type < PIECE_COUNT) { pixelFont.draw(renderer, statsX + 10, statsY + statsH - 80, "HOLD", 1.0f, {255, 220, 0, 255}); drawSmallPiece(renderer, blocksTex, game.held().type, statsX + 60, statsY + statsH - 80, finalBlockSize * 0.6f); } // Pause overlay: don't draw pause UI when the exit-confirm popup is showing if (game.isPaused() && !showExitConfirmPopup) { // Semi-transparent overlay SDL_SetRenderDrawColor(renderer, 0, 0, 0, 180); SDL_FRect pauseOverlay{0, 0, LOGICAL_W, LOGICAL_H}; SDL_RenderFillRect(renderer, &pauseOverlay); // Pause text pixelFont.draw(renderer, LOGICAL_W * 0.5f - 80, LOGICAL_H * 0.5f - 20, "PAUSED", 2.0f, {255, 255, 255, 255}); pixelFont.draw(renderer, LOGICAL_W * 0.5f - 120, LOGICAL_H * 0.5f + 30, "Press P to resume", 0.8f, {200, 200, 220, 255}); } // Exit confirmation popup (modal) if (showExitConfirmPopup) { // Compute content offsets for consistent placement across window sizes float contentW = LOGICAL_W * logicalScale; float contentH = LOGICAL_H * logicalScale; float contentOffsetX = (winW - contentW) * 0.5f / logicalScale; float contentOffsetY = (winH - contentH) * 0.5f / logicalScale; float popupW = 420, popupH = 180; float popupX = (LOGICAL_W - popupW) / 2; float popupY = (LOGICAL_H - popupH) / 2; // Dim entire window (use window coordinates so it always covers 100% of the target) SDL_SetRenderViewport(renderer, nullptr); SDL_SetRenderDrawColor(renderer, 0, 0, 0, 200); SDL_FRect fullWin{0.f, 0.f, (float)winW, (float)winH}; SDL_RenderFillRect(renderer, &fullWin); // Restore logical viewport for drawing content-local popup SDL_SetRenderViewport(renderer, &logicalVP); // Draw popup box (drawRect will apply contentOffset internally) drawRect(popupX - 4, popupY - 4, popupW + 8, popupH + 8, {60, 70, 90, 255}); drawRect(popupX, popupY, popupW, popupH, {20, 22, 28, 240}); // Center title and body text inside popup (use pixelFont for retro P2 font) const std::string title = "Exit game?"; const std::string line1 = "Are you sure you want to"; const std::string line2 = "leave the current game?"; int wTitle=0,hTitle=0; pixelFont.measure( title, 1.6f, wTitle, hTitle); int wL1=0,hL1=0; pixelFont.measure( line1, 0.9f, wL1, hL1); int wL2=0,hL2=0; pixelFont.measure( line2, 0.9f, wL2, hL2); float titleX = popupX + (popupW - (float)wTitle) * 0.5f; float l1X = popupX + (popupW - (float)wL1) * 0.5f; float l2X = popupX + (popupW - (float)wL2) * 0.5f; pixelFont.draw(renderer, titleX + contentOffsetX, popupY + contentOffsetY + 20, title, 1.6f, {255, 220, 0, 255}); pixelFont.draw(renderer, l1X + contentOffsetX, popupY + contentOffsetY + 60, line1, 0.9f, SDL_Color{220,220,230,255}); pixelFont.draw(renderer, l2X + contentOffsetX, popupY + contentOffsetY + 84, line2, 0.9f, SDL_Color{220,220,230,255}); // Buttons (center labels inside buttons) - use pixelFont for labels float btnW = 140, btnH = 46; float yesX = popupX + popupW * 0.25f - btnW/2.0f; float noX = popupX + popupW * 0.75f - btnW/2.0f; float btnY = popupY + popupH - 60; drawRect(yesX - 2, btnY - 2, btnW + 4, btnH + 4, {100, 120, 140, 255}); drawRect(yesX, btnY, btnW, btnH, {200, 60, 60, 255}); const std::string yes = "YES"; int wy=0,hy=0; pixelFont.measure( yes, 1.0f, wy, hy); pixelFont.draw(renderer, yesX + (btnW - (float)wy) * 0.5f + contentOffsetX, btnY + (btnH - (float)hy) * 0.5f + contentOffsetY, yes, 1.0f, {255,255,255,255}); drawRect(noX - 2, btnY - 2, btnW + 4, btnH + 4, {100, 120, 140, 255}); drawRect(noX, btnY, btnW, btnH, {80, 140, 80, 255}); const std::string no = "NO"; int wn=0,hn=0; pixelFont.measure( no, 1.0f, wn, hn); pixelFont.draw(renderer, noX + (btnW - (float)wn) * 0.5f + contentOffsetX, btnY + (btnH - (float)hn) * 0.5f + contentOffsetY, no, 1.0f, {255,255,255,255}); } // Controls hint at bottom font.draw(renderer, 20, LOGICAL_H - 30, "ARROWS=Move Z/X=Rotate C=Hold SPACE=Drop P=Pause ESC=Menu", 1.0f, {150, 150, 170, 255}); } break; case AppState::GameOver: font.draw(renderer, LOGICAL_W * 0.5f - 120, 140, "GAME OVER", 3.0f, SDL_Color{255, 80, 60, 255}); { char buf[128]; std::snprintf(buf, sizeof(buf), "SCORE %d LINES %d LEVEL %d", game.score(), game.lines(), game.level()); font.draw(renderer, LOGICAL_W * 0.5f - 120, 220, buf, 1.2f, SDL_Color{220, 220, 230, 255}); } font.draw(renderer, LOGICAL_W * 0.5f - 120, 270, "PRESS ENTER / SPACE", 1.2f, SDL_Color{200, 200, 220, 255}); break; } SDL_RenderPresent(renderer); SDL_SetRenderScale(renderer, 1.f, 1.f); } if (logoTex) SDL_DestroyTexture(logoTex); if (backgroundTex) SDL_DestroyTexture(backgroundTex); if (levelBackgroundTex) SDL_DestroyTexture(levelBackgroundTex); if (blocksTex) SDL_DestroyTexture(blocksTex); lineEffect.shutdown(); Audio::instance().shutdown(); SoundEffectManager::instance().shutdown(); font.shutdown(); TTF_Quit(); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); return 0; }