From 39da4484cad86264d26e29acf304022690825884 Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Sun, 23 Nov 2025 08:28:44 +0100 Subject: [PATCH] cleaning code --- CMakeLists.txt | 54 -- src/main_dist.cpp | 1732 --------------------------------------------- src/main_new.cpp | 35 - 3 files changed, 1821 deletions(-) delete mode 100644 src/main_dist.cpp delete mode 100644 src/main_new.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c6b1dc9..46e4b36 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,58 +121,4 @@ target_include_directories(tetris PRIVATE ${CMAKE_SOURCE_DIR}/src/persistence ${CMAKE_SOURCE_DIR}/src/core ${CMAKE_SOURCE_DIR}/src/states -) - -# Experimental refactored version (for testing new architecture) -add_executable(tetris_refactored - src/main_new.cpp - src/gameplay/core/Game.cpp - src/core/GravityManager.cpp - src/core/state/StateManager.cpp - # New core architecture classes - src/core/application/ApplicationManager.cpp - src/core/input/InputManager.cpp - src/core/assets/AssetManager.cpp - src/core/GlobalState.cpp - src/graphics/renderers/RenderManager.cpp - src/persistence/Scores.cpp - src/graphics/effects/Starfield.cpp - src/graphics/effects/Starfield3D.cpp - src/graphics/ui/Font.cpp - src/graphics/renderers/GameRenderer.cpp - src/audio/Audio.cpp - src/gameplay/effects/LineEffect.cpp - src/audio/SoundEffect.cpp - # State implementations - src/states/LoadingState.cpp - src/states/MenuState.cpp - src/states/OptionsState.cpp - src/states/LevelSelectorState.cpp - src/states/PlayingState.cpp -) - -if (WIN32) - # Embed the application icon into the refactored executable too - target_sources(tetris_refactored PRIVATE src/app_icon.rc) - add_dependencies(tetris_refactored copy_favicon) - add_custom_command(TARGET tetris_refactored POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different ${FAVICON_SRC} $/favicon.ico - COMMENT "Copy favicon.ico next to refactored executable" - ) -endif() - -target_link_libraries(tetris_refactored PRIVATE SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image cpr::cpr nlohmann_json::nlohmann_json) - -if (WIN32) - target_link_libraries(tetris_refactored PRIVATE mfplat mfreadwrite mfuuid) -endif() - -target_include_directories(tetris_refactored PRIVATE - ${CMAKE_SOURCE_DIR}/src - ${CMAKE_SOURCE_DIR}/src/audio - ${CMAKE_SOURCE_DIR}/src/gameplay - ${CMAKE_SOURCE_DIR}/src/graphics - ${CMAKE_SOURCE_DIR}/src/persistence - ${CMAKE_SOURCE_DIR}/src/core - ${CMAKE_SOURCE_DIR}/src/states ) \ No newline at end of file diff --git a/src/main_dist.cpp b/src/main_dist.cpp deleted file mode 100644 index 9bcec03..0000000 --- a/src/main_dist.cpp +++ /dev/null @@ -1,1732 +0,0 @@ -// 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 - -#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/OptionsState.h" -#include "states/LevelSelectorState.h" -#include "states/PlayingState.h" -#include "audio/MenuWrappers.h" -#include "utils/ImagePathResolver.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); -} - -static SDL_Texture* loadTextureFromImage(SDL_Renderer* renderer, const std::string& path, int* outW = nullptr, int* outH = nullptr) { - if (!renderer) { - return nullptr; - } - - const std::string resolvedPath = AssetPath::resolveImagePath(path); - SDL_Surface* surface = IMG_Load(resolvedPath.c_str()); - if (!surface) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load image %s (resolved: %s): %s", path.c_str(), resolvedPath.c_str(), SDL_GetError()); - return nullptr; - } - - if (outW) { *outW = surface->w; } - if (outH) { *outH = surface->h; } - - SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface); - SDL_DestroySurface(surface); - - if (!texture) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create texture from %s: %s", resolvedPath.c_str(), SDL_GetError()); - return nullptr; - } - - if (resolvedPath != path) { - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loaded %s via %s", path.c_str(), resolvedPath.c_str()); - } - - return texture; -} - -struct LevelBackgroundFader { - SDL_Texture* currentTex = nullptr; - SDL_Texture* nextTex = nullptr; - int currentLevel = -1; - int queuedLevel = -1; - float fadeElapsedMs = 0.0f; - float fadeDurationMs = 3500.0f; -}; - -static void destroyTexture(SDL_Texture*& tex) { - if (tex) { - SDL_DestroyTexture(tex); - tex = nullptr; - } -} - -static bool queueLevelBackground(LevelBackgroundFader& fader, SDL_Renderer* renderer, int level) { - if (!renderer) { - return false; - } - - level = std::clamp(level, 0, 32); - if (fader.currentLevel == level || fader.queuedLevel == level) { - return true; - } - - char bgPath[256]; - std::snprintf(bgPath, sizeof(bgPath), "assets/images/tetris_main_back_level%d.jpg", level); - - SDL_Texture* newTexture = loadTextureFromImage(renderer, bgPath); - if (!newTexture) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to queue background for level %d: %s", level, bgPath); - return false; - } - - destroyTexture(fader.nextTex); - fader.nextTex = newTexture; - fader.queuedLevel = level; - fader.fadeElapsedMs = 0.0f; - - if (!fader.currentTex) { - fader.currentTex = fader.nextTex; - fader.currentLevel = fader.queuedLevel; - fader.nextTex = nullptr; - fader.queuedLevel = -1; - } - - return true; -} - -static void updateLevelBackgroundFade(LevelBackgroundFader& fader, float frameMs) { - if (!fader.currentTex || !fader.nextTex) { - return; - } - - fader.fadeElapsedMs += frameMs; - if (fader.fadeElapsedMs >= fader.fadeDurationMs) { - destroyTexture(fader.currentTex); - fader.currentTex = fader.nextTex; - fader.currentLevel = fader.queuedLevel; - fader.nextTex = nullptr; - fader.queuedLevel = -1; - fader.fadeElapsedMs = 0.0f; - } -} - -static void renderLevelBackgrounds(const LevelBackgroundFader& fader, SDL_Renderer* renderer, int winW, int winH) { - if (!renderer) { - return; - } - - SDL_FRect fullRect{0.f, 0.f, static_cast(winW), static_cast(winH)}; - - if (fader.currentTex && fader.nextTex) { - const float duration = std::max(1.0f, fader.fadeDurationMs); - const float alpha = std::clamp(fader.fadeElapsedMs / duration, 0.0f, 1.0f); - - SDL_SetTextureAlphaMod(fader.currentTex, Uint8((1.0f - alpha) * 255.0f)); - SDL_RenderTexture(renderer, fader.currentTex, nullptr, &fullRect); - SDL_SetTextureAlphaMod(fader.currentTex, 255); - - SDL_SetTextureAlphaMod(fader.nextTex, Uint8(alpha * 255.0f)); - SDL_RenderTexture(renderer, fader.nextTex, nullptr, &fullRect); - SDL_SetTextureAlphaMod(fader.nextTex, 255); - } else if (fader.currentTex) { - SDL_RenderTexture(renderer, fader.currentTex, nullptr, &fullRect); - } else if (fader.nextTex) { - SDL_RenderTexture(renderer, fader.nextTex, nullptr, &fullRect); - } -} - -static void resetLevelBackgrounds(LevelBackgroundFader& fader) { - destroyTexture(fader.currentTex); - destroyTexture(fader.nextTex); - fader.currentLevel = -1; - fader.queuedLevel = -1; - fader.fadeElapsedMs = 0.0f; -} - -// Hover state for level popup ( -1 = none, 0..19 = hovered level ) -// Now managed by LevelSelectorState - -// ...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 drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musicEnabled); - -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 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 showSettingsPopup = false; -static bool showExitConfirmPopup = false; -static int exitPopupSelectedButton = 1; // 0 = YES, 1 = NO -static bool musicEnabled = true; -static int hoveredButton = -1; // -1 = none, 0 = play, 1 = level, 2 = settings -static std::string playerName = "PLAYER"; - -// ----------------------------------------------------------------------------- -// 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 assets via SDL_image so we can use compressed formats - SDL_Texture* logoTex = loadTextureFromImage(renderer, "assets/images/logo.bmp"); - // Load small logo (used by Menu to show whole logo) - int logoSmallW = 0, logoSmallH = 0; - SDL_Texture* logoSmallTex = loadTextureFromImage(renderer, "assets/images/logo_small.bmp", &logoSmallW, &logoSmallH); - - // Load background using SDL_image (prefers JPEG) - SDL_Texture* backgroundTex = loadTextureFromImage(renderer, "assets/images/main_background.bmp"); - - // 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 - LevelBackgroundFader levelBackgrounds; - - // Default start level selection: 0 (declare here so it's in scope for all handlers) - int startLevelSelection = 0; - - // Load blocks texture using SDL_image (falls back to procedural blocks if necessary) - SDL_Texture* blocksTex = loadTextureFromImage(renderer, "assets/images/blocks90px_001.bmp"); - // No global exposure of blocksTex; states receive textures via StateContext. - - if (!blocksTex) { - // Create a 630x90 texture (7 blocks * 90px each) - blocksTex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, 630, 90); - - // Generate blocks by drawing colored rectangles to texture - SDL_SetRenderTarget(renderer, blocksTex); - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0); - SDL_RenderClear(renderer); - - for (int i = 0; i < PIECE_COUNT; ++i) { - SDL_Color c = COLORS[i + 1]; - SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a); - SDL_FRect rect{(float)(i * 90), 0, 90, 90}; - SDL_RenderFillRect(renderer, &rect); - } - - SDL_SetRenderTarget(renderer, nullptr); - } - - 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.showSettingsPopup = &showSettingsPopup; - ctx.showExitConfirmPopup = &showExitConfirmPopup; - ctx.exitPopupSelectedButton = &exitPopupSelectedButton; - ctx.playerName = &playerName; - ctx.fullscreenFlag = &isFullscreen; - ctx.applyFullscreen = [window, &isFullscreen](bool enable) { - SDL_SetWindowFullscreen(window, enable ? SDL_WINDOW_FULLSCREEN : 0); - isFullscreen = enable; - }; - ctx.queryFullscreen = [window]() -> bool { - return (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) != 0; - }; - ctx.requestQuit = [&running]() { - running = false; - }; - - // Instantiate state objects - auto loadingState = std::make_unique(ctx); - auto menuState = std::make_unique(ctx); - auto optionsState = std::make_unique(ctx); - auto levelSelectorState = 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(); }); - - stateMgr.registerHandler(AppState::Options, [&](const SDL_Event& e){ optionsState->handleEvent(e); }); - stateMgr.registerOnEnter(AppState::Options, [&](){ optionsState->onEnter(); }); - stateMgr.registerOnExit(AppState::Options, [&](){ optionsState->onExit(); }); - - stateMgr.registerHandler(AppState::LevelSelector, [&](const SDL_Event& e){ levelSelectorState->handleEvent(e); }); - stateMgr.registerOnEnter(AppState::LevelSelector, [&](){ levelSelectorState->onEnter(); }); - stateMgr.registerOnExit(AppState::LevelSelector, [&](){ levelSelectorState->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 || e.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) - running = false; - else { - // Route event to state manager handlers for per-state logic - stateMgr.handleEvent(e); - // Keep the local `state` variable in sync with StateManager in case - // a state handler requested a transition (handlers may call - // stateMgr.setState()). Many branches below rely on the local - // `state` variable, so update it immediately after handling. - state = stateMgr.getState(); - - // 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 (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; - float spacing = isSmall ? btnW * 1.15f : btnW * 1.05f; - std::array buttonRects{}; - for (int i = 0; i < 4; ++i) { - float center = btnCX + (static_cast(i) - 1.5f) * spacing; - buttonRects[i] = SDL_FRect{center - btnW / 2.0f, btnCY - btnH / 2.0f, btnW, btnH}; - } - - auto pointInRect = [&](const SDL_FRect& r) { - return lx >= r.x && lx <= r.x + r.w && ly >= r.y && ly <= r.y + r.h; - }; - - if (pointInRect(buttonRects[0])) { - game.reset(startLevelSelection); - state = AppState::Playing; - stateMgr.setState(state); - } else if (pointInRect(buttonRects[1])) { - state = AppState::LevelSelector; - stateMgr.setState(state); - } else if (pointInRect(buttonRects[2])) { - state = AppState::Options; - stateMgr.setState(state); - } else if (pointInRect(buttonRects[3])) { - showExitConfirmPopup = true; - exitPopupSelectedButton = 1; - } - - 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) { - 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.0f; - float popupH = 230.0f; - float popupX = (LOGICAL_W - popupW) * 0.5f + contentOffsetX; - float popupY = (LOGICAL_H - popupH) * 0.5f + contentOffsetY; - float btnW = 140.0f; - float btnH = 50.0f; - float yesX = popupX + popupW * 0.3f - btnW / 2.0f; - float noX = popupX + popupW * 0.7f - btnW / 2.0f; - float btnY = popupY + popupH - btnH - 30.0f; - bool insidePopup = lx >= popupX && lx <= popupX + popupW && ly >= popupY && ly <= popupY + popupH; - if (insidePopup) { - if (lx >= yesX && lx <= yesX + btnW && ly >= btnY && ly <= btnY + btnH) { - showExitConfirmPopup = false; - running = false; - } else if (lx >= noX && lx <= noX + btnW && ly >= btnY && ly <= btnY + btnH) { - showExitConfirmPopup = false; - } - } else { - showExitConfirmPopup = 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 && !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; - float spacing = isSmall ? btnW * 1.15f : btnW * 1.05f; - hoveredButton = -1; - for (int i = 0; i < 4; ++i) { - float center = btnCX + (static_cast(i) - 1.5f) * spacing; - SDL_FRect rect{center - btnW / 2.0f, btnCY - btnH / 2.0f, btnW, btnH}; - if (lx >= rect.x && lx <= rect.x + rect.w && ly >= rect.y && ly <= rect.y + rect.h) { - hoveredButton = i; - break; - } - } - } - } - } - } - } - - // --- 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 - updateLevelBackgroundFade(levelBackgrounds, float(frameMs)); - - // 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::LevelSelector: - levelSelectorState->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) { - int bgLevel = std::clamp(game.level(), 0, 32); - queueLevelBackground(levelBackgrounds, renderer, bgLevel); - renderLevelBackgrounds(levelBackgrounds, renderer, winW, winH); - } else if (state == AppState::Loading) { - // Use 3D starfield for loading screen (full screen) - starfield3D.draw(renderer); - } else if (state == AppState::Menu || state == AppState::LevelSelector || state == AppState::Options) { - // Use static background for menu, stretched to window; no starfield on sides - if (backgroundTex) { - SDL_FRect fullRect = { 0, 0, (float)winW, (float)winH }; - 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::LevelSelector: - // Delegate level selector rendering to LevelSelectorState - levelSelectorState->render(renderer, logicalScale, logicalVP); - break; - case AppState::LevelSelect: - { - const std::string title = "SELECT LEVEL"; - int tW = 0, tH = 0; - font.measure(title, 2.5f, tW, tH); - float titleX = (LOGICAL_W - (float)tW) / 2.0f; - font.draw(renderer, titleX, 80, title, 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); - resetLevelBackgrounds(levelBackgrounds); - if (blocksTex) - SDL_DestroyTexture(blocksTex); - if (logoSmallTex) - SDL_DestroyTexture(logoSmallTex); - lineEffect.shutdown(); - Audio::instance().shutdown(); - SoundEffectManager::instance().shutdown(); - font.shutdown(); - TTF_Quit(); - SDL_DestroyRenderer(renderer); - SDL_DestroyWindow(window); - SDL_Quit(); - return 0; -} diff --git a/src/main_new.cpp b/src/main_new.cpp deleted file mode 100644 index 5eff08d..0000000 --- a/src/main_new.cpp +++ /dev/null @@ -1,35 +0,0 @@ -// main_new.cpp - Simplified application entry point for the refactored build -// Sets critical SDL hints (DPI awareness, scaling) before initializing subsystems - -#include -#include -#include "core/application/ApplicationManager.h" -#include - -int main(int argc, char* argv[]) { - // Ensure per-monitor DPI awareness so fullscreen/input mapping is correct on high-DPI displays - SDL_SetHint("SDL_WINDOWS_DPI_AWARENESS", "permonitorv2"); - // Keep pixel art crisp when scaling logical content - SDL_SetHint("SDL_RENDER_SCALE_QUALITY", "nearest"); - - ApplicationManager app; - if (!app.initialize(argc, argv)) { - std::cerr << "Failed to initialize application" << std::endl; - return 1; - } - - try { - app.run(); - // Ensure orderly teardown before C++ static destruction - app.shutdown(); - } catch (const std::exception& ex) { - std::cerr << "Fatal error: " << ex.what() << std::endl; - app.shutdown(); - return 2; - } catch (...) { - std::cerr << "Unknown fatal error" << std::endl; - app.shutdown(); - return 3; - } - return 0; -}