some problems fixed

This commit is contained in:
2025-08-17 21:13:58 +02:00
parent d75bfcf4d0
commit b5ef9172b3
18 changed files with 1139 additions and 231 deletions

View File

@ -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 <SDL3/SDL.h>
@ -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);
}
// Dont 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<LineEffect>();
if (m_renderManager && m_renderManager->getSDLRenderer()) {
m_lineEffect->init(m_renderManager->getSDLRenderer());
}
m_game = std::make_unique<Game>(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<float>(winW) / LOGICAL_W;
float scaleY = static_cast<float>(winH) / LOGICAL_H;
float logicalScale = std::min(scaleX, scaleY);
int vpW = static_cast<int>(LOGICAL_W * logicalScale);
int vpH = static_cast<int>(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<float>(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 (dont 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<float>(Config::Logical::WIDTH);
const float LOGICAL_H = static_cast<float>(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; // dont 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<float>(Config::Logical::WIDTH);
const float LOGICAL_H = static_cast<float>(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<float>(winW),
static_cast<float>(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<float>(Config::Window::DEFAULT_WIDTH);
const float LOGICAL_H = static_cast<float>(Config::Window::DEFAULT_HEIGHT);
float scaleX = static_cast<float>(winW) / LOGICAL_W;
float scaleY = static_cast<float>(winH) / LOGICAL_H;
float logicalScale = std::min(scaleX, scaleY);
int scaledW = static_cast<int>(LOGICAL_W * logicalScale);
int scaledH = static_cast<int>(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();

View File

@ -96,6 +96,12 @@ private:
std::unique_ptr<Game> m_game;
std::unique_ptr<LineEffect> 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
};

View File

@ -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);