Files
spacetris/src/core/application/ApplicationManager.cpp

1366 lines
64 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "ApplicationManager.h"
#include "../state/StateManager.h"
#include "../input/InputManager.h"
#include "../interfaces/IAudioSystem.h"
#include "../interfaces/IRenderer.h"
#include "../interfaces/IAssetLoader.h"
#include "../interfaces/IInputHandler.h"
#include <filesystem>
#include "../../audio/Audio.h"
#include "../../audio/SoundEffect.h"
#include "../../persistence/Scores.h"
#include "../../states/State.h"
#include "../../states/LoadingState.h"
#include "../../states/MenuState.h"
#include "../../states/LevelSelectorState.h"
#include "../../states/PlayingState.h"
#include "../assets/AssetManager.h"
#include "../Config.h"
#include "../GlobalState.h"
#include "../../graphics/renderers/RenderManager.h"
#include "../../graphics/ui/Font.h"
#include "../../graphics/effects/Starfield3D.h"
#include "../../graphics/effects/Starfield.h"
#include "../../graphics/renderers/GameRenderer.h"
#include "../../gameplay/core/Game.h"
#include "../../gameplay/effects/LineEffect.h"
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <iostream>
#include <cmath>
#include <fstream>
#include <algorithm>
ApplicationManager::ApplicationManager() = default;
static void traceFile(const char* msg) {
std::ofstream f("tetris_trace.log", std::ios::app);
if (f) f << msg << "\n";
}
// Helper: extracted from inline lambda to avoid MSVC parsing issues with complex lambdas
void ApplicationManager::renderLoading(ApplicationManager* app, RenderManager& renderer) {
// Clear background first
renderer.clear(0, 0, 0, 255);
// Use 3D starfield for loading screen (full screen)
if (app->m_starfield3D) {
int winW_actual = 0, winH_actual = 0;
if (app->m_renderManager) app->m_renderManager->getWindowSize(winW_actual, winH_actual);
if (winW_actual > 0 && winH_actual > 0) app->m_starfield3D->resize(winW_actual, winH_actual);
app->m_starfield3D->draw(renderer.getSDLRenderer());
}
SDL_Rect logicalVP = {0,0,0,0};
float logicalScale = 1.0f;
if (app->m_renderManager) {
logicalVP = app->m_renderManager->getLogicalViewport();
logicalScale = app->m_renderManager->getLogicalScale();
}
SDL_SetRenderViewport(renderer.getSDLRenderer(), &logicalVP);
SDL_SetRenderScale(renderer.getSDLRenderer(), logicalScale, logicalScale);
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);
SDL_FRect fr;
fr.x = x + contentOffsetX;
fr.y = y + contentOffsetY;
fr.w = w;
fr.h = h;
SDL_RenderFillRect(renderer.getSDLRenderer(), &fr);
};
// Compute dynamic logical width/height based on the RenderManager's
// computed viewport and scale so the loading UI sizes itself to the
// actual content area rather than a hardcoded design size.
float LOGICAL_W = static_cast<float>(Config::Logical::WIDTH);
float LOGICAL_H = static_cast<float>(Config::Logical::HEIGHT);
if (logicalScale > 0.0f && logicalVP.w > 0 && logicalVP.h > 0) {
// logicalVP is in window pixels; divide by scale to get logical units
LOGICAL_W = static_cast<float>(logicalVP.w) / logicalScale;
LOGICAL_H = static_cast<float>(logicalVP.h) / logicalScale;
}
const bool isLimitedHeight = LOGICAL_H < 450.0f;
SDL_Texture* logoTex = app->m_assetManager->getTexture("logo");
const float logoHeight = logoTex ? (isLimitedHeight ? LOGICAL_H * 0.25f : LOGICAL_H * 0.4f) : 0;
const float loadingTextHeight = 20;
const float barHeight = 20;
const float barPaddingVertical = isLimitedHeight ? 15 : 35;
const float percentTextHeight = 24;
const float spacingBetweenElements = isLimitedHeight ? 5 : 15;
const float totalContentHeight = logoHeight + (logoHeight > 0 ? spacingBetweenElements : 0) + loadingTextHeight + barPaddingVertical + barHeight + spacingBetweenElements + percentTextHeight;
float currentY = (LOGICAL_H - totalContentHeight) / 2.0f;
if (logoTex) {
const int lw = 872, lh = 273;
const float maxLogoWidth = std::min(LOGICAL_W * 0.9f, 600.0f);
const float availableHeight = isLimitedHeight ? LOGICAL_H * 0.25f : LOGICAL_H * 0.4f;
const float availableWidth = maxLogoWidth;
const float scaleFactorWidth = availableWidth / static_cast<float>(lw);
const float scaleFactorHeight = availableHeight / static_cast<float>(lh);
const float scaleFactor = std::min(scaleFactorWidth, scaleFactorHeight);
const float displayWidth = lw * scaleFactor;
const float displayHeight = lh * scaleFactor;
const float logoX = (LOGICAL_W - displayWidth) / 2.0f;
SDL_FRect dst{logoX + contentOffsetX, currentY + contentOffsetY, displayWidth, displayHeight};
SDL_RenderTexture(renderer.getSDLRenderer(), logoTex, nullptr, &dst);
currentY += displayHeight + spacingBetweenElements;
}
FontAtlas* pixelFont = (FontAtlas*)app->m_assetManager->getFont("pixel_font");
FontAtlas* fallbackFont = (FontAtlas*)app->m_assetManager->getFont("main_font");
FontAtlas* loadingFont = pixelFont ? pixelFont : fallbackFont;
if (loadingFont) {
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;
loadingFont->draw(renderer.getSDLRenderer(), textX + contentOffsetX, currentY + contentOffsetY, loadingText, 1.0f, {255,204,0,255});
}
currentY += loadingTextHeight + barPaddingVertical;
const int barW = 400, barH = 20;
const int bx = (LOGICAL_W - barW) / 2;
float loadingProgress = app->m_assetManager->getLoadingProgress();
drawRectOriginal(bx - 3, currentY - 3, barW + 6, barH + 6, {68,68,80,255});
drawRectOriginal(bx, currentY, barW, barH, {34,34,34,255});
drawRectOriginal(bx, currentY, int(barW * loadingProgress), barH, {255,204,0,255});
currentY += barH + spacingBetweenElements;
if (loadingFont) {
int percentage = int(loadingProgress * 100);
char percentText[16];
std::snprintf(percentText, sizeof(percentText), "%d%%", percentage);
std::string pStr(percentText);
int pW=0, pH=0; loadingFont->measure(pStr, 1.5f, pW, pH);
float percentX = (LOGICAL_W - (float)pW) * 0.5f;
loadingFont->draw(renderer.getSDLRenderer(), percentX + contentOffsetX, currentY + contentOffsetY, pStr, 1.5f, {255,204,0,255});
}
SDL_SetRenderViewport(renderer.getSDLRenderer(), nullptr);
SDL_SetRenderScale(renderer.getSDLRenderer(), 1.0f, 1.0f);
}
ApplicationManager::~ApplicationManager() {
if (m_initialized) {
shutdown();
}
}
bool ApplicationManager::initialize(int argc, char* argv[]) {
if (m_initialized) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "ApplicationManager already initialized");
return true;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Initializing ApplicationManager...");
// Initialize GlobalState
GlobalState::instance().initialize();
// Set initial logical dimensions
GlobalState::instance().updateLogicalDimensions(m_windowWidth, m_windowHeight);
// Initialize SDL first
if (!initializeSDL()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize SDL");
return false;
}
// Initialize managers
if (!initializeManagers()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize managers");
cleanupSDL();
return false;
}
// Register services for dependency injection
registerServices();
// Initialize game systems
if (!initializeGame()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize game systems");
cleanupManagers();
cleanupSDL();
return false;
}
m_initialized = true;
m_running = true;
m_lastFrameTime = SDL_GetTicks();
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "ApplicationManager initialized successfully");
return true;
}
void ApplicationManager::run() {
if (!m_initialized) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "ApplicationManager not initialized");
return;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Starting main application loop");
traceFile("Main loop starting");
while (m_running) {
SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Main loop iteration start: m_running=%d", m_running ? 1 : 0);
traceFile("Main loop iteration");
// Calculate delta time
uint64_t currentTime = SDL_GetTicks();
float deltaTime = (currentTime - m_lastFrameTime) / 1000.0f;
m_lastFrameTime = currentTime;
// Limit delta time to prevent spiral of death
if (deltaTime > Config::Performance::MIN_FRAME_TIME) {
deltaTime = Config::Performance::MIN_FRAME_TIME;
}
// Main loop phases
processEvents();
if (m_running) {
update(deltaTime);
traceFile("about to call render");
render();
}
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Main application loop ended");
}
void ApplicationManager::shutdown() {
if (!m_initialized) {
return;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Shutting down ApplicationManager...");
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();
// Shutdown GlobalState last
GlobalState::instance().shutdown();
m_initialized = false;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "ApplicationManager shutdown complete");
}
bool ApplicationManager::initializeSDL() {
// Initialize SDL subsystems
int sdlResult = SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
if (sdlResult < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_Init failed: %s", SDL_GetError());
return false;
}
// Initialize SDL_ttf
int ttfResult = TTF_Init();
if (ttfResult < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "TTF_Init failed: %s", SDL_GetError());
SDL_Quit();
return false;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "SDL initialized successfully");
return true;
}
bool ApplicationManager::initializeManagers() {
// Create and initialize RenderManager
m_renderManager = std::make_unique<RenderManager>();
if (!m_renderManager->initialize(m_windowWidth, m_windowHeight, m_windowTitle)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize RenderManager");
return false;
}
// Create InputManager
m_inputManager = std::make_unique<InputManager>();
if (!m_inputManager) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create InputManager");
return false;
}
// Create and initialize AssetManager
m_assetManager = std::make_unique<AssetManager>();
if (!m_assetManager->initialize(m_renderManager->getSDLRenderer())) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize AssetManager");
return false;
}
// Ensure SoundEffectManager is initialized early so SFX loads work
SoundEffectManager::instance().init();
// Create StateManager (will be enhanced in next steps)
m_stateManager = std::make_unique<StateManager>(AppState::Loading);
// Create and initialize starfields
m_starfield3D = std::make_unique<Starfield3D>();
m_starfield3D->init(Config::Logical::WIDTH, Config::Logical::HEIGHT, 200);
m_starfield = std::make_unique<Starfield>();
m_starfield->init(Config::Logical::WIDTH, Config::Logical::HEIGHT, 50);
// Register InputManager handlers to forward events to StateManager so
// state-specific event handlers receive SDL_Event objects just like main.cpp.
if (m_inputManager && m_stateManager) {
m_inputManager->registerKeyHandler([this](SDL_Scancode sc, bool pressed){
if (!m_stateManager) return;
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")
// 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){
if (!m_stateManager) return;
SDL_Event ev{};
ev.type = pressed ? SDL_EVENT_MOUSE_BUTTON_DOWN : SDL_EVENT_MOUSE_BUTTON_UP;
ev.button.button = button;
ev.button.x = int(x);
ev.button.y = int(y);
m_stateManager->handleEvent(ev);
});
m_inputManager->registerMouseMotionHandler([this](float x, float y, float dx, float dy){
if (!m_stateManager) return;
SDL_Event ev{};
ev.type = SDL_EVENT_MOUSE_MOTION;
ev.motion.x = int(x);
ev.motion.y = int(y);
ev.motion.xrel = int(dx);
ev.motion.yrel = int(dy);
m_stateManager->handleEvent(ev);
});
m_inputManager->registerWindowEventHandler([this](const SDL_WindowEvent& we){
// Handle window resize events for RenderManager
if (we.type == SDL_EVENT_WINDOW_RESIZED && m_renderManager) {
m_renderManager->handleWindowResize(we.data1, we.data2);
// Update GlobalState logical dimensions when window resizes
GlobalState::instance().updateLogicalDimensions(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
ev.window = we;
m_stateManager->handleEvent(ev);
});
m_inputManager->registerQuitHandler([this](){
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[QUIT] InputManager quit handler invoked - setting running=false");
m_running = false;
});
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Managers initialized successfully");
return true;
}
void ApplicationManager::registerServices() {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Registering services for dependency injection...");
// Register concrete implementations as interface singletons
if (m_renderManager) {
std::shared_ptr<RenderManager> renderPtr(m_renderManager.get(), [](RenderManager*) {
// Custom deleter that does nothing since the unique_ptr manages lifetime
});
m_serviceContainer.registerSingleton<IRenderer>(renderPtr);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Registered IRenderer service");
}
if (m_assetManager) {
std::shared_ptr<AssetManager> assetPtr(m_assetManager.get(), [](AssetManager*) {
// Custom deleter that does nothing since the unique_ptr manages lifetime
});
m_serviceContainer.registerSingleton<IAssetLoader>(assetPtr);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Registered IAssetLoader service");
}
if (m_inputManager) {
std::shared_ptr<InputManager> inputPtr(m_inputManager.get(), [](InputManager*) {
// Custom deleter that does nothing since the unique_ptr manages lifetime
});
m_serviceContainer.registerSingleton<IInputHandler>(inputPtr);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Registered IInputHandler service");
}
// Register Audio system singleton
auto& audioInstance = Audio::instance();
auto audioPtr = std::shared_ptr<Audio>(&audioInstance, [](Audio*) {
// Custom deleter that does nothing since Audio is a singleton
});
m_serviceContainer.registerSingleton<IAudioSystem>(audioPtr);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Registered IAudioSystem service");
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Service registration completed successfully");
}
bool ApplicationManager::initializeGame() {
// Load essential assets using AssetManager
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loading essential assets...");
// Set up asset loading tasks
AssetManager::LoadingTask logoTask{AssetManager::LoadingTask::TEXTURE, "logo", Config::Assets::LOGO_BMP};
AssetManager::LoadingTask backgroundTask{AssetManager::LoadingTask::TEXTURE, "background", Config::Assets::BACKGROUND_BMP};
AssetManager::LoadingTask blocksTask{AssetManager::LoadingTask::TEXTURE, "blocks", Config::Assets::BLOCKS_BMP};
AssetManager::LoadingTask fontTask{AssetManager::LoadingTask::FONT, "main_font", Config::Fonts::DEFAULT_FONT_PATH, Config::Fonts::DEFAULT_FONT_SIZE};
AssetManager::LoadingTask pixelFontTask{AssetManager::LoadingTask::FONT, "pixel_font", Config::Fonts::PIXEL_FONT_PATH, Config::Fonts::PIXEL_FONT_SIZE};
// Pre-load the pixel (retro) font synchronously so the loading screen can render text immediately
if (!m_assetManager->getFont("pixel_font")) {
if (m_assetManager->loadFont("pixel_font", Config::Fonts::PIXEL_FONT_PATH, Config::Fonts::PIXEL_FONT_SIZE)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Preloaded pixel_font for loading screen");
} else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Failed to preload pixel_font; loading screen will fallback to main_font");
}
}
// Add tasks to AssetManager (pixel font task will be skipped if already loaded)
m_assetManager->addLoadingTask(logoTask);
m_assetManager->addLoadingTask(backgroundTask);
m_assetManager->addLoadingTask(blocksTask);
m_assetManager->addLoadingTask(fontTask);
m_assetManager->addLoadingTask(pixelFontTask);
// Execute loading tasks with progress callback
m_assetManager->executeLoadingTasks([](float progress) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Asset loading progress: %.1f%%", progress * 100.0f);
});
// 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();
// Create and populate shared StateContext similar to main.cpp so states like MenuState
// receive the same pointers and flags they expect.
// Create ScoreManager and load scores
m_scoreManager = std::make_unique<ScoreManager>();
if (m_scoreManager) m_scoreManager->load();
// 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) {
m_game->setSoundCallback([&](int linesCleared){
SoundEffectManager::instance().playSound("clear_line", 1.0f);
// voice lines handled via asset manager loaded sounds
if (linesCleared == 2) SoundEffectManager::instance().playRandomSound({"nice_combo"}, 1.0f);
else if (linesCleared == 3) SoundEffectManager::instance().playRandomSound({"great_move"}, 1.0f);
else if (linesCleared == 4) SoundEffectManager::instance().playRandomSound({"amazing"}, 1.0f);
});
m_game->setLevelUpCallback([&](int newLevel){
SoundEffectManager::instance().playSound("lets_go", 1.0f);
});
}
// Prepare a StateContext-like struct by setting up handlers that capture
// pointers and flags. State objects in this refactor expect these to be
// available via StateManager event/update/render hooks, so we'll store them
// as lambdas that reference members here.
// Start background music loading similar to main.cpp: Audio init + file discovery
Audio::instance().init();
// Discover available tracks (up to 100) and queue for background loading
m_totalTracks = 0;
for (int i = 1; i <= 100; ++i) {
char buf[128];
std::snprintf(buf, sizeof(buf), "assets/music/music%03d.mp3", i);
// Use simple file existence check via std::filesystem
if (std::filesystem::exists(buf)) {
Audio::instance().addTrackAsync(buf);
++m_totalTracks;
} else {
break;
}
}
if (m_totalTracks > 0) {
Audio::instance().startBackgroundLoading();
// 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
}
// Instantiate state objects and populate a StateContext similar to main.cpp
// so that existing state classes (MenuState, LoadingState, etc.) receive
// the resources they expect.
{
m_stateContext.stateManager = m_stateManager.get();
m_stateContext.game = m_game.get();
m_stateContext.scores = m_scoreManager.get();
m_stateContext.starfield = m_starfield.get();
m_stateContext.starfield3D = m_starfield3D.get();
m_stateContext.font = (FontAtlas*)m_assetManager->getFont("main_font");
m_stateContext.pixelFont = (FontAtlas*)m_assetManager->getFont("pixel_font");
m_stateContext.lineEffect = m_lineEffect.get();
m_stateContext.logoTex = m_assetManager->getTexture("logo");
// Attempt to load a small logo variant if present to match original UX
SDL_Texture* logoSmall = m_assetManager->getTexture("logo_small");
if (!logoSmall) {
// Try to load image from disk and register with AssetManager
if (m_assetManager->loadTexture("logo_small", "assets/images/logo_small.bmp")) {
logoSmall = m_assetManager->getTexture("logo_small");
}
}
m_stateContext.logoSmallTex = logoSmall;
if (logoSmall) {
int w = 0, h = 0; if (m_renderManager) m_renderManager->getTextureSize(logoSmall, w, h);
m_stateContext.logoSmallW = w; m_stateContext.logoSmallH = h;
} else { m_stateContext.logoSmallW = 0; m_stateContext.logoSmallH = 0; }
m_stateContext.backgroundTex = m_assetManager->getTexture("background");
m_stateContext.blocksTex = m_assetManager->getTexture("blocks");
m_stateContext.musicEnabled = &m_musicEnabled;
m_stateContext.musicStarted = &m_musicStarted;
m_stateContext.musicLoaded = &m_musicLoaded;
m_stateContext.startLevelSelection = &m_startLevelSelection;
m_stateContext.hoveredButton = &m_hoveredButton;
m_stateContext.showSettingsPopup = &m_showSettingsPopup;
m_stateContext.showExitConfirmPopup = &m_showExitConfirmPopup;
m_stateContext.exitPopupSelectedButton = &m_exitPopupSelectedButton;
// Create state instances
m_loadingState = std::make_unique<LoadingState>(m_stateContext);
m_menuState = std::make_unique<MenuState>(m_stateContext);
m_levelSelectorState = std::make_unique<LevelSelectorState>(m_stateContext);
m_playingState = std::make_unique<PlayingState>(m_stateContext);
// Register handlers that forward to these state objects
if (m_stateManager) {
m_stateManager->registerEventHandler(AppState::Loading, [this](const SDL_Event& e){ if (m_loadingState) m_loadingState->handleEvent(e); });
m_stateManager->registerOnEnter(AppState::Loading, [this](){ if (m_loadingState) m_loadingState->onEnter(); });
m_stateManager->registerOnExit(AppState::Loading, [this](){ if (m_loadingState) m_loadingState->onExit(); });
m_stateManager->registerEventHandler(AppState::Menu, [this](const SDL_Event& e){ if (m_menuState) m_menuState->handleEvent(e); });
m_stateManager->registerOnEnter(AppState::Menu, [this](){ if (m_menuState) m_menuState->onEnter(); });
m_stateManager->registerOnExit(AppState::Menu, [this](){ if (m_menuState) m_menuState->onExit(); });
m_stateManager->registerEventHandler(AppState::LevelSelector, [this](const SDL_Event& e){ if (m_levelSelectorState) m_levelSelectorState->handleEvent(e); });
m_stateManager->registerOnEnter(AppState::LevelSelector, [this](){ if (m_levelSelectorState) m_levelSelectorState->onEnter(); });
m_stateManager->registerOnExit(AppState::LevelSelector, [this](){ if (m_levelSelectorState) m_levelSelectorState->onExit(); });
m_stateManager->registerEventHandler(AppState::Playing, [this](const SDL_Event& e){ if (m_playingState) m_playingState->handleEvent(e); });
m_stateManager->registerOnEnter(AppState::Playing, [this](){ if (m_playingState) m_playingState->onEnter(); });
m_stateManager->registerOnExit(AppState::Playing, [this](){ if (m_playingState) m_playingState->onExit(); });
}
}
// Finally call setupStateHandlers for inline visuals and additional hooks
setupStateHandlers();
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Game systems initialized with asset loading");
return true;
}
void ApplicationManager::setupStateHandlers() {
// Helper function for drawing rectangles
auto drawRect = [](SDL_Renderer* renderer, float x, float y, float w, float h, uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
SDL_SetRenderDrawColor(renderer, r, g, b, a);
SDL_FRect rect = { x, y, w, h };
SDL_RenderFillRect(renderer, &rect);
};
// Helper function for drawing menu buttons with enhanced styling
auto drawEnhancedButton = [drawRect](SDL_Renderer* renderer, FontAtlas* font, float cx, float cy, float w, float h,
const std::string& label, bool isHovered, bool isSelected) {
float x = cx - w/2;
float y = cy - h/2;
// Button styling based on state
SDL_Color bgColor = isSelected ? SDL_Color{100, 150, 255, 255} :
isHovered ? SDL_Color{80, 120, 200, 255} :
SDL_Color{60, 90, 160, 255};
// Draw border and background
drawRect(renderer, x-2, y-2, w+4, h+4, 60, 80, 140, 255); // Border
drawRect(renderer, x, y, w, h, bgColor.r, bgColor.g, bgColor.b, bgColor.a); // Background
// Draw text if font is available
if (font) {
float textScale = 1.8f;
float approxCharW = 12.0f * textScale;
float textW = label.length() * approxCharW;
float textX = x + (w - textW) / 2.0f;
float textY = y + (h - 20.0f * textScale) / 2.0f;
// Draw shadow
font->draw(renderer, textX + 2, textY + 2, label, textScale, {0, 0, 0, 180});
// Draw main text
font->draw(renderer, textX, textY, label, textScale, {255, 255, 255, 255});
}
};
// Loading State Handlers (matching original main.cpp implementation)
// Extracted to a helper to avoid complex inline lambda parsing issues on MSVC
auto loadingRenderForwarder = [this](RenderManager& renderer) {
// forward to helper defined below
renderLoading(this, renderer);
};
m_stateManager->registerRenderHandler(AppState::Loading, loadingRenderForwarder);
m_stateManager->registerUpdateHandler(AppState::Loading,
[this](float deltaTime) {
// Update 3D starfield so stars move during loading
if (m_starfield3D) {
// 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");
}
});
// Menu State render: draw background full-screen, then delegate to MenuState::render
m_stateManager->registerRenderHandler(AppState::Menu,
[this](RenderManager& renderer) {
// Clear and draw background to full window
renderer.clear(0, 0, 20, 255);
int winW = 0, winH = 0;
if (m_renderManager) m_renderManager->getWindowSize(winW, winH);
SDL_Texture* background = m_assetManager->getTexture("background");
if (background && winW > 0 && winH > 0) {
SDL_FRect bgRect = { 0, 0, (float)winW, (float)winH };
renderer.renderTexture(background, nullptr, &bgRect);
}
// 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) {
m_menuState->render(renderer.getSDLRenderer(), logicalScale, logicalVP);
}
// Reset to defaults
SDL_SetRenderViewport(renderer.getSDLRenderer(), nullptr);
SDL_SetRenderScale(renderer.getSDLRenderer(), 1.0f, 1.0f);
});
// LevelSelector State render: draw background full-screen, then delegate to LevelSelectorState::render
m_stateManager->registerRenderHandler(AppState::LevelSelector,
[this](RenderManager& renderer) {
// Clear and draw background to full window
renderer.clear(0, 0, 20, 255);
int winW = 0, winH = 0;
if (m_renderManager) m_renderManager->getWindowSize(winW, winH);
SDL_Texture* background = m_assetManager->getTexture("background");
if (background && winW > 0 && winH > 0) {
SDL_FRect bgRect = { 0, 0, (float)winW, (float)winH };
renderer.renderTexture(background, nullptr, &bgRect);
}
// 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) {
m_levelSelectorState->render(renderer.getSDLRenderer(), logicalScale, logicalVP);
}
// Reset to defaults
SDL_SetRenderViewport(renderer.getSDLRenderer(), nullptr);
SDL_SetRenderScale(renderer.getSDLRenderer(), 1.0f, 1.0f);
});
m_stateManager->registerUpdateHandler(AppState::Menu,
[this](float deltaTime) {
// Update logo animation counter
// 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 music as soon as at least one track has decoded (dont wait for all)
// 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();
Audio::instance().start();
m_musicStarted = true;
}
}
// Track completion status for UI
if (!m_musicLoaded && Audio::instance().isLoadingComplete()) {
m_musicLoaded = true;
}
});
m_stateManager->registerEventHandler(AppState::Menu,
[this](const SDL_Event& event) {
// Forward keyboard events (Enter/Escape) to trigger actions, match original main.cpp
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) {
if (event.key.scancode == SDL_SCANCODE_RETURN || event.key.scancode == SDL_SCANCODE_RETURN2 || event.key.scancode == SDL_SCANCODE_KP_ENTER) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Starting game from menu (Enter)");
// Reset start level and transition
// In the original main, game.reset(...) was called; here we only switch state.
m_stateManager->setState(AppState::Playing);
return;
}
if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
// If an exit confirmation is already showing, accept it and quit.
if (m_showExitConfirmPopup) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Quitting from menu (Escape confirmed)");
m_running = false;
return;
}
// Otherwise, show the exit confirmation popup instead of quitting immediately.
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Showing exit confirmation (Escape)");
m_showExitConfirmPopup = true;
return;
}
// S: toggle SFX enable state (music handled globally)
if (event.key.scancode == SDL_SCANCODE_S) {
SoundEffectManager::instance().setEnabled(!SoundEffectManager::instance().isEnabled());
}
}
// Mouse handling: map SDL mouse coords into logical content coords and
// perform hit-tests for menu buttons similar to main.cpp.
if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
float mx = (float)event.button.x;
float my = (float)event.button.y;
// 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;
float ly = (my - logicalVP.y) / logicalScale;
// Compute dynamic logical dimensions from viewport/scale
float dynW = (logicalScale > 0.f && logicalVP.w > 0) ? (float)logicalVP.w / logicalScale : (float)Config::Logical::WIDTH;
float dynH = (logicalScale > 0.f && logicalVP.h > 0) ? (float)logicalVP.h / logicalScale : (float)Config::Logical::HEIGHT;
// Respect settings popup
if (m_showSettingsPopup) {
m_showSettingsPopup = false;
} else {
bool isSmall = ((dynW * logicalScale) < 700.0f);
float btnW = isSmall ? (dynW * 0.4f) : 300.0f;
float btnH = isSmall ? 60.0f : 70.0f;
float btnCX = dynW * 0.5f;
const float btnYOffset = 40.0f;
float btnCY = dynH * 0.86f + btnYOffset;
SDL_FRect playBtn{btnCX - btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH};
SDL_FRect levelBtn{btnCX + btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH};
if (lx >= playBtn.x && lx <= playBtn.x + playBtn.w && ly >= playBtn.y && ly <= playBtn.y + playBtn.h) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Menu: Play button clicked");
m_stateManager->setState(AppState::Playing);
} else if (lx >= levelBtn.x && lx <= levelBtn.x + levelBtn.w && ly >= levelBtn.y && ly <= levelBtn.y + levelBtn.h) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Menu: Level button clicked");
m_stateManager->setState(AppState::LevelSelector);
} else {
// Settings area detection (top-right small area)
SDL_FRect settingsBtn{dynW - 60, 10, 50, 30};
if (lx >= settingsBtn.x && lx <= settingsBtn.x + settingsBtn.w && ly >= settingsBtn.y && ly <= settingsBtn.y + settingsBtn.h) {
m_showSettingsPopup = true;
}
}
}
}
}
// Mouse motion handling for hover
if (event.type == SDL_EVENT_MOUSE_MOTION) {
float mx = (float)event.motion.x;
float my = (float)event.motion.y;
// 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;
if (!m_showSettingsPopup) {
// Compute dynamic logical dimensions
float dynW = (logicalScale > 0.f && logicalVP.w > 0) ? (float)logicalVP.w / logicalScale : (float)Config::Logical::WIDTH;
float dynH = (logicalScale > 0.f && logicalVP.h > 0) ? (float)logicalVP.h / logicalScale : (float)Config::Logical::HEIGHT;
bool isSmall = ((dynW * logicalScale) < 700.0f);
float btnW = isSmall ? (dynW * 0.4f) : 300.0f;
float btnH = isSmall ? 60.0f : 70.0f;
float btnCX = dynW * 0.5f;
const float btnYOffset = 40.0f;
float btnCY = dynH * 0.86f + btnYOffset;
SDL_FRect playBtn{btnCX - btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH};
SDL_FRect levelBtn{btnCX + btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH};
m_hoveredButton = -1;
if (lx >= playBtn.x && lx <= playBtn.x + playBtn.w && ly >= playBtn.y && ly <= playBtn.y + playBtn.h) {
m_hoveredButton = 0;
} else if (lx >= levelBtn.x && lx <= levelBtn.x + levelBtn.w && ly >= levelBtn.y && ly <= levelBtn.y + levelBtn.h) {
m_hoveredButton = 1;
}
}
}
}
});
// 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) {
// 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
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<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,
m_exitPopupSelectedButton
);
// 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);
}
});
// Debug overlay: show current window and logical sizes on the right side of the screen
auto debugOverlay = [this](RenderManager& renderer) {
// Window size
int winW = 0, winH = 0;
renderer.getWindowSize(winW, winH);
// Logical viewport and scale
SDL_Rect logicalVP{0,0,0,0};
float logicalScale = 1.0f;
if (m_renderManager) {
logicalVP = m_renderManager->getLogicalViewport();
logicalScale = m_renderManager->getLogicalScale();
}
// Use dynamic logical dimensions from GlobalState
float LOGICAL_W = static_cast<float>(GlobalState::instance().getLogicalWidth());
float LOGICAL_H = static_cast<float>(GlobalState::instance().getLogicalHeight());
// Use logical viewport so overlay is aligned with game content
SDL_SetRenderViewport(renderer.getSDLRenderer(), &logicalVP);
SDL_SetRenderScale(renderer.getSDLRenderer(), logicalScale, logicalScale);
// Choose font (pixel first, fallback to main)
FontAtlas* pixelFont = (FontAtlas*)(m_assetManager ? m_assetManager->getFont("pixel_font") : nullptr);
FontAtlas* mainFont = (FontAtlas*)(m_assetManager ? m_assetManager->getFont("main_font") : nullptr);
FontAtlas* font = pixelFont ? pixelFont : mainFont;
// Inline small helper for drawing a filled rect in logical coords
auto fillRect = [&](float x, float y, float w, float h, SDL_Color c) {
SDL_SetRenderDrawColor(renderer.getSDLRenderer(), c.r, c.g, c.b, c.a);
SDL_FRect r{ x, y, w, h };
SDL_RenderFillRect(renderer.getSDLRenderer(), &r);
};
// Prepare text lines
char buf[128];
std::snprintf(buf, sizeof(buf), "Win: %d x %d", winW, winH);
std::string sWin(buf);
std::snprintf(buf, sizeof(buf), "Logical: %.0f x %.0f", LOGICAL_W, LOGICAL_H);
std::string sLogical(buf);
std::snprintf(buf, sizeof(buf), "Scale: %.2f", logicalScale);
std::string sScale(buf);
// Determine size of longest line
int w1=0,h1=0, w2=0,h2=0, w3=0,h3=0;
if (font) {
font->measure(sWin, 1.0f, w1, h1);
font->measure(sLogical, 1.0f, w2, h2);
font->measure(sScale, 1.0f, w3, h3);
}
int maxW = std::max({w1,w2,w3});
int totalH = (h1 + h2 + h3) + 8; // small padding
// Position based on actual screen width (center horizontally)
const float margin = 8.0f;
// float x = (LOGICAL_W - (float)maxW) * 0.5f; // Center horizontally
// float y = margin;
// Desired position in window (pixel) coords
int winW_px = 0, winH_px = 0;
renderer.getWindowSize(winW_px, winH_px);
float desiredWinX = (float(winW_px) - (float)maxW) * 0.5f; // center on full window width
float desiredWinY = margin; // near top of the window
// Convert window coords to logical coords under current viewport/scale
float invScale = (logicalScale > 0.0f) ? (1.0f / logicalScale) : 1.0f;
float x = (desiredWinX - float(logicalVP.x)) * invScale;
float y = (desiredWinY - float(logicalVP.y)) * invScale;
// Draw background box for readability
fillRect(x - 6.0f, y - 6.0f, (float)maxW + 12.0f, (float)totalH + 8.0f, {0, 0, 0, 180});
// Draw text lines
SDL_Color textColor = {255, 204, 0, 255};
if (font) {
font->draw(renderer.getSDLRenderer(), x, y, sWin, 1.0f, textColor);
font->draw(renderer.getSDLRenderer(), x, y + (float)h1, sLogical, 1.0f, textColor);
font->draw(renderer.getSDLRenderer(), x, y + (float)(h1 + h2), sScale, 1.0f, textColor);
}
// Reset viewport/scale
SDL_SetRenderViewport(renderer.getSDLRenderer(), nullptr);
SDL_SetRenderScale(renderer.getSDLRenderer(), 1.0f, 1.0f);
};
// Register debug overlay for all primary states so it draws on top
if (m_stateManager) {
m_stateManager->registerRenderHandler(AppState::Loading, debugOverlay);
m_stateManager->registerRenderHandler(AppState::Menu, debugOverlay);
m_stateManager->registerRenderHandler(AppState::LevelSelector, debugOverlay);
m_stateManager->registerRenderHandler(AppState::Playing, debugOverlay);
m_stateManager->registerRenderHandler(AppState::GameOver, debugOverlay);
}
}
void ApplicationManager::processEvents() {
// Let InputManager process all SDL events
if (m_inputManager) {
m_inputManager->processEvents();
// Check if InputManager detected a quit request
if (m_inputManager->shouldQuit()) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "InputManager reports shouldQuit() == true — requesting shutdown");
requestShutdown();
return;
}
}
// Handle any additional events not processed by InputManager
// (In this case, we rely on InputManager for most event handling)
}
void ApplicationManager::update(float deltaTime) {
// Update AssetManager for progressive loading
if (m_assetManager) {
m_assetManager->update(deltaTime);
}
// Update InputManager
if (m_inputManager) {
m_inputManager->update(deltaTime);
}
// Always update 3D starfield so background animates even during loading/menu
if (m_starfield3D) {
m_starfield3D->update(deltaTime);
}
// Update StateManager
if (m_stateManager) {
// 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");
}
}
void ApplicationManager::render() {
if (!m_renderManager) {
return;
}
SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "ApplicationManager::render - about to begin frame");
// Trace render begin
traceFile("render begin");
m_renderManager->beginFrame();
SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "ApplicationManager::render - beginFrame complete");
// Delegate rendering to StateManager
if (m_stateManager) {
m_stateManager->render(*m_renderManager);
SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "ApplicationManager::render - state render completed for state %s", m_stateManager->getStateName(m_stateManager->getState()));
}
m_renderManager->endFrame();
SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "ApplicationManager::render - endFrame complete");
traceFile("render endFrame complete");
}
void ApplicationManager::cleanupManagers() {
// 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();
m_renderManager.reset();
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Managers cleaned up");
}
void ApplicationManager::cleanupSDL() {
TTF_Quit();
SDL_Quit();
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "SDL cleaned up");
}