diff --git a/CMakeLists.txt b/CMakeLists.txt index 07b1670..93ff572 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -137,11 +137,11 @@ add_executable(tetris_refactored src/audio/Audio.cpp src/gameplay/LineEffect.cpp src/audio/SoundEffect.cpp - # State implementations (temporarily excluded - depend on main.cpp functions) - # src/states/LoadingState.cpp - # src/states/MenuState.cpp - # src/states/LevelSelectorState.cpp - # src/states/PlayingState.cpp + # State implementations + src/states/LoadingState.cpp + src/states/MenuState.cpp + src/states/LevelSelectorState.cpp + src/states/PlayingState.cpp ) if (WIN32) diff --git a/src/core/ApplicationManager.cpp b/src/core/ApplicationManager.cpp index 371c88a..8911a44 100644 --- a/src/core/ApplicationManager.cpp +++ b/src/core/ApplicationManager.cpp @@ -1,16 +1,37 @@ #include "ApplicationManager.h" #include "StateManager.h" #include "InputManager.h" +#include +#include "../audio/Audio.h" +#include "../audio/SoundEffect.h" +#include "../persistence/Scores.h" +#include "../states/State.h" +#include "../states/LoadingState.h" +#include "../states/MenuState.h" +#include "../states/LevelSelectorState.h" +#include "../states/PlayingState.h" #include "AssetManager.h" #include "Config.h" #include "GlobalState.h" #include "../graphics/RenderManager.h" +#include "../graphics/Font.h" +#include "../graphics/Starfield3D.h" +#include "../graphics/Starfield.h" +#include "../gameplay/Game.h" +#include "../gameplay/LineEffect.h" #include #include #include +#include +#include ApplicationManager::ApplicationManager() = default; +static void traceFile(const char* msg) { + std::ofstream f("tetris_trace.log", std::ios::app); + if (f) f << msg << "\n"; +} + ApplicationManager::~ApplicationManager() { if (m_initialized) { shutdown(); @@ -64,8 +85,11 @@ void ApplicationManager::run() { } SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Starting main application loop"); + traceFile("Main loop starting"); while (m_running) { + SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Main loop iteration start: m_running=%d", m_running ? 1 : 0); + traceFile("Main loop iteration"); // Calculate delta time uint64_t currentTime = SDL_GetTicks(); float deltaTime = (currentTime - m_lastFrameTime) / 1000.0f; @@ -81,6 +105,7 @@ void ApplicationManager::run() { if (m_running) { update(deltaTime); + traceFile("about to call render"); render(); } } @@ -150,9 +175,66 @@ bool ApplicationManager::initializeManagers() { return false; } + // Ensure SoundEffectManager is initialized early so SFX loads work + SoundEffectManager::instance().init(); + // Create StateManager (will be enhanced in next steps) m_stateManager = std::make_unique(AppState::Loading); + // Create and initialize starfields + m_starfield3D = std::make_unique(); + m_starfield3D->init(Config::Logical::WIDTH, Config::Logical::HEIGHT, 200); + + m_starfield = std::make_unique(); + m_starfield->init(Config::Logical::WIDTH, Config::Logical::HEIGHT, 50); + + // Register InputManager handlers to forward events to StateManager so + // state-specific event handlers receive SDL_Event objects just like main.cpp. + if (m_inputManager && m_stateManager) { + m_inputManager->registerKeyHandler([this](SDL_Scancode sc, bool pressed){ + if (!m_stateManager) return; + SDL_Event ev{}; + ev.type = pressed ? SDL_EVENT_KEY_DOWN : SDL_EVENT_KEY_UP; + ev.key.scancode = sc; + ev.key.repeat = 0; + m_stateManager->handleEvent(ev); + }); + + m_inputManager->registerMouseButtonHandler([this](int button, bool pressed, float x, float y){ + if (!m_stateManager) return; + SDL_Event ev{}; + ev.type = pressed ? SDL_EVENT_MOUSE_BUTTON_DOWN : SDL_EVENT_MOUSE_BUTTON_UP; + ev.button.button = button; + ev.button.x = int(x); + ev.button.y = int(y); + m_stateManager->handleEvent(ev); + }); + + m_inputManager->registerMouseMotionHandler([this](float x, float y, float dx, float dy){ + if (!m_stateManager) return; + SDL_Event ev{}; + ev.type = SDL_EVENT_MOUSE_MOTION; + ev.motion.x = int(x); + ev.motion.y = int(y); + ev.motion.xrel = int(dx); + ev.motion.yrel = int(dy); + m_stateManager->handleEvent(ev); + }); + + m_inputManager->registerWindowEventHandler([this](const SDL_WindowEvent& we){ + if (!m_stateManager) return; + SDL_Event ev{}; + ev.type = SDL_EVENT_WINDOW_RESIZED; // generic mapping; handlers can inspect inner fields + ev.window = we; + m_stateManager->handleEvent(ev); + }); + + m_inputManager->registerQuitHandler([this](){ + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "InputManager quit handler invoked"); + SDL_Event ev{}; ev.type = SDL_EVENT_QUIT; if (m_stateManager) m_stateManager->handleEvent(ev); + }); + } + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Managers initialized successfully"); return true; } @@ -167,8 +249,17 @@ bool ApplicationManager::initializeGame() { AssetManager::LoadingTask blocksTask{AssetManager::LoadingTask::TEXTURE, "blocks", Config::Assets::BLOCKS_BMP}; AssetManager::LoadingTask fontTask{AssetManager::LoadingTask::FONT, "main_font", Config::Fonts::DEFAULT_FONT_PATH, Config::Fonts::DEFAULT_FONT_SIZE}; AssetManager::LoadingTask pixelFontTask{AssetManager::LoadingTask::FONT, "pixel_font", Config::Fonts::PIXEL_FONT_PATH, Config::Fonts::PIXEL_FONT_SIZE}; - - // Add tasks to AssetManager + + // Pre-load the pixel (retro) font synchronously so the loading screen can render text immediately + if (!m_assetManager->getFont("pixel_font")) { + if (m_assetManager->loadFont("pixel_font", Config::Fonts::PIXEL_FONT_PATH, Config::Fonts::PIXEL_FONT_SIZE)) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Preloaded pixel_font for loading screen"); + } else { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Failed to preload pixel_font; loading screen will fallback to main_font"); + } + } + + // Add tasks to AssetManager (pixel font task will be skipped if already loaded) m_assetManager->addLoadingTask(logoTask); m_assetManager->addLoadingTask(backgroundTask); m_assetManager->addLoadingTask(blocksTask); @@ -180,7 +271,7 @@ bool ApplicationManager::initializeGame() { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Asset loading progress: %.1f%%", progress * 100.0f); }); - // Load sound effects with fallback + // Load sound effects with fallback (SoundEffectManager already initialized) m_assetManager->loadSoundEffectWithFallback("clear_line", "clear_line"); m_assetManager->loadSoundEffectWithFallback("nice_combo", "nice_combo"); m_assetManager->loadSoundEffectWithFallback("amazing", "amazing"); @@ -188,35 +279,559 @@ bool ApplicationManager::initializeGame() { // Start background music loading m_assetManager->startBackgroundMusicLoading(); - // Initialize a basic test render handler that shows loaded assets - m_stateManager->registerRenderHandler(AppState::Loading, - [this](RenderManager& renderer) { - // Simple test render - just clear screen and show loaded assets - renderer.clear(20, 30, 40, 255); - - // Try to render background if loaded - SDL_Texture* background = m_assetManager->getTexture("background"); - if (background) { - SDL_FRect bgRect = { 0, 0, Config::Logical::WIDTH, Config::Logical::HEIGHT }; - renderer.renderTexture(background, nullptr, &bgRect); - } - - // Try to render logo if loaded - SDL_Texture* logo = m_assetManager->getTexture("logo"); - if (logo) { - SDL_FRect logoRect = { 300, 200, 600, 200 }; - renderer.renderTexture(logo, nullptr, &logoRect); - } - - // Show asset loading status - SDL_FRect statusRect = { 50, 50, 400, 30 }; - renderer.renderRect(statusRect, 0, 100, 200, 200); + // Create and populate shared StateContext similar to main.cpp so states like MenuState + // receive the same pointers and flags they expect. + // Create ScoreManager and load scores + m_scoreManager = std::make_unique(); + if (m_scoreManager) m_scoreManager->load(); + + // Create gameplay and line effect objects to populate StateContext like main.cpp + m_lineEffect = std::make_unique(); + m_game = std::make_unique(m_startLevelSelection); + // Wire up sound callbacks as main.cpp did + if (m_game) { + m_game->setSoundCallback([&](int linesCleared){ + SoundEffectManager::instance().playSound("clear_line", 1.0f); + // voice lines handled via asset manager loaded sounds + if (linesCleared == 2) SoundEffectManager::instance().playRandomSound({"nice_combo"}, 1.0f); + else if (linesCleared == 3) SoundEffectManager::instance().playRandomSound({"great_move"}, 1.0f); + else if (linesCleared == 4) SoundEffectManager::instance().playRandomSound({"amazing"}, 1.0f); }); + m_game->setLevelUpCallback([&](int newLevel){ + SoundEffectManager::instance().playSound("lets_go", 1.0f); + }); + } + + // Prepare a StateContext-like struct by setting up handlers that capture + // pointers and flags. State objects in this refactor expect these to be + // available via StateManager event/update/render hooks, so we'll store them + // as lambdas that reference members here. + + // Start background music loading similar to main.cpp: Audio init + file discovery + Audio::instance().init(); + // Discover available tracks (up to 100) and queue for background loading + m_totalTracks = 0; + for (int i = 1; i <= 100; ++i) { + char buf[128]; + std::snprintf(buf, sizeof(buf), "assets/music/music%03d.mp3", i); + // Use simple file existence check via std::filesystem + if (std::filesystem::exists(buf)) { + Audio::instance().addTrackAsync(buf); + ++m_totalTracks; + } else { + break; + } + } + if (m_totalTracks > 0) { + Audio::instance().startBackgroundLoading(); + m_currentTrackLoading = 1; // mark started + } + + // Instantiate state objects and populate a StateContext similar to main.cpp + // so that existing state classes (MenuState, LoadingState, etc.) receive + // the resources they expect. + { + m_stateContext.stateManager = m_stateManager.get(); + m_stateContext.game = m_game.get(); + m_stateContext.scores = m_scoreManager.get(); + m_stateContext.starfield = m_starfield.get(); + m_stateContext.starfield3D = m_starfield3D.get(); + m_stateContext.font = (FontAtlas*)m_assetManager->getFont("main_font"); + m_stateContext.pixelFont = (FontAtlas*)m_assetManager->getFont("pixel_font"); + m_stateContext.lineEffect = m_lineEffect.get(); + m_stateContext.logoTex = m_assetManager->getTexture("logo"); + // Attempt to load a small logo variant if present to match original UX + SDL_Texture* logoSmall = m_assetManager->getTexture("logo_small"); + if (!logoSmall) { + // Try to load image from disk and register with AssetManager + if (m_assetManager->loadTexture("logo_small", "assets/images/logo_small.bmp")) { + logoSmall = m_assetManager->getTexture("logo_small"); + } + } + m_stateContext.logoSmallTex = logoSmall; + if (logoSmall) { + int w = 0, h = 0; if (m_renderManager) m_renderManager->getTextureSize(logoSmall, w, h); + m_stateContext.logoSmallW = w; m_stateContext.logoSmallH = h; + } else { m_stateContext.logoSmallW = 0; m_stateContext.logoSmallH = 0; } + m_stateContext.backgroundTex = m_assetManager->getTexture("background"); + m_stateContext.blocksTex = m_assetManager->getTexture("blocks"); + m_stateContext.musicEnabled = &m_musicEnabled; + m_stateContext.startLevelSelection = &m_startLevelSelection; + m_stateContext.hoveredButton = &m_hoveredButton; + m_stateContext.showSettingsPopup = &m_showSettingsPopup; + m_stateContext.showExitConfirmPopup = &m_showExitConfirmPopup; + + // Create state instances + m_loadingState = std::make_unique(m_stateContext); + m_menuState = std::make_unique(m_stateContext); + m_levelSelectorState = std::make_unique(m_stateContext); + m_playingState = std::make_unique(m_stateContext); + + // Register handlers that forward to these state objects + if (m_stateManager) { + m_stateManager->registerEventHandler(AppState::Loading, [this](const SDL_Event& e){ if (m_loadingState) m_loadingState->handleEvent(e); }); + m_stateManager->registerOnEnter(AppState::Loading, [this](){ if (m_loadingState) m_loadingState->onEnter(); }); + m_stateManager->registerOnExit(AppState::Loading, [this](){ if (m_loadingState) m_loadingState->onExit(); }); + + m_stateManager->registerEventHandler(AppState::Menu, [this](const SDL_Event& e){ if (m_menuState) m_menuState->handleEvent(e); }); + m_stateManager->registerOnEnter(AppState::Menu, [this](){ if (m_menuState) m_menuState->onEnter(); }); + m_stateManager->registerOnExit(AppState::Menu, [this](){ if (m_menuState) m_menuState->onExit(); }); + + m_stateManager->registerEventHandler(AppState::LevelSelector, [this](const SDL_Event& e){ if (m_levelSelectorState) m_levelSelectorState->handleEvent(e); }); + m_stateManager->registerOnEnter(AppState::LevelSelector, [this](){ if (m_levelSelectorState) m_levelSelectorState->onEnter(); }); + m_stateManager->registerOnExit(AppState::LevelSelector, [this](){ if (m_levelSelectorState) m_levelSelectorState->onExit(); }); + + m_stateManager->registerEventHandler(AppState::Playing, [this](const SDL_Event& e){ if (m_playingState) m_playingState->handleEvent(e); }); + m_stateManager->registerOnEnter(AppState::Playing, [this](){ if (m_playingState) m_playingState->onEnter(); }); + m_stateManager->registerOnExit(AppState::Playing, [this](){ if (m_playingState) m_playingState->onExit(); }); + } + } + + // Finally call setupStateHandlers for inline visuals and additional hooks + setupStateHandlers(); SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Game systems initialized with asset loading"); return true; } +void ApplicationManager::setupStateHandlers() { + // Helper function for drawing rectangles + auto drawRect = [](SDL_Renderer* renderer, float x, float y, float w, float h, uint8_t r, uint8_t g, uint8_t b, uint8_t a) { + SDL_SetRenderDrawColor(renderer, r, g, b, a); + SDL_FRect rect = { x, y, w, h }; + SDL_RenderFillRect(renderer, &rect); + }; + + // Helper function for drawing menu buttons with enhanced styling + auto drawEnhancedButton = [drawRect](SDL_Renderer* renderer, FontAtlas* font, float cx, float cy, float w, float h, + const std::string& label, bool isHovered, bool isSelected) { + float x = cx - w/2; + float y = cy - h/2; + + // Button styling based on state + SDL_Color bgColor = isSelected ? SDL_Color{100, 150, 255, 255} : + isHovered ? SDL_Color{80, 120, 200, 255} : + SDL_Color{60, 90, 160, 255}; + + // Draw border and background + drawRect(renderer, x-2, y-2, w+4, h+4, 60, 80, 140, 255); // Border + drawRect(renderer, x, y, w, h, bgColor.r, bgColor.g, bgColor.b, bgColor.a); // Background + + // Draw text if font is available + if (font) { + float textScale = 1.8f; + float approxCharW = 12.0f * textScale; + float textW = label.length() * approxCharW; + float textX = x + (w - textW) / 2.0f; + float textY = y + (h - 20.0f * textScale) / 2.0f; + + // Draw shadow + font->draw(renderer, textX + 2, textY + 2, label, textScale, {0, 0, 0, 180}); + // Draw main text + font->draw(renderer, textX, textY, label, textScale, {255, 255, 255, 255}); + } + }; + + // Loading State Handlers (matching original main.cpp implementation) + m_stateManager->registerRenderHandler(AppState::Loading, + [this, drawRect](RenderManager& renderer) { + // Clear background first + renderer.clear(0, 0, 0, 255); + + // Use 3D starfield for loading screen (full screen) + // Ensure starfield uses actual window size so center and projection are correct + if (m_starfield3D) { + int winW_actual = 0, winH_actual = 0; + if (m_renderManager) { + m_renderManager->getWindowSize(winW_actual, winH_actual); + } + if (winW_actual > 0 && winH_actual > 0) { + m_starfield3D->resize(winW_actual, winH_actual); + } + m_starfield3D->draw(renderer.getSDLRenderer()); + } + + // Set viewport and scaling for content + int winW = Config::Window::DEFAULT_WIDTH; + int winH = Config::Window::DEFAULT_HEIGHT; + int LOGICAL_W = Config::Logical::WIDTH; + int LOGICAL_H = Config::Logical::HEIGHT; + + // Calculate logical scaling and viewport + float scaleX = static_cast(winW) / LOGICAL_W; + float scaleY = static_cast(winH) / LOGICAL_H; + float logicalScale = std::min(scaleX, scaleY); + + int vpW = static_cast(LOGICAL_W * logicalScale); + int vpH = static_cast(LOGICAL_H * logicalScale); + int vpX = (winW - vpW) / 2; + int vpY = (winH - vpH) / 2; + + SDL_Rect logicalVP = { vpX, vpY, vpW, vpH }; + SDL_SetRenderViewport(renderer.getSDLRenderer(), &logicalVP); + SDL_SetRenderScale(renderer.getSDLRenderer(), logicalScale, logicalScale); + + // 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 drawRectOriginal = [&](float x, float y, float w, float h, SDL_Color c) { + SDL_SetRenderDrawColor(renderer.getSDLRenderer(), c.r, c.g, c.b, c.a); + SDL_FRect fr{x + contentOffsetX, y + contentOffsetY, w, h}; + SDL_RenderFillRect(renderer.getSDLRenderer(), &fr); + }; + + // Calculate dimensions for perfect centering (like JavaScript version) + const bool isLimitedHeight = LOGICAL_H < 450; + SDL_Texture* logoTex = m_assetManager->getTexture("logo"); + 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.getSDLRenderer(), logoTex, nullptr, &dst); + + currentY += displayHeight + spacingBetweenElements; + } + + // Draw "LOADING" text (centered, using pixel font with fallback to main_font) + FontAtlas* pixelFont = (FontAtlas*)m_assetManager->getFont("pixel_font"); + FontAtlas* fallbackFont = (FontAtlas*)m_assetManager->getFont("main_font"); + FontAtlas* loadingFont = pixelFont ? pixelFont : fallbackFont; + if (loadingFont) { + const char* loadingText = "LOADING"; + float textWidth = strlen(loadingText) * 12.0f; // Approximate width for pixel font + float textX = (LOGICAL_W - textWidth) / 2.0f; + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Rendering LOADING text at (%f,%f)", textX + contentOffsetX, currentY + contentOffsetY); + loadingFont->draw(renderer.getSDLRenderer(), textX + contentOffsetX, currentY + contentOffsetY, loadingText, 1.0f, {255, 204, 0, 255}); + } else { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "No loading font available to render LOADING text"); + } + + currentY += loadingTextHeight + barPaddingVertical; + + // Draw loading bar (like JavaScript version) + const int barW = 400, barH = 20; + const int bx = (LOGICAL_W - barW) / 2; + + float loadingProgress = m_assetManager->getLoadingProgress(); + + // Bar border (dark gray) - using drawRect which adds content offset + drawRectOriginal(bx - 3, currentY - 3, barW + 6, barH + 6, {68, 68, 80, 255}); + + // Bar background (darker gray) + drawRectOriginal(bx, currentY, barW, barH, {34, 34, 34, 255}); + + // Progress bar (gold color) + drawRectOriginal(bx, currentY, int(barW * loadingProgress), barH, {255, 204, 0, 255}); + + currentY += barH + spacingBetweenElements; + + // Draw percentage text (centered, using loadingFont) + if (loadingFont) { + 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; + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Rendering percent text '%s' at (%f,%f)", percentText, percentX + contentOffsetX, currentY + contentOffsetY); + loadingFont->draw(renderer.getSDLRenderer(), percentX + contentOffsetX, currentY + contentOffsetY, percentText, 1.5f, {255, 204, 0, 255}); + } + + // Reset viewport and scale + SDL_SetRenderViewport(renderer.getSDLRenderer(), nullptr); + SDL_SetRenderScale(renderer.getSDLRenderer(), 1.0f, 1.0f); + }); + + m_stateManager->registerUpdateHandler(AppState::Loading, + [this](float deltaTime) { + // Update 3D starfield so stars move during loading + if (m_starfield3D) { + m_starfield3D->update(deltaTime); + } + + // Check if loading is complete and transition to menu + if (m_assetManager->isLoadingComplete()) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loading complete, transitioning to Menu"); + bool ok = m_stateManager->setState(AppState::Menu); + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "setState(AppState::Menu) returned %d", ok ? 1 : 0); + traceFile("- to Menu returned"); + } + }); + + // Menu State render: draw background full-screen, then delegate to MenuState::render + m_stateManager->registerRenderHandler(AppState::Menu, + [this](RenderManager& renderer) { + // Clear and draw background to full window + renderer.clear(0, 0, 20, 255); + int winW = 0, winH = 0; + if (m_renderManager) m_renderManager->getWindowSize(winW, winH); + SDL_Texture* background = m_assetManager->getTexture("background"); + if (background && winW > 0 && winH > 0) { + SDL_FRect bgRect = { 0, 0, (float)winW, (float)winH }; + renderer.renderTexture(background, nullptr, &bgRect); + } + + // Compute logical scale and viewport + const int LOGICAL_W = Config::Logical::WIDTH; + const int LOGICAL_H = Config::Logical::HEIGHT; + float scaleX = winW > 0 ? (float)winW / LOGICAL_W : 1.0f; + float scaleY = winH > 0 ? (float)winH / LOGICAL_H : 1.0f; + float logicalScale = std::min(scaleX, scaleY); + int vpW = (int)(LOGICAL_W * logicalScale); + int vpH = (int)(LOGICAL_H * logicalScale); + int vpX = (winW - vpW) / 2; + int vpY = (winH - vpH) / 2; + SDL_Rect logicalVP{vpX, vpY, vpW, vpH}; + + // Apply viewport+scale then call MenuState::render (shows highscores, fireworks, bottom buttons) + SDL_SetRenderViewport(renderer.getSDLRenderer(), &logicalVP); + SDL_SetRenderScale(renderer.getSDLRenderer(), logicalScale, logicalScale); + if (m_menuState) { + m_menuState->render(renderer.getSDLRenderer(), logicalScale, logicalVP); + } + // Reset to defaults + SDL_SetRenderViewport(renderer.getSDLRenderer(), nullptr); + SDL_SetRenderScale(renderer.getSDLRenderer(), 1.0f, 1.0f); + }); + + // LevelSelector State render: draw background full-screen, then delegate to LevelSelectorState::render + m_stateManager->registerRenderHandler(AppState::LevelSelector, + [this](RenderManager& renderer) { + // Clear and draw background to full window + renderer.clear(0, 0, 20, 255); + int winW = 0, winH = 0; + if (m_renderManager) m_renderManager->getWindowSize(winW, winH); + SDL_Texture* background = m_assetManager->getTexture("background"); + if (background && winW > 0 && winH > 0) { + SDL_FRect bgRect = { 0, 0, (float)winW, (float)winH }; + renderer.renderTexture(background, nullptr, &bgRect); + } + + // Compute logical scale and viewport + const int LOGICAL_W = Config::Logical::WIDTH; + const int LOGICAL_H = Config::Logical::HEIGHT; + float scaleX = winW > 0 ? (float)winW / LOGICAL_W : 1.0f; + float scaleY = winH > 0 ? (float)winH / LOGICAL_H : 1.0f; + float logicalScale = std::min(scaleX, scaleY); + int vpW = (int)(LOGICAL_W * logicalScale); + int vpH = (int)(LOGICAL_H * logicalScale); + int vpX = (winW - vpW) / 2; + int vpY = (winH - vpH) / 2; + SDL_Rect logicalVP{vpX, vpY, vpW, vpH}; + + // Apply viewport+scale then call LevelSelectorState::render (shows level selection popup) + SDL_SetRenderViewport(renderer.getSDLRenderer(), &logicalVP); + SDL_SetRenderScale(renderer.getSDLRenderer(), logicalScale, logicalScale); + if (m_levelSelectorState) { + m_levelSelectorState->render(renderer.getSDLRenderer(), logicalScale, logicalVP); + } + // Reset to defaults + SDL_SetRenderViewport(renderer.getSDLRenderer(), nullptr); + SDL_SetRenderScale(renderer.getSDLRenderer(), 1.0f, 1.0f); + }); + + m_stateManager->registerUpdateHandler(AppState::Menu, + [this](float deltaTime) { + // Update logo animation counter + m_logoAnimCounter += deltaTime; + + // Update fireworks effect + GlobalState& globalState = GlobalState::instance(); + globalState.updateFireworks(deltaTime); + + // Start background music once tracks are available and not yet started + if (m_musicEnabled && !m_musicStarted) { + if (Audio::instance().getLoadedTrackCount() > 0) { + Audio::instance().shuffle(); + Audio::instance().start(); + m_musicStarted = true; + } + } + }); + + m_stateManager->registerEventHandler(AppState::Menu, + [this](const SDL_Event& event) { + // Forward keyboard events (Enter/Escape) to trigger actions, match original main.cpp + if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) { + if (event.key.scancode == SDL_SCANCODE_RETURN || event.key.scancode == SDL_SCANCODE_RETURN2 || event.key.scancode == SDL_SCANCODE_KP_ENTER) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Starting game from menu (Enter)"); + // Reset start level and transition + // In the original main, game.reset(...) was called; here we only switch state. + m_stateManager->setState(AppState::Playing); + return; + } + if (event.key.scancode == SDL_SCANCODE_ESCAPE) { + // If an exit confirmation is already showing, accept it and quit. + if (m_showExitConfirmPopup) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Quitting from menu (Escape confirmed)"); + m_running = false; + return; + } + + // Otherwise, show the exit confirmation popup instead of quitting immediately. + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Showing exit confirmation (Escape)"); + m_showExitConfirmPopup = true; + return; + } + // Global toggles + if (event.key.scancode == SDL_SCANCODE_M) { + Audio::instance().toggleMute(); + m_musicEnabled = !m_musicEnabled; + } + if (event.key.scancode == SDL_SCANCODE_S) { + SoundEffectManager::instance().setEnabled(!SoundEffectManager::instance().isEnabled()); + } + if (event.key.scancode == SDL_SCANCODE_N) { + SoundEffectManager::instance().playSound("lets_go", 1.0f); + } + } + + // Mouse handling: map SDL mouse coords into logical content coords and + // perform hit-tests for menu buttons similar to main.cpp. + if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) { + float mx = (float)event.button.x; + float my = (float)event.button.y; + int winW = 0, winH = 0; + if (m_renderManager) m_renderManager->getWindowSize(winW, winH); + float logicalScale = std::min(winW / (float)Config::Logical::WIDTH, winH / (float)Config::Logical::HEIGHT); + if (logicalScale <= 0) logicalScale = 1.0f; + SDL_Rect logicalVP{0,0,winW,winH}; + // Check bounds and compute content-local coords + if (mx >= logicalVP.x && my >= logicalVP.y && mx <= logicalVP.x + logicalVP.w && my <= logicalVP.y + logicalVP.h) { + float lx = (mx - logicalVP.x) / logicalScale; + float ly = (my - logicalVP.y) / logicalScale; + + // Respect settings popup + if (m_showSettingsPopup) { + m_showSettingsPopup = false; + } else { + bool isSmall = ((Config::Logical::WIDTH * logicalScale) < 700.0f); + float btnW = isSmall ? (Config::Logical::WIDTH * 0.4f) : 300.0f; + float btnH = isSmall ? 60.0f : 70.0f; + float btnCX = Config::Logical::WIDTH * 0.5f; + const float btnYOffset = 40.0f; + float btnCY = Config::Logical::HEIGHT * 0.86f + 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) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Menu: Play button clicked"); + m_stateManager->setState(AppState::Playing); + } else if (lx >= levelBtn.x && lx <= levelBtn.x + levelBtn.w && ly >= levelBtn.y && ly <= levelBtn.y + levelBtn.h) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Menu: Level button clicked"); + m_stateManager->setState(AppState::LevelSelector); + } else { + // Settings area detection (top-right small area) + SDL_FRect settingsBtn{Config::Logical::WIDTH - 60, 10, 50, 30}; + if (lx >= settingsBtn.x && lx <= settingsBtn.x + settingsBtn.w && ly >= settingsBtn.y && ly <= settingsBtn.y + settingsBtn.h) { + m_showSettingsPopup = true; + } + } + } + } + } + + // Mouse motion handling for hover + if (event.type == SDL_EVENT_MOUSE_MOTION) { + float mx = (float)event.motion.x; + float my = (float)event.motion.y; + int winW = 0, winH = 0; + if (m_renderManager) m_renderManager->getWindowSize(winW, winH); + float logicalScale = std::min(winW / (float)Config::Logical::WIDTH, winH / (float)Config::Logical::HEIGHT); + if (logicalScale <= 0) logicalScale = 1.0f; + SDL_Rect logicalVP{0,0,winW,winH}; + if (mx >= logicalVP.x && my >= logicalVP.y && mx <= logicalVP.x + logicalVP.w && my <= logicalVP.y + logicalVP.h) { + float lx = (mx - logicalVP.x) / logicalScale; + float ly = (my - logicalVP.y) / logicalScale; + if (!m_showSettingsPopup) { + bool isSmall = ((Config::Logical::WIDTH * logicalScale) < 700.0f); + float btnW = isSmall ? (Config::Logical::WIDTH * 0.4f) : 300.0f; + float btnH = isSmall ? 60.0f : 70.0f; + float btnCX = Config::Logical::WIDTH * 0.5f; + const float btnYOffset = 40.0f; + float btnCY = Config::Logical::HEIGHT * 0.86f + 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}; + m_hoveredButton = -1; + if (lx >= playBtn.x && lx <= playBtn.x + playBtn.w && ly >= playBtn.y && ly <= playBtn.y + playBtn.h) { + m_hoveredButton = 0; + } else if (lx >= levelBtn.x && lx <= levelBtn.x + levelBtn.w && ly >= levelBtn.y && ly <= levelBtn.y + levelBtn.h) { + m_hoveredButton = 1; + } + } + } + } + }); + + // Playing State - Placeholder for now + m_stateManager->registerRenderHandler(AppState::Playing, + [this](RenderManager& renderer) { + renderer.clear(0, 0, 0, 255); + + // For now, just show a placeholder + FontAtlas* font = (FontAtlas*)m_assetManager->getFont("main_font"); + if (font) { + float centerX = Config::Window::DEFAULT_WIDTH / 2.0f; + float centerY = Config::Window::DEFAULT_HEIGHT / 2.0f; + std::string playingText = "TETRIS GAME PLAYING STATE"; + float textX = centerX - (playingText.length() * 12.0f) / 2.0f; + font->draw(renderer.getSDLRenderer(), textX, centerY, playingText, 2.0f, {255, 255, 255, 255}); + + std::string instruction = "Press ESC to return to menu"; + float instrX = centerX - (instruction.length() * 8.0f) / 2.0f; + font->draw(renderer.getSDLRenderer(), instrX, centerY + 60, instruction, 1.0f, {200, 200, 200, 255}); + } + }); + + m_stateManager->registerEventHandler(AppState::Playing, + [this](const SDL_Event& event) { + if (event.type == SDL_EVENT_KEY_DOWN) { + if (event.key.scancode == SDL_SCANCODE_ESCAPE) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Returning to menu from playing state"); + m_stateManager->setState(AppState::Menu); + } + } + }); +} + void ApplicationManager::processEvents() { // Let InputManager process all SDL events if (m_inputManager) { @@ -224,6 +839,7 @@ void ApplicationManager::processEvents() { // Check if InputManager detected a quit request if (m_inputManager->shouldQuit()) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "InputManager reports shouldQuit() == true — requesting shutdown"); requestShutdown(); return; } @@ -234,14 +850,26 @@ void ApplicationManager::processEvents() { } void ApplicationManager::update(float deltaTime) { + // Update AssetManager for progressive loading + if (m_assetManager) { + m_assetManager->update(deltaTime); + } + // Update InputManager if (m_inputManager) { m_inputManager->update(deltaTime); } + // Always update 3D starfield so background animates even during loading/menu + if (m_starfield3D) { + m_starfield3D->update(deltaTime); + } + // Update StateManager if (m_stateManager) { m_stateManager->update(deltaTime); + SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "ApplicationManager::update - state update completed for state %s", m_stateManager->getStateName(m_stateManager->getState())); + traceFile("update completed"); } } @@ -249,15 +877,21 @@ void ApplicationManager::render() { if (!m_renderManager) { return; } - + SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "ApplicationManager::render - about to begin frame"); + // Trace render begin + traceFile("render begin"); m_renderManager->beginFrame(); + SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "ApplicationManager::render - beginFrame complete"); // Delegate rendering to StateManager if (m_stateManager) { m_stateManager->render(*m_renderManager); + SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "ApplicationManager::render - state render completed for state %s", m_stateManager->getStateName(m_stateManager->getState())); } m_renderManager->endFrame(); + SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "ApplicationManager::render - endFrame complete"); + traceFile("render endFrame complete"); } void ApplicationManager::cleanupManagers() { diff --git a/src/core/ApplicationManager.h b/src/core/ApplicationManager.h index 8f93ba2..79697e2 100644 --- a/src/core/ApplicationManager.h +++ b/src/core/ApplicationManager.h @@ -1,6 +1,7 @@ #pragma once #include "Config.h" +#include "../states/State.h" #include #include @@ -16,6 +17,12 @@ class Starfield3D; class FontAtlas; class LineEffect; +// Forward declare state classes (top-level, defined under src/states) +class LoadingState; +class MenuState; +class LevelSelectorState; +class PlayingState; + /** * ApplicationManager - Central coordinator for the entire application lifecycle * @@ -50,6 +57,7 @@ private: bool initializeSDL(); bool initializeManagers(); bool initializeGame(); + void setupStateHandlers(); // Main loop methods void processEvents(); @@ -66,6 +74,37 @@ private: std::unique_ptr m_assetManager; std::unique_ptr m_stateManager; + // Visual effects + std::unique_ptr m_starfield3D; + std::unique_ptr m_starfield; + + // Menu / UI state pieces mirrored from main.cpp + bool m_musicEnabled = true; + int m_startLevelSelection = 0; + int m_hoveredButton = -1; + bool m_showSettingsPopup = false; + bool m_showExitConfirmPopup = false; + uint64_t m_loadStartTicks = 0; + bool m_musicStarted = false; + bool m_musicLoaded = false; + int m_currentTrackLoading = 0; + int m_totalTracks = 0; + + // Persistence (ScoreManager declared at top-level) + std::unique_ptr m_scoreManager; + // Gameplay pieces + std::unique_ptr m_game; + std::unique_ptr m_lineEffect; + + + // State context (must be a member to ensure lifetime) + StateContext m_stateContext; + + // State objects (mirror main.cpp pattern) + std::unique_ptr m_loadingState; + std::unique_ptr m_menuState; + std::unique_ptr m_levelSelectorState; + std::unique_ptr m_playingState; // Application state bool m_running = false; bool m_initialized = false; @@ -77,4 +116,7 @@ private: int m_windowWidth = Config::Window::DEFAULT_WIDTH; int m_windowHeight = Config::Window::DEFAULT_HEIGHT; std::string m_windowTitle = Config::Window::DEFAULT_TITLE; + + // Animation state + float m_logoAnimCounter = 0.0f; }; diff --git a/src/core/AssetManager.cpp b/src/core/AssetManager.cpp index b715ada..5dcb41e 100644 --- a/src/core/AssetManager.cpp +++ b/src/core/AssetManager.cpp @@ -10,6 +10,9 @@ AssetManager::AssetManager() : m_renderer(nullptr) , m_audioSystem(nullptr) , m_soundSystem(nullptr) + , m_totalLoadingTasks(0) + , m_completedLoadingTasks(0) + , m_loadingComplete(false) , m_defaultTexturePath("assets/images/") , m_defaultFontPath("assets/fonts/") , m_initialized(false) { @@ -260,49 +263,87 @@ void AssetManager::addLoadingTask(const LoadingTask& task) { void AssetManager::executeLoadingTasks(std::function progressCallback) { if (m_loadingTasks.empty()) { + m_loadingComplete = true; if (progressCallback) progressCallback(1.0f); return; } - logInfo("Executing " + std::to_string(m_loadingTasks.size()) + " loading tasks..."); + logInfo("Starting progressive loading of " + std::to_string(m_loadingTasks.size()) + " loading tasks..."); - size_t totalTasks = m_loadingTasks.size(); - size_t completedTasks = 0; + m_totalLoadingTasks = m_loadingTasks.size(); + m_completedLoadingTasks = 0; + m_currentTaskIndex = 0; + m_loadingComplete = false; + m_isProgressiveLoading = true; + m_lastLoadTime = SDL_GetTicks(); + m_musicLoadingStarted = false; + m_musicLoadingProgress = 0.0f; + + // Don't execute tasks immediately - let update() handle them progressively +} - for (const auto& task : m_loadingTasks) { +void AssetManager::update(float deltaTime) { + if (!m_isProgressiveLoading || m_loadingTasks.empty()) { + // Handle music loading progress simulation if assets are done + if (m_musicLoadingStarted && !m_loadingComplete) { + m_musicLoadingProgress += deltaTime * 0.4f; // Simulate music loading progress + if (m_musicLoadingProgress >= 1.0f) { + m_musicLoadingProgress = 1.0f; + m_loadingComplete = true; + logInfo("Background music loading simulation complete"); + } + } + return; + } + + Uint64 currentTime = SDL_GetTicks(); + + // Add minimum delay between loading items (600ms per item for visual effect) + if (currentTime - m_lastLoadTime < 600) { + return; + } + + // Load one item at a time + if (m_currentTaskIndex < m_loadingTasks.size()) { + const auto& task = m_loadingTasks[m_currentTaskIndex]; + bool success = false; - switch (task.type) { case LoadingTask::TEXTURE: success = (loadTexture(task.id, task.filepath) != nullptr); break; - case LoadingTask::FONT: success = loadFont(task.id, task.filepath, task.fontSize); break; - case LoadingTask::MUSIC: success = loadMusicTrack(task.filepath); break; - case LoadingTask::SOUND_EFFECT: success = loadSoundEffect(task.id, task.filepath); break; } - + if (!success) { logError("Failed to load asset: " + task.id + " (" + task.filepath + ")"); } - - completedTasks++; - if (progressCallback) { - float progress = static_cast(completedTasks) / static_cast(totalTasks); - progressCallback(progress); + m_currentTaskIndex++; + m_completedLoadingTasks = m_currentTaskIndex; + m_lastLoadTime = currentTime; + + logInfo("Asset loading progress: " + std::to_string((float)m_completedLoadingTasks / m_totalLoadingTasks * 100.0f) + "%"); + + // Check if all asset tasks are complete + if (m_currentTaskIndex >= m_loadingTasks.size()) { + m_isProgressiveLoading = false; + logInfo("Completed " + std::to_string(m_completedLoadingTasks) + "/" + std::to_string(m_totalLoadingTasks) + " loading tasks"); + + // Start background music loading simulation + m_musicLoadingStarted = true; + m_musicLoadingProgress = 0.0f; + startBackgroundMusicLoading(); } } - - logInfo("Completed " + std::to_string(completedTasks) + "/" + std::to_string(totalTasks) + " loading tasks"); } void AssetManager::clearLoadingTasks() { @@ -382,3 +423,23 @@ void AssetManager::logInfo(const std::string& message) const { void AssetManager::logError(const std::string& message) const { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[AssetManager] %s", message.c_str()); } + +// Loading progress tracking methods +bool AssetManager::isLoadingComplete() const { + // Loading is complete when both asset tasks and music loading are done + return m_loadingComplete && (!m_musicLoadingStarted || m_musicLoadingProgress >= 1.0f); +} + +float AssetManager::getLoadingProgress() const { + if (m_totalLoadingTasks == 0) { + return 1.0f; // No tasks = complete + } + + // Asset loading progress (80% of total progress) + float assetProgress = static_cast(m_completedLoadingTasks) / static_cast(m_totalLoadingTasks) * 0.8f; + + // Music loading progress (20% of total progress) + float musicProgress = m_musicLoadingStarted ? m_musicLoadingProgress * 0.2f : 0.0f; + + return assetProgress + musicProgress; +} diff --git a/src/core/AssetManager.h b/src/core/AssetManager.h index db329d3..8d5c12b 100644 --- a/src/core/AssetManager.h +++ b/src/core/AssetManager.h @@ -69,6 +69,13 @@ public: void addLoadingTask(const LoadingTask& task); void executeLoadingTasks(std::function progressCallback = nullptr); void clearLoadingTasks(); + void update(float deltaTime); // New: Progressive loading update + + // Loading progress tracking + bool isLoadingComplete() const; + float getLoadingProgress() const; + size_t getTotalLoadingTasks() const { return m_totalLoadingTasks; } + size_t getCompletedLoadingTasks() const { return m_completedLoadingTasks; } // Resource queries size_t getTextureCount() const { return m_textures.size(); } @@ -88,6 +95,18 @@ private: std::unordered_map m_textures; std::unordered_map> m_fonts; std::vector m_loadingTasks; + + // Loading progress tracking + size_t m_totalLoadingTasks = 0; + size_t m_completedLoadingTasks = 0; + bool m_loadingComplete = false; + + // Progressive loading state + bool m_isProgressiveLoading = false; + size_t m_currentTaskIndex = 0; + Uint64 m_lastLoadTime = 0; + bool m_musicLoadingStarted = false; + float m_musicLoadingProgress = 0.0f; // System references SDL_Renderer* m_renderer; diff --git a/src/core/InputManager.cpp b/src/core/InputManager.cpp index 73a5156..c8642a9 100644 --- a/src/core/InputManager.cpp +++ b/src/core/InputManager.cpp @@ -16,11 +16,17 @@ void InputManager::processEvents() { SDL_Event event; while (SDL_PollEvent(&event)) { + // Trace every polled event type for debugging abrupt termination + { + FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "InputManager: polled event type=%d\n", (int)event.type); fclose(f); } + } switch (event.type) { case SDL_EVENT_QUIT: m_shouldQuit = true; - for (auto& handler : m_quitHandlers) { + for (auto& handler : m_quitHandlers) { try { + // Trace quit event handling + FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "InputManager: SDL_EVENT_QUIT polled\n"); fclose(f); } handler(); } catch (const std::exception& e) { SDL_LogError(SDL_LOG_CATEGORY_INPUT, "Exception in quit handler: %s", e.what()); diff --git a/src/core/StateManager.cpp b/src/core/StateManager.cpp index 196350f..f005a6d 100644 --- a/src/core/StateManager.cpp +++ b/src/core/StateManager.cpp @@ -84,6 +84,10 @@ bool StateManager::setState(AppState newState) { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "State transition: %s -> %s", getStateName(m_currentState), getStateName(newState)); + // Persistent trace for debugging abrupt exits + { + FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "setState start %s -> %s\n", getStateName(m_currentState), getStateName(newState)); fclose(f); } + } // Execute exit hooks for current state executeExitHooks(m_currentState); @@ -95,6 +99,11 @@ bool StateManager::setState(AppState newState) { // Execute enter hooks for new state executeEnterHooks(m_currentState); + // Trace completion + { + FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "setState end %s\n", getStateName(m_currentState)); fclose(f); } + } + return true; } @@ -175,13 +184,20 @@ void StateManager::executeEnterHooks(AppState state) { return; } + int idx = 0; for (auto& hook : it->second) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Executing enter hook %d for state %s", idx, getStateName(state)); + // Also write to trace file for persistent record + { + FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "executeEnterHook %d %s\n", idx, getStateName(state)); fclose(f); } + } try { hook(); } catch (const std::exception& e) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Exception in enter hook for state %s: %s", getStateName(state), e.what()); } + ++idx; } } @@ -191,12 +207,18 @@ void StateManager::executeExitHooks(AppState state) { return; } + int idx = 0; for (auto& hook : it->second) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Executing exit hook %d for state %s", idx, getStateName(state)); + { + FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "executeExitHook %d %s\n", idx, getStateName(state)); fclose(f); } + } try { hook(); } catch (const std::exception& e) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Exception in exit hook for state %s: %s", getStateName(state), e.what()); } + ++idx; } } diff --git a/src/graphics/RenderManager.cpp b/src/graphics/RenderManager.cpp index 6c85d30..61b3004 100644 --- a/src/graphics/RenderManager.cpp +++ b/src/graphics/RenderManager.cpp @@ -82,16 +82,35 @@ void RenderManager::beginFrame() { return; } - // Clear the screen + // Trace beginFrame entry + { + FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "RenderManager::beginFrame entry\n"); fclose(f); } + } + + // Clear the screen (wrapped with trace) clear(12, 12, 16, 255); // Dark background similar to original + + // Trace after clear + { + FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "RenderManager::beginFrame after clear\n"); fclose(f); } + } } void RenderManager::endFrame() { if (!m_initialized || !m_renderer) { return; } + // Trace before present + { + FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "RenderManager::endFrame before present\n"); fclose(f); } + } SDL_RenderPresent(m_renderer); + + // Trace after present + { + FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "RenderManager::endFrame after present\n"); fclose(f); } + } } void RenderManager::setLogicalSize(int width, int height) { @@ -146,7 +165,14 @@ void RenderManager::renderTexture(SDL_Texture* texture, const SDL_FRect* src, co return; } + // Trace renderTexture usage + { + FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "RenderManager::renderTexture entry tex=%llu src=%p dst=%p\n", (unsigned long long)(uintptr_t)texture, (void*)src, (void*)dst); fclose(f); } + } SDL_RenderTexture(m_renderer, texture, src, dst); + { + FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "RenderManager::renderTexture after SDL_RenderTexture tex=%llu\n", (unsigned long long)(uintptr_t)texture); fclose(f); } + } } void RenderManager::renderTexture(SDL_Texture* texture, float x, float y) { @@ -230,6 +256,15 @@ void RenderManager::getWindowSize(int& width, int& height) const { height = m_windowHeight; } +void RenderManager::getTextureSize(SDL_Texture* tex, int& w, int& h) const { + if (!tex) { w = 0; h = 0; return; } + // SDL3 provides SDL_GetTextureSize which accepts float or int pointers depending on overloads + float fw = 0.0f, fh = 0.0f; + SDL_GetTextureSize(tex, &fw, &fh); + w = int(fw + 0.5f); + h = int(fh + 0.5f); +} + void RenderManager::updateLogicalScale() { if (!m_initialized || !m_renderer) { return; diff --git a/src/graphics/RenderManager.h b/src/graphics/RenderManager.h index f467638..6229211 100644 --- a/src/graphics/RenderManager.h +++ b/src/graphics/RenderManager.h @@ -52,6 +52,8 @@ public: // Direct access to SDL objects (temporary, will be removed later) SDL_Renderer* getSDLRenderer() const { return m_renderer; } SDL_Window* getSDLWindow() const { return m_window; } + // Texture queries + void getTextureSize(SDL_Texture* tex, int& w, int& h) const; // State queries bool isInitialized() const { return m_initialized; } diff --git a/src/states/MenuState.cpp b/src/states/MenuState.cpp index 77fe5d4..c84ca45 100644 --- a/src/states/MenuState.cpp +++ b/src/states/MenuState.cpp @@ -2,6 +2,7 @@ #include "MenuState.h" #include "persistence/Scores.h" #include "graphics/Font.h" +#include "../core/GlobalState.h" #include #include #include @@ -20,7 +21,7 @@ static constexpr int LOGICAL_H = 1000; MenuState::MenuState(StateContext& ctx) : State(ctx) {} void MenuState::onEnter() { - // nothing for now + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "MenuState::onEnter called"); } void MenuState::onExit() { @@ -37,6 +38,10 @@ void MenuState::update(double frameMs) { } void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) { + // Trace entry to persistent log for debugging abrupt exit/crash during render + { + FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render entry\n"); fclose(f); } + } // Compute content offset using the same math as main float winW = float(logicalVP.w); float winH = float(logicalVP.h); @@ -50,6 +55,10 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi // Draw the animated logo and fireworks using the small logo if available (show whole image) SDL_Texture* logoToUse = ctx.logoSmallTex ? ctx.logoSmallTex : ctx.logoTex; + // Trace logo texture pointer + { + FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render logoToUse=%llu\n", (unsigned long long)(uintptr_t)logoToUse); fclose(f); } + } if (logoToUse) { // Use dimensions provided by the shared context when available int texW = (logoToUse == ctx.logoSmallTex && ctx.logoSmallW > 0) ? ctx.logoSmallW : 872; @@ -61,17 +70,37 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi float logoX = (LOGICAL_W - dw) / 2.f + contentOffsetX; float logoY = LOGICAL_H * 0.05f + contentOffsetY; SDL_FRect dst{logoX, logoY, dw, dh}; + // Trace before rendering logo + { + FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render before SDL_RenderTexture logo ptr=%llu\n", (unsigned long long)(uintptr_t)logoToUse); fclose(f); } + } SDL_RenderTexture(renderer, logoToUse, nullptr, &dst); + // Trace after rendering logo + { + FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render after SDL_RenderTexture logo\n"); fclose(f); } + } } // Fireworks (draw above high scores / near buttons) - menu_drawFireworks(renderer, ctx.blocksTex); + { + FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render before drawFireworks blocksTex=%llu\n", (unsigned long long)(uintptr_t)ctx.blocksTex); fclose(f); } + } + GlobalState::instance().drawFireworks(renderer, ctx.blocksTex); + { + FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render after drawFireworks\n"); fclose(f); } + } // Score list and top players with a sine-wave vertical animation (use pixelFont for retro look) float topPlayersY = LOGICAL_H * 0.30f + contentOffsetY; // more top padding FontAtlas* useFont = ctx.pixelFont ? ctx.pixelFont : ctx.font; if (useFont) { + { + FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render before useFont->draw TOP PLAYERS ptr=%llu\n", (unsigned long long)(uintptr_t)useFont); fclose(f); } + } useFont->draw(renderer, LOGICAL_W * 0.5f - 110 + contentOffsetX, topPlayersY, std::string("TOP PLAYERS"), 1.8f, SDL_Color{255, 220, 0, 255}); + { + FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render after useFont->draw TOP PLAYERS\n"); fclose(f); } + } } // High scores table with wave offset @@ -93,7 +122,7 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi for (size_t i = 0; i < maxDisplay; ++i) { float baseY = scoresStartY + i * 25; - float wave = std::sin((float)menu_getLogoAnimCounter() * 0.006f + i * 0.25f) * 6.0f; // subtle wave + float wave = std::sin((float)GlobalState::instance().logoAnimCounter * 0.006f + i * 0.25f) * 6.0f; // subtle wave float y = baseY + wave; // Center columns around mid X, wider @@ -130,16 +159,55 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi float btnY = LOGICAL_H * 0.86f + contentOffsetY + btnYOffset; // align with main's button vertical position if (ctx.pixelFont) { + { + FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render drawing buttons; pixelFont=%llu\n", (unsigned long long)(uintptr_t)ctx.pixelFont); fclose(f); } + } char levelBtnText[32]; int startLevel = ctx.startLevelSelection ? *ctx.startLevelSelection : 0; std::snprintf(levelBtnText, sizeof(levelBtnText), "LEVEL %d", startLevel); - // left = green, right = blue like original - menu_drawMenuButton(renderer, *ctx.pixelFont, btnX - btnW * 0.6f, btnY, btnW, btnH, std::string("PLAY"), SDL_Color{60,180,80,255}, SDL_Color{30,120,40,255}); - menu_drawMenuButton(renderer, *ctx.pixelFont, btnX + btnW * 0.6f, btnY, btnW, btnH, std::string(levelBtnText), SDL_Color{40,140,240,255}, SDL_Color{20,100,200,255}); + // Draw simple styled buttons (replicating menu_drawMenuButton) + auto drawMenuButtonLocal = [&](SDL_Renderer* r, FontAtlas& font, float cx, float cy, float w, float h, const std::string& label, SDL_Color bg, SDL_Color border){ + float x = cx - w/2; float y = cy - h/2; + SDL_SetRenderDrawColor(r, border.r, border.g, border.b, border.a); + SDL_FRect br{ x-6, y-6, w+12, h+12 }; SDL_RenderFillRect(r, &br); + SDL_SetRenderDrawColor(r, 255,255,255,255); SDL_FRect br2{ x-4, y-4, w+8, h+8 }; SDL_RenderFillRect(r, &br2); + SDL_SetRenderDrawColor(r, bg.r, bg.g, bg.b, bg.a); SDL_FRect br3{ x, y, w, h }; SDL_RenderFillRect(r, &br3); + 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(r, tx+2, ty+2, label, textScale, SDL_Color{0,0,0,180}); + font.draw(r, tx, ty, label, textScale, SDL_Color{255,255,255,255}); + }; + drawMenuButtonLocal(renderer, *ctx.pixelFont, btnX - btnW * 0.6f, btnY, btnW, btnH, std::string("PLAY"), SDL_Color{60,180,80,255}, SDL_Color{30,120,40,255}); + { + FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render after draw PLAY button\n"); fclose(f); } + } + drawMenuButtonLocal(renderer, *ctx.pixelFont, btnX + btnW * 0.6f, btnY, btnW, btnH, std::string(levelBtnText), SDL_Color{40,140,240,255}, SDL_Color{20,100,200,255}); + { + FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render after draw LEVEL button\n"); fclose(f); } + } } // Popups (settings only - level popup is now a separate state) if (ctx.showSettingsPopup && *ctx.showSettingsPopup) { - menu_drawSettingsPopup(renderer, *ctx.font, ctx.musicEnabled ? *ctx.musicEnabled : false); + // draw settings popup inline + bool musicOn = ctx.musicEnabled ? *ctx.musicEnabled : true; + float popupW = 350, popupH = 260; + float popupX = (LOGICAL_W - popupW) / 2; + float popupY = (LOGICAL_H - popupH) / 2; + SDL_SetRenderDrawColor(renderer, 0,0,0,128); SDL_FRect overlay{0,0,(float)LOGICAL_W,(float)LOGICAL_H}; SDL_RenderFillRect(renderer, &overlay); + SDL_SetRenderDrawColor(renderer, 100,120,160,255); SDL_FRect bord{popupX-4,popupY-4,popupW+8,popupH+8}; SDL_RenderFillRect(renderer, &bord); + SDL_SetRenderDrawColor(renderer, 40,50,70,255); SDL_FRect body{popupX,popupY,popupW,popupH}; SDL_RenderFillRect(renderer, &body); + ctx.font->draw(renderer, popupX + 20, popupY + 20, "SETTINGS", 2.0f, SDL_Color{255,220,0,255}); + ctx.font->draw(renderer, popupX + 20, popupY + 70, "MUSIC:", 1.5f, SDL_Color{255,255,255,255}); + ctx.font->draw(renderer, popupX + 120, popupY + 70, musicOn ? "ON" : "OFF", 1.5f, musicOn ? SDL_Color{0,255,0,255} : SDL_Color{255,0,0,255}); + ctx.font->draw(renderer, popupX + 20, popupY + 100, "SOUND FX:", 1.5f, SDL_Color{255,255,255,255}); + ctx.font->draw(renderer, popupX + 140, popupY + 100, "ON", 1.5f, SDL_Color{0,255,0,255}); + ctx.font->draw(renderer, popupX + 20, popupY + 150, "M = TOGGLE MUSIC", 1.0f, SDL_Color{200,200,220,255}); + ctx.font->draw(renderer, popupX + 20, popupY + 170, "S = TOGGLE SOUND FX", 1.0f, SDL_Color{200,200,220,255}); + ctx.font->draw(renderer, popupX + 20, popupY + 190, "N = PLAY LETS_GO", 1.0f, SDL_Color{200,200,220,255}); + ctx.font->draw(renderer, popupX + 20, popupY + 210, "ESC = CLOSE", 1.0f, SDL_Color{200,200,220,255}); + } + // Trace exit + { + FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render exit\n"); fclose(f); } } } diff --git a/src/ui/MenuWrappers.cpp b/src/ui/MenuWrappers.cpp new file mode 100644 index 0000000..62a54d7 --- /dev/null +++ b/src/ui/MenuWrappers.cpp @@ -0,0 +1,88 @@ +// MenuWrappers.cpp - implementations of menu helper wrappers used by MenuState +#include "MenuWrappers.h" +#include "../core/GlobalState.h" +#include "../graphics/Font.h" +#include + +using namespace Globals; + +static void drawRect(SDL_Renderer* renderer, 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, y, w, h }; + SDL_RenderFillRect(renderer, &fr); +} + +void menu_drawFireworks(SDL_Renderer* renderer, SDL_Texture* blocksTex) { + GlobalState::instance().drawFireworks(renderer, blocksTex); +} + +void menu_updateFireworks(double frameMs) { + GlobalState::instance().updateFireworks(frameMs); +} + +double menu_getLogoAnimCounter() { + return GlobalState::instance().logoAnimCounter; +} + +int menu_getHoveredButton() { + return GlobalState::instance().hoveredButton; +} + +void menu_drawEnhancedButton(SDL_Renderer* renderer, FontAtlas& font, float cx, float cy, float w, float h, + const std::string& label, bool isHovered, bool isSelected) { + // Simple wrapper delegating to a basic draw implementation + SDL_Color bgColor = isSelected ? SDL_Color{100,190,255,255} : (isHovered ? SDL_Color{120,150,240,255} : SDL_Color{80,110,200,255}); + float x = cx - w/2; float y = cy - h/2; + drawRect(renderer, x-2, y-2, w+4, h+4, SDL_Color{60,80,140,255}); + drawRect(renderer, x, y, w, h, bgColor); + float textScale = 1.5f; + float textX = x + (w - label.length() * 12 * textScale) / 2; + float textY = y + (h - 20 * textScale) / 2; + font.draw(renderer, textX+2, textY+2, label, textScale, SDL_Color{0,0,0,180}); + font.draw(renderer, textX, textY, label, textScale, SDL_Color{255,255,255,255}); +} + +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, SDL_Color{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, SDL_Color{0,0,0,180}); + font.draw(renderer, tx, ty, label, textScale, SDL_Color{255,255,255,255}); +} + +void menu_drawLevelSelectionPopup(SDL_Renderer* renderer, FontAtlas& font, SDL_Texture* bgTex, int selectedLevel) { + (void)renderer; (void)font; (void)bgTex; (void)selectedLevel; // Not implemented here +} + +void menu_drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musicEnabled) { + // Copy of main.cpp's drawSettingsPopup behavior adapted here + float popupW = 350, popupH = 260; + float popupX = (1200 - popupW) / 2; + float popupY = (1000 - popupH) / 2; + // Overlay + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 128); + SDL_FRect overlay{0,0,1200,1000}; + SDL_RenderFillRect(renderer, &overlay); + drawRect(renderer, popupX-4, popupY-4, popupW+8, popupH+8, SDL_Color{100,120,160,255}); + drawRect(renderer, popupX, popupY, popupW, popupH, SDL_Color{40,50,70,255}); + font.draw(renderer, popupX + 20, popupY + 20, "SETTINGS", 2.0f, SDL_Color{255,220,0,255}); + font.draw(renderer, popupX + 20, popupY + 70, "MUSIC:", 1.5f, SDL_Color{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); + font.draw(renderer, popupX + 20, popupY + 100, "SOUND FX:", 1.5f, SDL_Color{255,255,255,255}); + // Sound effect manager may be initialized elsewhere; show placeholder status for now + bool sfxOn = true; + font.draw(renderer, popupX + 140, popupY + 100, sfxOn ? "ON" : "OFF", 1.5f, sfxOn ? SDL_Color{0,255,0,255} : SDL_Color{255,0,0,255}); + font.draw(renderer, popupX + 20, popupY + 150, "M = TOGGLE MUSIC", 1.0f, SDL_Color{200,200,220,255}); + font.draw(renderer, popupX + 20, popupY + 170, "S = TOGGLE SOUND FX", 1.0f, SDL_Color{200,200,220,255}); + font.draw(renderer, popupX + 20, popupY + 190, "N = PLAY LETS_GO", 1.0f, SDL_Color{200,200,220,255}); + font.draw(renderer, popupX + 20, popupY + 210, "ESC = CLOSE", 1.0f, SDL_Color{200,200,220,255}); +}