1421 lines
67 KiB
C++
1421 lines
67 KiB
C++
#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/OptionsState.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_image/SDL_image.h>
|
||
#include <SDL3_ttf/SDL_ttf.h>
|
||
#include "../../utils/ImagePathResolver.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;
|
||
}
|
||
m_isFullscreen = m_renderManager->isFullscreen();
|
||
|
||
// 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 F, F11 or Alt+Enter (or Alt+KP_Enter)
|
||
if (sc == SDL_SCANCODE_F || 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);
|
||
m_isFullscreen = m_renderManager->isFullscreen();
|
||
}
|
||
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 = we.type;
|
||
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");
|
||
traceFile("ApplicationManager: quit handler -> m_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;
|
||
m_stateContext.playerName = &m_playerName;
|
||
m_stateContext.fullscreenFlag = &m_isFullscreen;
|
||
m_stateContext.applyFullscreen = [this](bool enable) {
|
||
if (m_renderManager) {
|
||
m_renderManager->setFullscreen(enable);
|
||
m_isFullscreen = m_renderManager->isFullscreen();
|
||
} else {
|
||
m_isFullscreen = enable;
|
||
}
|
||
};
|
||
m_stateContext.queryFullscreen = [this]() -> bool {
|
||
if (m_renderManager) {
|
||
return m_renderManager->isFullscreen();
|
||
}
|
||
return m_isFullscreen;
|
||
};
|
||
m_stateContext.requestQuit = [this]() {
|
||
requestShutdown();
|
||
};
|
||
|
||
// Create state instances
|
||
m_loadingState = std::make_unique<LoadingState>(m_stateContext);
|
||
m_menuState = std::make_unique<MenuState>(m_stateContext);
|
||
m_optionsState = std::make_unique<OptionsState>(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::Options, [this](const SDL_Event& e){ if (m_optionsState) m_optionsState->handleEvent(e); });
|
||
m_stateManager->registerOnEnter(AppState::Options, [this](){ if (m_optionsState) m_optionsState->onEnter(); });
|
||
m_stateManager->registerOnExit(AppState::Options, [this](){ if (m_optionsState) m_optionsState->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);
|
||
});
|
||
|
||
m_stateManager->registerRenderHandler(AppState::Options,
|
||
[this](RenderManager& renderer) {
|
||
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);
|
||
}
|
||
|
||
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_optionsState) {
|
||
m_optionsState->render(renderer.getSDLRenderer(), logicalScale, logicalVP);
|
||
}
|
||
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 (don’t 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.jpg", bgLevel);
|
||
const std::string resolvedBgPath = AssetPath::resolveImagePath(bgPath);
|
||
SDL_Surface* s = IMG_Load(resolvedBgPath.c_str());
|
||
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;
|
||
if (resolvedBgPath != bgPath) {
|
||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loaded level %d background via %s", bgLevel, resolvedBgPath.c_str());
|
||
}
|
||
} 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<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)
|
||
);
|
||
|
||
// 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");
|
||
}
|