diff --git a/CMakeLists.txt b/CMakeLists.txt index 93ff572..5dcd72c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,6 +41,7 @@ add_executable(tetris src/graphics/Starfield.cpp src/graphics/Starfield3D.cpp src/graphics/Font.cpp + src/graphics/GameRenderer.cpp src/audio/Audio.cpp src/gameplay/LineEffect.cpp src/audio/SoundEffect.cpp @@ -134,6 +135,7 @@ add_executable(tetris_refactored src/graphics/Starfield.cpp src/graphics/Starfield3D.cpp src/graphics/Font.cpp + src/graphics/GameRenderer.cpp src/audio/Audio.cpp src/gameplay/LineEffect.cpp src/audio/SoundEffect.cpp diff --git a/check_events.cpp b/check_events.cpp new file mode 100644 index 0000000..bfeb252 --- /dev/null +++ b/check_events.cpp @@ -0,0 +1,5 @@ +#include +#include +int main() { std::cout << \ +SDL_EVENT_QUIT: +\ << SDL_EVENT_QUIT << std::endl; return 0; } diff --git a/src/audio/Audio.cpp b/src/audio/Audio.cpp index 38ecf48..dfc5f8e 100644 --- a/src/audio/Audio.cpp +++ b/src/audio/Audio.cpp @@ -63,14 +63,33 @@ bool Audio::ensureStream(){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] SDL_OpenAudioDeviceStream failed: %s", SDL_GetError()); return false; } + // Ensure the device is running so SFX can be heard even before music starts + SDL_ResumeAudioStreamDevice(audioStream); return true; } -void Audio::start(){ if(!ensureStream()) return; if(!playing){ current=-1; nextTrack(); SDL_ResumeAudioStreamDevice(audioStream); playing=true; } } +void Audio::start(){ + if(!ensureStream()) return; + // If no track is selected yet, try to select one now (in case tracks loaded after initial start) + if(current < 0) { + nextTrack(); + } + SDL_ResumeAudioStreamDevice(audioStream); + playing = true; +} void Audio::toggleMute(){ muted=!muted; } -void Audio::nextTrack(){ if(tracks.empty()) return; for(size_t i=0;i tracksToProcess; - { - std::lock_guard lock(pendingTracksMutex); - tracksToProcess = pendingTracks; - } - - for (const std::string& path : tracksToProcess) { + while (true) { + std::string path; + { + std::lock_guard lock(pendingTracksMutex); + if (pendingTracks.empty()) break; + path = std::move(pendingTracks.front()); + pendingTracks.erase(pendingTracks.begin()); + } AudioTrack t; t.path = path; #ifdef _WIN32 @@ -200,7 +227,7 @@ void Audio::backgroundLoadingThread() { tracks.push_back(std::move(t)); } - loadedCount++; + loadedCount++; // Small delay to prevent overwhelming the system std::this_thread::sleep_for(std::chrono::milliseconds(10)); diff --git a/src/core/ApplicationManager.cpp b/src/core/ApplicationManager.cpp index 8911a44..d98f60c 100644 --- a/src/core/ApplicationManager.cpp +++ b/src/core/ApplicationManager.cpp @@ -17,6 +17,7 @@ #include "../graphics/Font.h" #include "../graphics/Starfield3D.h" #include "../graphics/Starfield.h" +#include "../graphics/GameRenderer.h" #include "../gameplay/Game.h" #include "../gameplay/LineEffect.h" #include @@ -122,6 +123,10 @@ void ApplicationManager::shutdown() { m_running = false; + // Stop audio systems before tearing down SDL to avoid aborts/asserts + Audio::instance().shutdown(); + SoundEffectManager::instance().shutdown(); + // Cleanup in reverse order of initialization cleanupManagers(); cleanupSDL(); @@ -193,11 +198,50 @@ bool ApplicationManager::initializeManagers() { 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); + + bool consume = false; + + // Global hotkeys (handled across all states) + if (pressed) { + // Toggle fullscreen on F11 or Alt+Enter (or Alt+KP_Enter) + if (sc == SDL_SCANCODE_F11 || + ((sc == SDL_SCANCODE_RETURN || sc == SDL_SCANCODE_RETURN2 || sc == SDL_SCANCODE_KP_ENTER) && + (SDL_GetModState() & SDL_KMOD_ALT))) { + if (m_renderManager) { + bool fs = m_renderManager->isFullscreen(); + m_renderManager->setFullscreen(!fs); + } + // Don’t also forward Alt+Enter as an Enter keypress to states (prevents accidental "Start") + consume = true; + } + + // M: Toggle/mute music; start playback if unmuting and not started yet + if (!consume && sc == SDL_SCANCODE_M) { + Audio::instance().toggleMute(); + m_musicEnabled = !m_musicEnabled; + if (m_musicEnabled && !m_musicStarted && Audio::instance().getLoadedTrackCount() > 0) { + Audio::instance().shuffle(); + Audio::instance().start(); + m_musicStarted = true; + } + consume = true; + } + + // N: Play a test sound effect + if (!consume && sc == SDL_SCANCODE_N) { + SoundEffectManager::instance().playSound("lets_go", 1.0f); + consume = true; + } + } + + // Forward to current state unless consumed + if (!consume) { + 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){ @@ -222,6 +266,12 @@ bool ApplicationManager::initializeManagers() { }); m_inputManager->registerWindowEventHandler([this](const SDL_WindowEvent& we){ + // Handle window resize events for RenderManager + if (we.type == SDL_EVENT_WINDOW_RESIZED && m_renderManager) { + m_renderManager->handleWindowResize(we.data1, we.data2); + } + + // Forward all window events to StateManager if (!m_stateManager) return; SDL_Event ev{}; ev.type = SDL_EVENT_WINDOW_RESIZED; // generic mapping; handlers can inspect inner fields @@ -230,8 +280,8 @@ bool ApplicationManager::initializeManagers() { }); 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, "[QUIT] InputManager quit handler invoked - setting running=false"); + m_running = false; }); } @@ -274,7 +324,9 @@ bool ApplicationManager::initializeGame() { // 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("great_move", "great_move"); m_assetManager->loadSoundEffectWithFallback("amazing", "amazing"); + m_assetManager->loadSoundEffectWithFallback("lets_go", "lets_go"); // Start background music loading m_assetManager->startBackgroundMusicLoading(); @@ -287,6 +339,9 @@ bool ApplicationManager::initializeGame() { // Create gameplay and line effect objects to populate StateContext like main.cpp m_lineEffect = std::make_unique(); + if (m_renderManager && m_renderManager->getSDLRenderer()) { + m_lineEffect->init(m_renderManager->getSDLRenderer()); + } m_game = std::make_unique(m_startLevelSelection); // Wire up sound callbacks as main.cpp did if (m_game) { @@ -324,6 +379,12 @@ bool ApplicationManager::initializeGame() { } if (m_totalTracks > 0) { Audio::instance().startBackgroundLoading(); + // Kick off playback now; Audio will pick a track once decoded. + // Do not mark as started yet; we'll flip the flag once a track is actually loaded. + if (m_musicEnabled) { + Audio::instance().shuffle(); + Audio::instance().start(); + } m_currentTrackLoading = 1; // mark started } @@ -356,6 +417,8 @@ bool ApplicationManager::initializeGame() { m_stateContext.backgroundTex = m_assetManager->getTexture("background"); m_stateContext.blocksTex = m_assetManager->getTexture("blocks"); m_stateContext.musicEnabled = &m_musicEnabled; + m_stateContext.musicStarted = &m_musicStarted; + m_stateContext.musicLoaded = &m_musicLoaded; m_stateContext.startLevelSelection = &m_startLevelSelection; m_stateContext.hoveredButton = &m_hoveredButton; m_stateContext.showSettingsPopup = &m_showSettingsPopup; @@ -451,32 +514,22 @@ void ApplicationManager::setupStateHandlers() { 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 }; + // Set viewport and scaling for content using ACTUAL window size + // Use RenderManager's computed logical viewport and scale so all states share the exact math + SDL_Rect logicalVP = {0,0,0,0}; + float logicalScale = 1.0f; + if (m_renderManager) { + logicalVP = m_renderManager->getLogicalViewport(); + logicalScale = m_renderManager->getLogicalScale(); + } 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; + // Calculate actual content area (centered within the viewport) + // Since we already have a centered viewport, content should be drawn at (0,0) in logical space + // The viewport itself handles the centering, so no additional offset is needed + float contentOffsetX = 0.0f; + float contentOffsetY = 0.0f; 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); @@ -535,9 +588,9 @@ void ApplicationManager::setupStateHandlers() { 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; + const std::string loadingText = "LOADING"; + int tW=0, tH=0; loadingFont->measure(loadingText, 1.0f, tW, tH); + float textX = (LOGICAL_W - (float)tW) * 0.5f; 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 { @@ -568,11 +621,11 @@ void ApplicationManager::setupStateHandlers() { 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; + std::string pStr(percentText); + int pW=0, pH=0; loadingFont->measure(pStr, 1.5f, pW, pH); + float percentX = (LOGICAL_W - (float)pW) * 0.5f; 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}); + loadingFont->draw(renderer.getSDLRenderer(), percentX + contentOffsetX, currentY + contentOffsetY, pStr, 1.5f, {255, 204, 0, 255}); } // Reset viewport and scale @@ -584,12 +637,18 @@ void ApplicationManager::setupStateHandlers() { [this](float deltaTime) { // Update 3D starfield so stars move during loading if (m_starfield3D) { - m_starfield3D->update(deltaTime); + // deltaTime here is in milliseconds; Starfield3D expects seconds + m_starfield3D->update(deltaTime / 1000.0f); } // Check if loading is complete and transition to menu if (m_assetManager->isLoadingComplete()) { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loading complete, transitioning to Menu"); + + // Update texture pointers now that assets are loaded + m_stateContext.backgroundTex = m_assetManager->getTexture("background"); + m_stateContext.blocksTex = m_assetManager->getTexture("blocks"); + 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"); @@ -609,19 +668,13 @@ void ApplicationManager::setupStateHandlers() { 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) + // Use RenderManager's computed logical viewport/scale for exact centering + SDL_Rect logicalVP = {0,0,0,0}; + float logicalScale = 1.0f; + if (m_renderManager) { + logicalVP = m_renderManager->getLogicalViewport(); + logicalScale = m_renderManager->getLogicalScale(); + } SDL_SetRenderViewport(renderer.getSDLRenderer(), &logicalVP); SDL_SetRenderScale(renderer.getSDLRenderer(), logicalScale, logicalScale); if (m_menuState) { @@ -645,19 +698,13 @@ void ApplicationManager::setupStateHandlers() { 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) + // Use RenderManager's computed logical viewport/scale for exact centering + SDL_Rect logicalVP = {0,0,0,0}; + float logicalScale = 1.0f; + if (m_renderManager) { + logicalVP = m_renderManager->getLogicalViewport(); + logicalScale = m_renderManager->getLogicalScale(); + } SDL_SetRenderViewport(renderer.getSDLRenderer(), &logicalVP); SDL_SetRenderScale(renderer.getSDLRenderer(), logicalScale, logicalScale); if (m_levelSelectorState) { @@ -671,13 +718,17 @@ void ApplicationManager::setupStateHandlers() { m_stateManager->registerUpdateHandler(AppState::Menu, [this](float deltaTime) { // Update logo animation counter - m_logoAnimCounter += deltaTime; + // deltaTime is in milliseconds; keep same behavior as main.cpp: counter += frameMs * 0.0008 + m_logoAnimCounter += (deltaTime * static_cast(Config::Animation::LOGO_ANIM_SPEED)); + // Also keep GlobalState's counter in sync for UI effects that read from it + GlobalState::instance().logoAnimCounter += (deltaTime * Config::Animation::LOGO_ANIM_SPEED); // Update fireworks effect GlobalState& globalState = GlobalState::instance(); + // updateFireworks expects milliseconds globalState.updateFireworks(deltaTime); - // Start background music once tracks are available and not yet started + // Start music as soon as at least one track has decoded (don’t wait for all) if (m_musicEnabled && !m_musicStarted) { if (Audio::instance().getLoadedTrackCount() > 0) { Audio::instance().shuffle(); @@ -685,6 +736,10 @@ void ApplicationManager::setupStateHandlers() { m_musicStarted = true; } } + // Track completion status for UI + if (!m_musicLoaded && Audio::instance().isLoadingComplete()) { + m_musicLoaded = true; + } }); m_stateManager->registerEventHandler(AppState::Menu, @@ -711,17 +766,10 @@ void ApplicationManager::setupStateHandlers() { m_showExitConfirmPopup = true; return; } - // Global toggles - if (event.key.scancode == SDL_SCANCODE_M) { - Audio::instance().toggleMute(); - m_musicEnabled = !m_musicEnabled; - } + // S: toggle SFX enable state (music handled globally) 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 @@ -729,11 +777,9 @@ void ApplicationManager::setupStateHandlers() { 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}; + // Use RenderManager's computed logical viewport/scale for precise mapping + SDL_Rect logicalVP{0,0,0,0}; float logicalScale = 1.0f; + if (m_renderManager) { logicalVP = m_renderManager->getLogicalViewport(); logicalScale = m_renderManager->getLogicalScale(); } // 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; @@ -773,11 +819,9 @@ void ApplicationManager::setupStateHandlers() { 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}; + // Use RenderManager's computed logical viewport/scale for precise mapping + SDL_Rect logicalVP{0,0,0,0}; float logicalScale = 1.0f; + if (m_renderManager) { logicalVP = m_renderManager->getLogicalViewport(); logicalScale = m_renderManager->getLogicalScale(); } 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; @@ -801,35 +845,301 @@ void ApplicationManager::setupStateHandlers() { } }); - // 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}); + // GameOver State - Handle restart and return to menu + m_stateManager->registerEventHandler(AppState::GameOver, + [this](const SDL_Event& event) { + if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) { + // Enter/Space - restart game + if (event.key.scancode == SDL_SCANCODE_RETURN || + event.key.scancode == SDL_SCANCODE_RETURN2 || + event.key.scancode == SDL_SCANCODE_KP_ENTER || + event.key.scancode == SDL_SCANCODE_SPACE) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Restarting game from GameOver (Enter/Space)"); + // Reset game with current start level and transition to Playing + if (m_stateContext.game) { + m_stateContext.game->reset(m_startLevelSelection); + } + m_stateManager->setState(AppState::Playing); + return; + } + // Escape - return to menu + if (event.key.scancode == SDL_SCANCODE_ESCAPE) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Returning to menu from GameOver (Escape)"); + m_stateManager->setState(AppState::Menu); + return; + } } }); + // Playing State - Full game rendering 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); + // Handle mouse clicks on the exit confirmation popup + if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && m_showExitConfirmPopup) { + 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); + + const float LOGICAL_W = static_cast(Config::Logical::WIDTH); + const float LOGICAL_H = static_cast(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); + + SDL_Rect logicalVP{0, 0, winW, winH}; + if (mx < logicalVP.x || my < logicalVP.y || mx > logicalVP.x + logicalVP.w || my > logicalVP.y + logicalVP.h) return; + + float lx = (mx - logicalVP.x) / (logicalScale > 0.f ? logicalScale : 1.f); + float ly = (my - logicalVP.y) / (logicalScale > 0.f ? logicalScale : 1.f); + + // Compute content offsets to convert to content-local logical coords (what renderer uses) + float contentW = LOGICAL_W * logicalScale; + float contentH = LOGICAL_H * logicalScale; + float contentOffsetX = (winW - contentW) * 0.5f / (logicalScale > 0.f ? logicalScale : 1.f); + float contentOffsetY = (winH - contentH) * 0.5f / (logicalScale > 0.f ? logicalScale : 1.f); + float localX = lx - contentOffsetX; + float localY = ly - contentOffsetY; + + // Popup geometry (must match GameRenderer) + float popupW = 420.0f, popupH = 180.0f; + float popupX = (LOGICAL_W - popupW) * 0.5f; + float popupY = (LOGICAL_H - popupH) * 0.5f; + float btnW = 140.0f, btnH = 46.0f; + float yesX = popupX + popupW * 0.25f - btnW * 0.5f; + float noX = popupX + popupW * 0.75f - btnW * 0.5f; + float btnY = popupY + popupH - 60.0f; + + // Only react if click is inside popup + if (localX >= popupX && localX <= popupX + popupW && localY >= popupY && localY <= popupY + popupH) { + if (localX >= yesX && localX <= yesX + btnW && localY >= btnY && localY <= btnY + btnH) { + // YES: go back to menu (reset game) + m_showExitConfirmPopup = false; + if (m_stateContext.game) m_stateContext.game->reset(m_startLevelSelection); + if (m_stateManager) m_stateManager->setState(AppState::Menu); + return; + } + if (localX >= noX && localX <= noX + btnW && localY >= btnY && localY <= btnY + btnH) { + // NO: close popup and resume + m_showExitConfirmPopup = false; + if (m_stateContext.game) m_stateContext.game->setPaused(false); + return; + } } } }); + m_stateManager->registerRenderHandler(AppState::Playing, + [this](RenderManager& renderer) { + // Clear the screen first + renderer.clear(0, 0, 0, 255); + + // Window size + int winW = 0, winH = 0; + renderer.getWindowSize(winW, winH); + + // Draw per-level background stretched to full window, with fade + if (m_stateContext.game) { + // Update fade progression (ms based on frame time not available here; approximate using SDL ticks delta if desired) + // We'll keep alpha as-is; Loading/Menu update can adjust if we wire a timer. For now, simply show the correct background. + int currentLevel = m_stateContext.game->level(); + int bgLevel = (currentLevel > 32) ? 32 : currentLevel; // Cap at 32 like main.cpp + + if (m_cachedBgLevel != bgLevel) { + if (m_nextLevelBackgroundTex) { SDL_DestroyTexture(m_nextLevelBackgroundTex); m_nextLevelBackgroundTex = nullptr; } + char bgPath[256]; + std::snprintf(bgPath, sizeof(bgPath), "assets/images/tetris_main_back_level%d.bmp", bgLevel); + SDL_Surface* s = SDL_LoadBMP(bgPath); + if (s && renderer.getSDLRenderer()) { + m_nextLevelBackgroundTex = SDL_CreateTextureFromSurface(renderer.getSDLRenderer(), s); + SDL_DestroySurface(s); + m_levelFadeAlpha = 0.0f; + m_levelFadeElapsed = 0.0f; + m_cachedBgLevel = bgLevel; + } else { + m_cachedBgLevel = -1; // don’t change if missing + if (s) SDL_DestroySurface(s); + } + } + + if (winW > 0 && winH > 0) { + SDL_FRect full{0,0,(float)winW,(float)winH}; + if (m_nextLevelBackgroundTex && m_levelFadeAlpha < 1.0f && m_levelBackgroundTex) { + SDL_SetTextureAlphaMod(m_levelBackgroundTex, Uint8((1.0f - m_levelFadeAlpha) * 255)); + SDL_RenderTexture(renderer.getSDLRenderer(), m_levelBackgroundTex, nullptr, &full); + SDL_SetTextureAlphaMod(m_nextLevelBackgroundTex, Uint8(m_levelFadeAlpha * 255)); + SDL_RenderTexture(renderer.getSDLRenderer(), m_nextLevelBackgroundTex, nullptr, &full); + SDL_SetTextureAlphaMod(m_levelBackgroundTex, 255); + SDL_SetTextureAlphaMod(m_nextLevelBackgroundTex, 255); + } else if (m_nextLevelBackgroundTex && (!m_levelBackgroundTex || m_levelFadeAlpha >= 1.0f)) { + if (m_levelBackgroundTex) SDL_DestroyTexture(m_levelBackgroundTex); + m_levelBackgroundTex = m_nextLevelBackgroundTex; + m_nextLevelBackgroundTex = nullptr; + m_levelFadeAlpha = 0.0f; + SDL_RenderTexture(renderer.getSDLRenderer(), m_levelBackgroundTex, nullptr, &full); + } else if (m_levelBackgroundTex) { + SDL_RenderTexture(renderer.getSDLRenderer(), m_levelBackgroundTex, nullptr, &full); + } + } + } + + // Compute logical scale from logical design size + const float LOGICAL_W = static_cast(Config::Logical::WIDTH); + const float LOGICAL_H = static_cast(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); + + // Use full-window viewport; GameRenderer applies its own content offsets for centering + SDL_Rect logicalVP = {0, 0, winW, winH}; + SDL_SetRenderViewport(renderer.getSDLRenderer(), &logicalVP); + + // Use GameRenderer for actual game rendering + GameRenderer::renderPlayingState( + renderer.getSDLRenderer(), + m_stateContext.game, + m_stateContext.pixelFont, + m_stateContext.lineEffect, + m_stateContext.blocksTex, + LOGICAL_W, + LOGICAL_H, + logicalScale, + static_cast(winW), + static_cast(winH), + m_showExitConfirmPopup + ); + + // Reset viewport + SDL_SetRenderViewport(renderer.getSDLRenderer(), nullptr); + }); + + // GameOver State - Simple game over screen + m_stateManager->registerRenderHandler(AppState::GameOver, + [this](RenderManager& renderer) { + // Clear the screen first + renderer.clear(12, 12, 16, 255); + + // Calculate viewport and scale for responsive layout + int winW = 0, winH = 0; + renderer.getWindowSize(winW, winH); + + const float LOGICAL_W = static_cast(Config::Window::DEFAULT_WIDTH); + const float LOGICAL_H = static_cast(Config::Window::DEFAULT_HEIGHT); + + float scaleX = static_cast(winW) / LOGICAL_W; + float scaleY = static_cast(winH) / LOGICAL_H; + float logicalScale = std::min(scaleX, scaleY); + + int scaledW = static_cast(LOGICAL_W * logicalScale); + int scaledH = static_cast(LOGICAL_H * logicalScale); + int offsetX = (winW - scaledW) / 2; + int offsetY = (winH - scaledH) / 2; + + SDL_Rect logicalVP = {offsetX, offsetY, scaledW, scaledH}; + SDL_SetRenderViewport(renderer.getSDLRenderer(), &logicalVP); + + // Draw starfield background + if (m_starfield) { + m_starfield->draw(renderer.getSDLRenderer()); + } + + // Game over text and stats + if (m_stateContext.pixelFont && m_stateContext.game) { + FontAtlas& font = *m_stateContext.pixelFont; + + // "GAME OVER" title + font.draw(renderer.getSDLRenderer(), LOGICAL_W * 0.5f - 120, 140, "GAME OVER", 3.0f, {255, 80, 60, 255}); + + // Game stats + char buf[128]; + std::snprintf(buf, sizeof(buf), "SCORE %d LINES %d LEVEL %d", + m_stateContext.game->score(), + m_stateContext.game->lines(), + m_stateContext.game->level()); + font.draw(renderer.getSDLRenderer(), LOGICAL_W * 0.5f - 180, 220, buf, 1.2f, {220, 220, 230, 255}); + + // Instructions + font.draw(renderer.getSDLRenderer(), LOGICAL_W * 0.5f - 120, 270, "PRESS ENTER / SPACE", 1.2f, {200, 200, 220, 255}); + } + + // Reset viewport + SDL_SetRenderViewport(renderer.getSDLRenderer(), nullptr); + }); + + // Playing State - Update handler for DAS/ARR movement timing + m_stateManager->registerUpdateHandler(AppState::Playing, + [this](double frameMs) { + if (!m_stateContext.game) return; + + // Get current keyboard state + const bool *ks = SDL_GetKeyboardState(nullptr); + bool left = ks[SDL_SCANCODE_LEFT] || ks[SDL_SCANCODE_A]; + bool right = ks[SDL_SCANCODE_RIGHT] || ks[SDL_SCANCODE_D]; + bool down = ks[SDL_SCANCODE_DOWN] || ks[SDL_SCANCODE_S]; + + // Handle soft drop + m_stateContext.game->setSoftDropping(down && !m_stateContext.game->isPaused()); + + // Handle DAS/ARR movement timing (from original main.cpp) + int moveDir = 0; + if (left && !right) + moveDir = -1; + else if (right && !left) + moveDir = +1; + + if (moveDir != 0 && !m_stateContext.game->isPaused()) { + if ((moveDir == -1 && !m_leftHeld) || (moveDir == +1 && !m_rightHeld)) { + // First press - immediate movement + m_stateContext.game->move(moveDir); + m_moveTimerMs = DAS; // Set initial delay + } else { + // Key held - handle repeat timing + m_moveTimerMs -= frameMs; + if (m_moveTimerMs <= 0) { + m_stateContext.game->move(moveDir); + m_moveTimerMs += ARR; // Set repeat rate + } + } + } else { + m_moveTimerMs = 0; // Reset timer when no movement + } + + // Update held state for next frame + m_leftHeld = left; + m_rightHeld = right; + + // Handle soft drop boost + if (down && !m_stateContext.game->isPaused()) { + m_stateContext.game->softDropBoost(frameMs); + } + + // Delegate to PlayingState for other updates (gravity, line effects) + if (m_playingState) { + m_playingState->update(frameMs); + } + + // Update background fade progression (match main.cpp semantics approx) + // Duration 1200ms fade (same as LEVEL_FADE_DURATION used in main.cpp snippets) + const float LEVEL_FADE_DURATION = 1200.0f; + if (m_nextLevelBackgroundTex) { + m_levelFadeElapsed += (float)frameMs; + m_levelFadeAlpha = std::min(1.0f, m_levelFadeElapsed / LEVEL_FADE_DURATION); + } + + // Check for game over and transition to GameOver state + if (m_stateContext.game->isGameOver()) { + // Submit score before transitioning + if (m_stateContext.scores) { + m_stateContext.scores->submit( + m_stateContext.game->score(), + m_stateContext.game->lines(), + m_stateContext.game->level(), + m_stateContext.game->elapsed() + ); + } + m_stateManager->setState(AppState::GameOver); + } + }); } void ApplicationManager::processEvents() { @@ -867,7 +1177,9 @@ void ApplicationManager::update(float deltaTime) { // Update StateManager if (m_stateManager) { - m_stateManager->update(deltaTime); + // NOTE: State update handlers expect milliseconds (frameMs). Convert seconds -> ms here. + float frameMs = deltaTime * 1000.0f; + m_stateManager->update(frameMs); SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "ApplicationManager::update - state update completed for state %s", m_stateManager->getStateName(m_stateManager->getState())); traceFile("update completed"); } @@ -896,6 +1208,12 @@ void ApplicationManager::render() { void ApplicationManager::cleanupManagers() { // Cleanup managers in reverse order + // Destroy gameplay background textures + if (m_levelBackgroundTex) { SDL_DestroyTexture(m_levelBackgroundTex); m_levelBackgroundTex = nullptr; } + if (m_nextLevelBackgroundTex) { SDL_DestroyTexture(m_nextLevelBackgroundTex); m_nextLevelBackgroundTex = nullptr; } + // Shutdown subsystems that own GPU resources before renderer destruction + if (m_lineEffect) { m_lineEffect->shutdown(); } + // Fonts are managed by AssetManager; ensure it shuts down after we stop states m_stateManager.reset(); m_assetManager.reset(); m_inputManager.reset(); diff --git a/src/core/ApplicationManager.h b/src/core/ApplicationManager.h index 79697e2..cc70bac 100644 --- a/src/core/ApplicationManager.h +++ b/src/core/ApplicationManager.h @@ -96,6 +96,12 @@ private: std::unique_ptr m_game; std::unique_ptr m_lineEffect; + // DAS/ARR movement timing (from original main.cpp) + bool m_leftHeld = false; + bool m_rightHeld = false; + double m_moveTimerMs = 0.0; + static constexpr double DAS = 170.0; // Delayed Auto Shift + static constexpr double ARR = 40.0; // Auto Repeat Rate // State context (must be a member to ensure lifetime) StateContext m_stateContext; @@ -119,4 +125,11 @@ private: // Animation state float m_logoAnimCounter = 0.0f; + + // Gameplay background (per-level) with fade, mirroring main.cpp behavior + SDL_Texture* m_levelBackgroundTex = nullptr; + SDL_Texture* m_nextLevelBackgroundTex = nullptr; // used during fade transitions + float m_levelFadeAlpha = 0.0f; // 0..1 blend factor + float m_levelFadeElapsed = 0.0f; // ms + int m_cachedBgLevel = -1; // last loaded background level index }; diff --git a/src/core/GlobalState.cpp b/src/core/GlobalState.cpp index f8121e3..f81c608 100644 --- a/src/core/GlobalState.cpp +++ b/src/core/GlobalState.cpp @@ -46,8 +46,9 @@ void GlobalState::updateFireworks(double frameMs) { // Create new fireworks occasionally if (currentTime - lastFireworkTime > 800 + (rand() % 1200)) { - float x = Config::Logical::WIDTH * 0.2f + (rand() % (int)(Config::Logical::WIDTH * 0.6f)); - float y = Config::Logical::HEIGHT * 0.3f + (rand() % (int)(Config::Logical::HEIGHT * 0.4f)); + // Spawn bias similar to legacy: lower-right area + float x = Config::Logical::WIDTH * (0.55f + (rand() % 35) / 100.0f); // ~55% - 90% + float y = Config::Logical::HEIGHT * (0.80f + (rand() % 15) / 100.0f); // ~80% - 95% createFirework(x, y); lastFireworkTime = currentTime; } @@ -60,15 +61,17 @@ void GlobalState::updateFireworks(double frameMs) { for (auto& particle : firework.particles) { if (particle.life <= 0) continue; - // Update physics - particle.x += particle.vx * (frameMs / 1000.0f); - particle.y += particle.vy * (frameMs / 1000.0f); - particle.vy += 150.0f * (frameMs / 1000.0f); // Gravity + // Update physics (gentler gravity, slight friction) + float dt = float(frameMs / 1000.0f); + particle.x += particle.vx * dt; + particle.y += particle.vy * dt; + particle.vx *= (1.0f - 0.6f * dt); // horizontal friction + particle.vy = particle.vy * (1.0f - 0.3f * dt) + 90.0f * dt; // gravity with damping particle.life -= frameMs; - // Fade size over time + // Smaller particles overall float lifeRatio = particle.life / particle.maxLife; - particle.size = 20.0f + 10.0f * lifeRatio; + particle.size = 6.0f + 5.0f * lifeRatio; if (particle.life > 0) { hasActiveParticles = true; @@ -100,7 +103,7 @@ void GlobalState::createFirework(float x, float y) { firework->particles.clear(); // Create particles - const int particleCount = 12 + (rand() % 8); + const int particleCount = 10 + (rand() % 6); for (int i = 0; i < particleCount; ++i) { BlockParticle particle; particle.x = x; @@ -108,14 +111,14 @@ void GlobalState::createFirework(float x, float y) { // Random velocity in all directions float angle = (float)(rand() % 360) * 3.14159f / 180.0f; - float speed = 80.0f + (rand() % 120); + float speed = 70.0f + (rand() % 90); particle.vx = cos(angle) * speed; particle.vy = sin(angle) * speed - 50.0f; // Slight upward bias particle.type = 1 + (rand() % 7); // Random tetris piece color - particle.maxLife = 1500.0f + (rand() % 1000); // 1.5-2.5 seconds + particle.maxLife = 1200.0f + (rand() % 800); // ~1.2-2.0 seconds particle.life = particle.maxLife; - particle.size = 15.0f + (rand() % 15); + particle.size = 6.0f + (rand() % 5); firework->particles.push_back(particle); } @@ -132,7 +135,8 @@ void GlobalState::drawFireworks(SDL_Renderer* renderer, SDL_Texture* blocksTex) // Calculate alpha based on remaining life float lifeRatio = particle.life / particle.maxLife; - Uint8 alpha = (Uint8)(255 * std::min(1.0f, lifeRatio * 2.0f)); + // Faster fade like legacy + Uint8 alpha = (Uint8)(255 * std::min(1.0f, lifeRatio * 1.6f)); // Set texture alpha SDL_SetTextureAlphaMod(blocksTex, alpha); diff --git a/src/gameplay/Game.cpp b/src/gameplay/Game.cpp index 3a3ea14..6bb1e0d 100644 --- a/src/gameplay/Game.cpp +++ b/src/gameplay/Game.cpp @@ -2,6 +2,7 @@ #include "gameplay/Game.h" #include #include +#include // Piece rotation bitmasks (row-major 4x4). Bit 0 = (0,0). static const std::array SHAPES = {{ @@ -238,7 +239,9 @@ bool Game::tryMoveDown() { void Game::tickGravity(double frameMs) { if (paused) return; // Don't tick gravity when paused + fallAcc += frameMs; + while (fallAcc >= gravityMs) { // Attempt to move down by one row if (tryMoveDown()) { diff --git a/src/gameplay/Game.h b/src/gameplay/Game.h index 0c95fdc..da03597 100644 --- a/src/gameplay/Game.h +++ b/src/gameplay/Game.h @@ -98,7 +98,7 @@ private: LevelUpCallback levelUpCallback; // Gravity tuning ----------------------------------------------------- // Global multiplier applied to all level timings (use to slow/speed whole-game gravity) - double gravityGlobalMultiplier{2.8}; + double gravityGlobalMultiplier{1.0}; // Gravity manager encapsulates frames table, multipliers and conversions GravityManager gravityMgr; // Backwards-compatible accessors (delegate to gravityMgr) diff --git a/src/gameplay/LineEffect.cpp b/src/gameplay/LineEffect.cpp index 6c80cbd..250ab05 100644 --- a/src/gameplay/LineEffect.cpp +++ b/src/gameplay/LineEffect.cpp @@ -2,6 +2,7 @@ #include "LineEffect.h" #include #include +#include "audio/Audio.h" #ifndef M_PI #define M_PI 3.14159265358979323846 @@ -80,15 +81,11 @@ bool LineEffect::init(SDL_Renderer* r) { } void LineEffect::shutdown() { - if (audioStream) { - SDL_DestroyAudioStream(audioStream); - audioStream = nullptr; - } + // No separate audio stream anymore; SFX go through shared Audio mixer } void LineEffect::initAudio() { - // For now, we'll generate simple beep sounds procedurally - // In a full implementation, you'd load WAV files + // Generate simple beep sounds procedurally (fallback when voice SFX not provided) // Generate a simple line clear beep (440Hz for 0.2 seconds) int sampleRate = 44100; @@ -265,33 +262,10 @@ void LineEffect::renderExplosion() { } void LineEffect::playLineClearSound(int lineCount) { - if (!audioStream) { - // Create audio stream for sound effects - SDL_AudioSpec spec = {}; - spec.format = SDL_AUDIO_S16; - spec.channels = 2; - spec.freq = 44100; - - audioStream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, nullptr, nullptr); - if (!audioStream) { - SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Warning: Could not create audio stream for line clear effects"); - return; - } - } - // Choose appropriate sound based on line count - const std::vector* sample = nullptr; - - if (lineCount == 4) { - sample = &tetrisSample; // Special sound for Tetris - //printf("TETRIS! 4 lines cleared!\n"); - } else { - sample = &lineClearSample; // Regular line clear sound - //printf("Line clear: %d lines\n", lineCount); - } - + const std::vector* sample = (lineCount == 4) ? &tetrisSample : &lineClearSample; if (sample && !sample->empty()) { - SDL_PutAudioStreamData(audioStream, sample->data(), - static_cast(sample->size() * sizeof(int16_t))); + // Mix via shared Audio device so it layers with music + Audio::instance().playSfx(*sample, 2, 44100, (lineCount == 4) ? 0.9f : 0.7f); } } diff --git a/src/graphics/GameRenderer.cpp b/src/graphics/GameRenderer.cpp new file mode 100644 index 0000000..a661ee4 --- /dev/null +++ b/src/graphics/GameRenderer.cpp @@ -0,0 +1,480 @@ +#include "GameRenderer.h" +#include "../gameplay/Game.h" +#include "../graphics/Font.h" +#include "../gameplay/LineEffect.h" +#include +#include +#include + +// Color constants (copied from main.cpp) +static const SDL_Color COLORS[] = { + {0, 0, 0, 255}, // 0: BLACK (empty) + {0, 255, 255, 255}, // 1: I-piece - cyan + {255, 255, 0, 255}, // 2: O-piece - yellow + {128, 0, 128, 255}, // 3: T-piece - purple + {0, 255, 0, 255}, // 4: S-piece - green + {255, 0, 0, 255}, // 5: Z-piece - red + {0, 0, 255, 255}, // 6: J-piece - blue + {255, 165, 0, 255} // 7: L-piece - orange +}; + +void GameRenderer::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 GameRenderer::drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType) { + if (!blocksTex || blockType < 0 || blockType >= PIECE_COUNT) { + // 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); +} + +void GameRenderer::drawPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, const Game::Piece& piece, float ox, float oy, float tileSize, bool isGhost) { + 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); + } + } + } + } +} + +void GameRenderer::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 == 0) { offsetX = tileSize * 0.5f; } // I-piece centering (assuming I = 0) + else if (pieceType == 1) { offsetX = tileSize * 0.5f; } // O-piece centering (assuming O = 1) + + // Use semi-transparent alpha for preview blocks + Uint8 previewAlpha = 180; + if (blocksTex) { + 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); + } + } + } + + // Reset alpha + if (blocksTex) { + SDL_SetTextureAlphaMod(blocksTex, 255); + } +} + +void GameRenderer::renderPlayingState( + SDL_Renderer* renderer, + Game* game, + FontAtlas* pixelFont, + LineEffect* lineEffect, + SDL_Texture* blocksTex, + float logicalW, + float logicalH, + float logicalScale, + float winW, + float winH, + bool showExitConfirmPopup +) { + if (!game || !pixelFont) return; + + // Calculate actual content area (centered within the window) + float contentScale = logicalScale; + float contentW = logicalW * contentScale; + float contentH = logicalH * contentScale; + float contentOffsetX = (winW - contentW) * 0.5f / contentScale; + float contentOffsetY = (winH - contentH) * 0.5f / contentScale; + + // Helper lambda for drawing rectangles with content offset + auto drawRectWithOffset = [&](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 + const float MIN_MARGIN = 40.0f; + const float TOP_MARGIN = 60.0f; + const float PANEL_WIDTH = 180.0f; + const float PANEL_SPACING = 30.0f; + const float NEXT_PIECE_HEIGHT = 120.0f; + const float BOTTOM_MARGIN = 60.0f; + + // Calculate layout dimensions + const float availableWidth = logicalW - (MIN_MARGIN * 2) - (PANEL_WIDTH * 2) - (PANEL_SPACING * 2); + const float availableHeight = logicalH - TOP_MARGIN - BOTTOM_MARGIN - NEXT_PIECE_HEIGHT; + + const float maxBlockSizeW = availableWidth / Game::COLS; + const float maxBlockSizeH = availableHeight / Game::ROWS; + const float BLOCK_SIZE = std::min(maxBlockSizeW, maxBlockSizeH); + 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 positions + const float totalContentHeight = NEXT_PIECE_HEIGHT + GRID_H; + const float availableVerticalSpace = logicalH - TOP_MARGIN - BOTTOM_MARGIN; + const float verticalCenterOffset = (availableVerticalSpace - totalContentHeight) * 0.5f; + const float contentStartY = TOP_MARGIN + verticalCenterOffset; + + const float totalLayoutWidth = PANEL_WIDTH + PANEL_SPACING + GRID_W + PANEL_SPACING + PANEL_WIDTH; + const float layoutStartX = (logicalW - totalLayoutWidth) * 0.5f; + + 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; + const float gridY = contentStartY + NEXT_PIECE_HEIGHT + contentOffsetY; + + const float statsY = gridY; + const float statsW = PANEL_WIDTH; + const float statsH = GRID_H; + + // Next piece preview position + 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 + if (game->hasCompletedLines() && lineEffect && !lineEffect->isActive()) { + auto completedLines = game->getCompletedLines(); + lineEffect->startLineClear(completedLines, static_cast(gridX), static_cast(gridY), static_cast(finalBlockSize)); + } + + // Draw game grid border + drawRectWithOffset(gridX - 3 - contentOffsetX, gridY - 3 - contentOffsetY, GRID_W + 6, GRID_H + 6, {100, 120, 200, 255}); + drawRectWithOffset(gridX - 1 - contentOffsetX, gridY - 1 - contentOffsetY, GRID_W + 2, GRID_H + 2, {60, 80, 160, 255}); + drawRectWithOffset(gridX - contentOffsetX, gridY - contentOffsetY, GRID_W, GRID_H, {20, 25, 35, 255}); + + // Draw panel backgrounds + SDL_SetRenderDrawColor(renderer, 10, 15, 25, 160); + SDL_FRect lbg{statsX - 16, gridY - 10, statsW + 32, GRID_H + 20}; + SDL_RenderFillRect(renderer, &lbg); + + SDL_FRect rbg{scoreX - 16, gridY - 16, statsW + 32, GRID_H + 32}; + SDL_RenderFillRect(renderer, &rbg); + + // Draw grid lines + SDL_SetRenderDrawColor(renderer, 40, 45, 60, 255); + for (int x = 1; x < Game::COLS; ++x) { + float lineX = gridX + x * finalBlockSize; + SDL_RenderLine(renderer, lineX, gridY, lineX, gridY + GRID_H); + } + for (int y = 1; y < Game::ROWS; ++y) { + float lineY = gridY + y * finalBlockSize; + SDL_RenderLine(renderer, gridX, lineY, gridX + GRID_W, lineY); + } + + // Draw block statistics panel border + drawRectWithOffset(statsX - 3 - contentOffsetX, statsY - 3 - contentOffsetY, statsW + 6, statsH + 6, {100, 120, 200, 255}); + drawRectWithOffset(statsX - contentOffsetX, statsY - contentOffsetY, statsW, statsH, {30, 35, 50, 255}); + + // Draw next piece preview panel border + drawRectWithOffset(nextX - 3 - contentOffsetX, nextY - 3 - contentOffsetY, nextW + 6, nextH + 6, {100, 120, 200, 255}); + drawRectWithOffset(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 && 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, static_cast(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"}; + float yCursor = statsY + 52; + + for (int i = 0; i < PIECE_COUNT; ++i) { + float py = yCursor; + + // Draw small piece icon + float previewSize = finalBlockSize * 0.55f; + drawSmallPiece(renderer, blocksTex, static_cast(i), statsX + 18, py, previewSize); + + // Compute preview height + 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 display + 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 bar + 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; + + pixelFont->draw(renderer, barX, barY - 16, percStr, 0.8f, {230, 230, 235, 255}); + + // Progress bar + SDL_SetRenderDrawColor(renderer, 170, 170, 175, 200); + SDL_FRect track{barX, barY, barW, barH}; + SDL_RenderFillRect(renderer, &track); + + 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); + + yCursor = barY + barH + 18.0f; + } + + // Draw score panel (right side) + const float contentTopOffset = 0.0f; + const float contentBottomOffset = 290.0f; + 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 + int startLv = game->startLevelBase(); + 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 display + 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 + 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, logicalW - 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, static_cast(game->held().type), statsX + 60, statsY + statsH - 80, finalBlockSize * 0.6f); + } + + // Pause overlay + if (game->isPaused() && !showExitConfirmPopup) { + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 180); + SDL_FRect pauseOverlay{0, 0, logicalW, logicalH}; + SDL_RenderFillRect(renderer, &pauseOverlay); + + pixelFont->draw(renderer, logicalW * 0.5f - 80, logicalH * 0.5f - 20, "PAUSED", 2.0f, {255, 255, 255, 255}); + pixelFont->draw(renderer, logicalW * 0.5f - 120, logicalH * 0.5f + 30, "Press P to resume", 0.8f, {200, 200, 220, 255}); + } + + // Exit confirmation popup + if (showExitConfirmPopup) { + float popupW = 420.0f, popupH = 180.0f; + float popupX = (logicalW - popupW) * 0.5f; + float popupY = (logicalH - popupH) * 0.5f; + + // Dim entire window (do not change viewport/scales here) + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 200); + SDL_FRect fullWin{0.f, 0.f, winW, winH}; + SDL_RenderFillRect(renderer, &fullWin); + + // Draw popup box in logical coords with content offsets + drawRectWithOffset(popupX - 4.0f, popupY - 4.0f, popupW + 8.0f, popupH + 8.0f, {60, 70, 90, 255}); + drawRectWithOffset(popupX, popupY, popupW, popupH, {20, 22, 28, 240}); + + // Text content (measure to perfectly center) + 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 + contentOffsetX; + float l1X = popupX + (popupW - (float)wL1) * 0.5f + contentOffsetX; + float l2X = popupX + (popupW - (float)wL2) * 0.5f + contentOffsetX; + + pixelFont->draw(renderer, titleX, popupY + contentOffsetY + 20.0f, title, 1.6f, {255, 220, 0, 255}); + pixelFont->draw(renderer, l1X, popupY + contentOffsetY + 60.0f, line1, 0.9f, {220, 220, 230, 255}); + pixelFont->draw(renderer, l2X, popupY + contentOffsetY + 84.0f, line2, 0.9f, {220, 220, 230, 255}); + + // Buttons + float btnW = 140.0f, btnH = 46.0f; + float yesX = popupX + popupW * 0.25f - btnW * 0.5f; + float noX = popupX + popupW * 0.75f - btnW * 0.5f; + float btnY = popupY + popupH - 60.0f; + + // YES button + drawRectWithOffset(yesX - 2.0f, btnY - 2.0f, btnW + 4.0f, btnH + 4.0f, {100, 120, 140, 255}); + drawRectWithOffset(yesX, btnY, btnW, btnH, {200, 60, 60, 255}); + const std::string yes = "YES"; + int wYes=0,hYes=0; pixelFont->measure(yes, 1.0f, wYes, hYes); + pixelFont->draw(renderer, yesX + (btnW - (float)wYes) * 0.5f + contentOffsetX, + btnY + (btnH - (float)hYes) * 0.5f + contentOffsetY, + yes, 1.0f, {255, 255, 255, 255}); + + // NO button + drawRectWithOffset(noX - 2.0f, btnY - 2.0f, btnW + 4.0f, btnH + 4.0f, {100, 120, 140, 255}); + drawRectWithOffset(noX, btnY, btnW, btnH, {80, 140, 80, 255}); + const std::string no = "NO"; + int wNo=0,hNo=0; pixelFont->measure(no, 1.0f, wNo, hNo); + pixelFont->draw(renderer, noX + (btnW - (float)wNo) * 0.5f + contentOffsetX, + btnY + (btnH - (float)hNo) * 0.5f + contentOffsetY, + no, 1.0f, {255, 255, 255, 255}); + } +} diff --git a/src/graphics/GameRenderer.h b/src/graphics/GameRenderer.h new file mode 100644 index 0000000..15e5192 --- /dev/null +++ b/src/graphics/GameRenderer.h @@ -0,0 +1,40 @@ +#pragma once +#include +#include "../gameplay/Game.h" + +// Forward declarations +class FontAtlas; +class LineEffect; + +/** + * GameRenderer - Utility class for rendering the Tetris game board and HUD. + * + * This class encapsulates all the game-specific rendering logic that was + * previously in main.cpp, making it reusable across different contexts. + */ +class GameRenderer { +public: + // Render the complete playing state including game board, HUD, and effects + static void renderPlayingState( + SDL_Renderer* renderer, + Game* game, + FontAtlas* pixelFont, + LineEffect* lineEffect, + SDL_Texture* blocksTex, + float logicalW, + float logicalH, + float logicalScale, + float winW, + float winH, + bool showExitConfirmPopup + ); + +private: + // Helper functions for drawing game elements + static void drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType); + static void drawPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, const Game::Piece& piece, float ox, float oy, float tileSize, bool isGhost = false); + static void drawSmallPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, PieceType pieceType, float x, float y, float tileSize); + + // Helper function for drawing rectangles + static void drawRect(SDL_Renderer* renderer, float x, float y, float w, float h, SDL_Color c); +}; diff --git a/src/graphics/RenderManager.cpp b/src/graphics/RenderManager.cpp index 61b3004..c8f260f 100644 --- a/src/graphics/RenderManager.cpp +++ b/src/graphics/RenderManager.cpp @@ -130,6 +130,8 @@ void RenderManager::setViewport(int x, int y, int width, int height) { SDL_Rect viewport = { x, y, width, height }; SDL_SetRenderViewport(m_renderer, &viewport); + // Keep cached logical viewport in sync if this matches our computed logical scale + m_logicalVP = viewport; } void RenderManager::setScale(float scaleX, float scaleY) { @@ -147,6 +149,7 @@ void RenderManager::resetViewport() { return; } + // Reset to full window viewport and recompute logical scale/viewport SDL_SetRenderViewport(m_renderer, nullptr); updateLogicalScale(); } @@ -281,7 +284,15 @@ void RenderManager::updateLogicalScale() { } setScale(scale, scale); + // Compute centered logical viewport that preserves aspect ratio and is centered in window + int vpW = static_cast(m_logicalWidth * scale); + int vpH = static_cast(m_logicalHeight * scale); + int vpX = (m_windowWidth - vpW) / 2; + int vpY = (m_windowHeight - vpH) / 2; + SDL_Rect vp{ vpX, vpY, vpW, vpH }; + SDL_SetRenderViewport(m_renderer, &vp); - // Set viewport to fill the entire window - setViewport(0, 0, m_windowWidth, m_windowHeight); + // Cache logical viewport and scale for callers + m_logicalVP = vp; + m_logicalScale = scale; } diff --git a/src/graphics/RenderManager.h b/src/graphics/RenderManager.h index 6229211..a2c192e 100644 --- a/src/graphics/RenderManager.h +++ b/src/graphics/RenderManager.h @@ -31,6 +31,10 @@ public: void setScale(float scaleX, float scaleY); void resetViewport(); + // Query the computed logical viewport and scale (useful for consistent input mapping) + SDL_Rect getLogicalViewport() const { return m_logicalVP; } + float getLogicalScale() const { return m_logicalScale; } + // Basic rendering operations void clear(Uint8 r = 0, Uint8 g = 0, Uint8 b = 0, Uint8 a = 255); @@ -75,7 +79,14 @@ private: // State bool m_initialized = false; bool m_isFullscreen = false; + // Cached logical viewport and scale (centered within window) + SDL_Rect m_logicalVP{0,0,0,0}; + float m_logicalScale = 1.0f; // Helper methods void updateLogicalScale(); + + // Query the computed logical viewport and scale (useful for consistent input mapping) + SDL_Rect getLogicalViewport() const { return m_logicalVP; } + float getLogicalScale() const { return m_logicalScale; } }; diff --git a/src/main_new.cpp b/src/main_new.cpp index 0eca828..7c03b7f 100644 --- a/src/main_new.cpp +++ b/src/main_new.cpp @@ -1,23 +1,35 @@ -// main.cpp - Simplified application entry point -// Delegates all application logic to ApplicationManager +// 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/ApplicationManager.h" #include int main(int argc, char* argv[]) { - // Create application manager - ApplicationManager app; + // 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"); - // Initialize the application + ApplicationManager app; if (!app.initialize(argc, argv)) { std::cerr << "Failed to initialize application" << std::endl; return 1; } - // Run the main application loop - app.run(); - - // Application manager destructor will handle cleanup + 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; } diff --git a/src/states/LevelSelectorState.cpp b/src/states/LevelSelectorState.cpp index ddd3036..8a7e866 100644 --- a/src/states/LevelSelectorState.cpp +++ b/src/states/LevelSelectorState.cpp @@ -187,15 +187,10 @@ void LevelSelectorState::handleEvent(const SDL_Event& e) { if (ctx.startLevelSelection) *ctx.startLevelSelection = hoveredLevel; } else if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN) { if (e.button.button == SDL_BUTTON_LEFT) { - // compute visible logical viewport - float vw = (lastLogicalScale > 0.f) ? float(lastLogicalVP.w) / lastLogicalScale : float(LOGICAL_W); - float vh = (lastLogicalScale > 0.f) ? float(lastLogicalVP.h) / lastLogicalScale : float(LOGICAL_H); - float offX = 0.f; - if (lastLogicalScale > 0.f) offX = (vw / 2.f) - (float(LOGICAL_W) / 2.f); - // convert mouse to logical coords + // convert mouse to logical coords (viewport is already centered) float lx = (float(e.button.x) - float(lastLogicalVP.x)) / (lastLogicalScale > 0.f ? lastLogicalScale : 1.f); float ly = (float(e.button.y) - float(lastLogicalVP.y)) / (lastLogicalScale > 0.f ? lastLogicalScale : 1.f); - SDL_FRect panel = DrawPanel(nullptr, vw, vh, /*draw=*/false, offX, 0.f); + SDL_FRect panel = DrawPanel(nullptr, LOGICAL_W, LOGICAL_H, /*draw=*/false, 0.f, 0.f); Grid g = MakeGrid(panel); int hit = HitTest(g, int(lx), int(ly)); if (hit != -1) { @@ -205,14 +200,10 @@ void LevelSelectorState::handleEvent(const SDL_Event& e) { } } } else if (e.type == SDL_EVENT_MOUSE_MOTION) { - // compute visible logical viewport and convert mouse coords once - float vw = (lastLogicalScale > 0.f) ? float(lastLogicalVP.w) / lastLogicalScale : float(LOGICAL_W); - float vh = (lastLogicalScale > 0.f) ? float(lastLogicalVP.h) / lastLogicalScale : float(LOGICAL_H); - float offX = 0.f; - if (lastLogicalScale > 0.f) offX = (vw / 2.f) - (float(LOGICAL_W) / 2.f); + // convert mouse to logical coords (viewport is already centered) float lx = (float(e.motion.x) - float(lastLogicalVP.x)) / (lastLogicalScale > 0.f ? lastLogicalScale : 1.f); float ly = (float(e.motion.y) - float(lastLogicalVP.y)) / (lastLogicalScale > 0.f ? lastLogicalScale : 1.f); - SDL_FRect panel = DrawPanel(nullptr, vw, vh, /*draw=*/false, offX, 0.f); + SDL_FRect panel = DrawPanel(nullptr, LOGICAL_W, LOGICAL_H, /*draw=*/false, 0.f, 0.f); Grid g = MakeGrid(panel); hoveredLevel = HitTest(g, int(lx), int(ly)); } @@ -233,18 +224,11 @@ void LevelSelectorState::render(SDL_Renderer* renderer, float logicalScale, SDL_ void LevelSelectorState::drawLevelSelectionPopup(SDL_Renderer* renderer) { if (!renderer) return; - // Important: main sets viewport + logical scale before calling us. - // Draw in logical coordinates to avoid artifacts in fullscreen. - // Use the actual visible logical size (viewport in logical coords) instead of hardcoded constants - float vw = (lastLogicalScale > 0.f) ? float(lastLogicalVP.w) / lastLogicalScale : float(LOGICAL_W); - float vh = (lastLogicalScale > 0.f) ? float(lastLogicalVP.h) / lastLogicalScale : float(LOGICAL_H); - - // compute horizontal offset so content centers within the visible logical viewport - float offX = 0.f; - if (lastLogicalScale > 0.f) { - float visibleLogicalW = float(lastLogicalVP.w) / lastLogicalScale; - offX = (visibleLogicalW / 2.f) - (vw / 2.f); - } + // Since ApplicationManager sets up a centered viewport, we draw directly in logical coordinates + // The viewport (LOGICAL_W x LOGICAL_H) is already centered within the window + float vw = float(LOGICAL_W); + float vh = float(LOGICAL_H); + float offX = 0.f; // No offset needed since viewport is centered // Panel and title strip (in logical space) SDL_FRect panel = DrawPanel(renderer, vw, vh-140.0f, /*draw=*/true, offX, 0.f); @@ -268,19 +252,14 @@ void LevelSelectorState::drawLevelSelectionPopup(SDL_Renderer* renderer) { } bool LevelSelectorState::isMouseInPopup(float mouseX, float mouseY, float& popupX, float& popupY, float& popupW, float& popupH) { - // Re-implement using new panel geometry in window coordinates - // Convert from window pixels to logical coords using cached viewport/scale + // Simplified: viewport is already centered, just convert mouse to logical coords (void)mouseX; (void)mouseY; float lx = 0.f, ly = 0.f; if (lastLogicalScale > 0.0f) { lx = (float(mouseX) - float(lastLogicalVP.x)) / lastLogicalScale; ly = (float(mouseY) - float(lastLogicalVP.y)) / lastLogicalScale; } - float vw = (lastLogicalScale > 0.f) ? float(lastLogicalVP.w) / lastLogicalScale : float(LOGICAL_W); - float vh = (lastLogicalScale > 0.f) ? float(lastLogicalVP.h) / lastLogicalScale : float(LOGICAL_H); - float offX = 0.f; - if (lastLogicalScale > 0.f) offX = (vw / 2.f) - (float(LOGICAL_W) / 2.f); - SDL_FRect p = DrawPanel(nullptr, vw, vh, /*draw=*/false, offX, 0.f); + SDL_FRect p = DrawPanel(nullptr, LOGICAL_W, LOGICAL_H, /*draw=*/false, 0.f, 0.f); popupX = p.x; popupY = p.y; popupW = p.w; popupH = p.h; return lx >= popupX && lx <= popupX + popupW && ly >= popupY && ly <= popupY + popupH; } @@ -292,11 +271,7 @@ int LevelSelectorState::getLevelFromMouse(float mouseX, float mouseY, float popu lx = (float(mouseX) - float(lastLogicalVP.x)) / lastLogicalScale; ly = (float(mouseY) - float(lastLogicalVP.y)) / lastLogicalScale; } - float vw = (lastLogicalScale > 0.f) ? float(lastLogicalVP.w) / lastLogicalScale : float(LOGICAL_W); - float vh = (lastLogicalScale > 0.f) ? float(lastLogicalVP.h) / lastLogicalScale : float(LOGICAL_H); - float offX = 0.f; - if (lastLogicalScale > 0.f) offX = (vw / 2.f) - (float(LOGICAL_W) / 2.f); - SDL_FRect p = DrawPanel(nullptr, vw, vh, /*draw=*/false, offX, 0.f); + SDL_FRect p = DrawPanel(nullptr, LOGICAL_W, LOGICAL_H, /*draw=*/false, 0.f, 0.f); Grid g = MakeGrid(p); return HitTest(g, (int)lx, (int)ly); } diff --git a/src/states/MenuState.cpp b/src/states/MenuState.cpp index c84ca45..b23a233 100644 --- a/src/states/MenuState.cpp +++ b/src/states/MenuState.cpp @@ -3,6 +3,7 @@ #include "persistence/Scores.h" #include "graphics/Font.h" #include "../core/GlobalState.h" +#include "../audio/Audio.h" #include #include #include @@ -42,14 +43,10 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi { 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); - 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; + // Since ApplicationManager sets up a centered viewport, we draw directly in logical coordinates + // No additional content offset is needed - the viewport itself handles centering + float contentOffsetX = 0.0f; + float contentOffsetY = 0.0f; // Background is drawn by main (stretched to the full window) to avoid double-draw. @@ -119,16 +116,17 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi useFont->draw(renderer, colX[4], scoresStartY - 28, "LEVEL", 1.1f, SDL_Color{200,200,220,255}); useFont->draw(renderer, colX[5], scoresStartY - 28, "TIME", 1.1f, SDL_Color{200,200,220,255}); } + + // Center columns around mid X, wider + float cx = LOGICAL_W * 0.5f + contentOffsetX; + float colX[] = { cx - 280, cx - 180, cx - 20, cx + 90, cx + 200, cx + 300 }; + for (size_t i = 0; i < maxDisplay; ++i) { float baseY = scoresStartY + i * 25; 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 - float cx = LOGICAL_W * 0.5f + contentOffsetX; - float colX[] = { cx - 280, cx - 180, cx - 20, cx + 90, cx + 200, cx + 300 }; - char rankStr[8]; std::snprintf(rankStr, sizeof(rankStr), "%zu.", i + 1); if (useFont) useFont->draw(renderer, colX[0], y, rankStr, 1.0f, SDL_Color{220, 220, 230, 255}); @@ -150,6 +148,8 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi } // Draw bottom action buttons with responsive sizing (reduced to match main mouse hit-test) + // Since we removed content offsets, calculate contentW directly from the scale and logical size + float contentW = LOGICAL_W * logicalScale; bool isSmall = (contentW < 700.0f); float btnW = isSmall ? (LOGICAL_W * 0.4f) : 300.0f; float btnH = isSmall ? 60.0f : 70.0f; diff --git a/src/states/PlayingState.cpp b/src/states/PlayingState.cpp index 571e00c..05e70ee 100644 --- a/src/states/PlayingState.cpp +++ b/src/states/PlayingState.cpp @@ -3,12 +3,18 @@ #include "gameplay/Game.h" #include "gameplay/LineEffect.h" #include "persistence/Scores.h" +#include "../audio/Audio.h" #include PlayingState::PlayingState(StateContext& ctx) : State(ctx) {} void PlayingState::onEnter() { - // Nothing yet; main still owns game creation + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[PLAYING] Entering Playing state"); + // Initialize the game with the selected starting level + if (ctx.game && ctx.startLevelSelection) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[PLAYING] Resetting game with level %d", *ctx.startLevelSelection); + ctx.game->reset(*ctx.startLevelSelection); + } } void PlayingState::onExit() { @@ -54,12 +60,40 @@ void PlayingState::handleEvent(const SDL_Event& e) { return; } - // Other gameplay keys already registered by main's Playing handler for now + // Tetris controls (only when not paused) + if (!ctx.game->isPaused()) { + // Rotation (still event-based for precise timing) + if (e.key.scancode == SDL_SCANCODE_UP || e.key.scancode == SDL_SCANCODE_W || + e.key.scancode == SDL_SCANCODE_Z) { + ctx.game->rotate(1); // Clockwise rotation + return; + } + if (e.key.scancode == SDL_SCANCODE_X) { + ctx.game->rotate(-1); // Counter-clockwise rotation + return; + } + + // Hard drop (space) + if (e.key.scancode == SDL_SCANCODE_SPACE) { + ctx.game->hardDrop(); + return; + } + } } + + // Note: Left/Right movement and soft drop are now handled by + // ApplicationManager's update handler for proper DAS/ARR timing } void PlayingState::update(double frameMs) { if (!ctx.game) return; + + static bool debugPrinted = false; + if (!debugPrinted) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[PLAYING] Starting updates, frameMs=%.2f, paused=%d", frameMs, ctx.game->isPaused()); + debugPrinted = true; + } + // forward per-frame gameplay updates (gravity, elapsed) if (!ctx.game->isPaused()) { ctx.game->tickGravity(frameMs); @@ -71,11 +105,8 @@ void PlayingState::update(double frameMs) { } } } - - if (ctx.game->isGameOver()) { - if (ctx.scores) ctx.scores->submit(ctx.game->score(), ctx.game->lines(), ctx.game->level(), ctx.game->elapsed()); - // Transitioning state must be done by the owner (main via StateManager hooks). We can't set state here. - } + + // Note: Game over detection and state transition is now handled by ApplicationManager } void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) { diff --git a/src/states/State.h b/src/states/State.h index e1eaf3b..610579a 100644 --- a/src/states/State.h +++ b/src/states/State.h @@ -40,6 +40,8 @@ struct StateContext { // Audio / SFX - forward declared types in main // Pointers to booleans/flags used by multiple states bool* musicEnabled = nullptr; + bool* musicStarted = nullptr; + bool* musicLoaded = nullptr; int* startLevelSelection = nullptr; int* hoveredButton = nullptr; // Menu popups (exposed from main)