Fixed loader, main menu and level selector

This commit is contained in:
2025-08-17 16:51:33 +02:00
parent e591aaba45
commit d75bfcf4d0
11 changed files with 1034 additions and 57 deletions

View File

@ -1,16 +1,37 @@
#include "ApplicationManager.h"
#include "StateManager.h"
#include "InputManager.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 "AssetManager.h"
#include "Config.h"
#include "GlobalState.h"
#include "../graphics/RenderManager.h"
#include "../graphics/Font.h"
#include "../graphics/Starfield3D.h"
#include "../graphics/Starfield.h"
#include "../gameplay/Game.h"
#include "../gameplay/LineEffect.h"
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <iostream>
#include <cmath>
#include <fstream>
ApplicationManager::ApplicationManager() = default;
static void traceFile(const char* msg) {
std::ofstream f("tetris_trace.log", std::ios::app);
if (f) f << msg << "\n";
}
ApplicationManager::~ApplicationManager() {
if (m_initialized) {
shutdown();
@ -64,8 +85,11 @@ void ApplicationManager::run() {
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Starting main application loop");
traceFile("Main loop starting");
while (m_running) {
SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Main loop iteration start: m_running=%d", m_running ? 1 : 0);
traceFile("Main loop iteration");
// Calculate delta time
uint64_t currentTime = SDL_GetTicks();
float deltaTime = (currentTime - m_lastFrameTime) / 1000.0f;
@ -81,6 +105,7 @@ void ApplicationManager::run() {
if (m_running) {
update(deltaTime);
traceFile("about to call render");
render();
}
}
@ -150,9 +175,66 @@ bool ApplicationManager::initializeManagers() {
return false;
}
// Ensure SoundEffectManager is initialized early so SFX loads work
SoundEffectManager::instance().init();
// Create StateManager (will be enhanced in next steps)
m_stateManager = std::make_unique<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;
SDL_Event ev{};
ev.type = pressed ? SDL_EVENT_KEY_DOWN : SDL_EVENT_KEY_UP;
ev.key.scancode = sc;
ev.key.repeat = 0;
m_stateManager->handleEvent(ev);
});
m_inputManager->registerMouseButtonHandler([this](int button, bool pressed, float x, float y){
if (!m_stateManager) return;
SDL_Event ev{};
ev.type = pressed ? SDL_EVENT_MOUSE_BUTTON_DOWN : SDL_EVENT_MOUSE_BUTTON_UP;
ev.button.button = button;
ev.button.x = int(x);
ev.button.y = int(y);
m_stateManager->handleEvent(ev);
});
m_inputManager->registerMouseMotionHandler([this](float x, float y, float dx, float dy){
if (!m_stateManager) return;
SDL_Event ev{};
ev.type = SDL_EVENT_MOUSE_MOTION;
ev.motion.x = int(x);
ev.motion.y = int(y);
ev.motion.xrel = int(dx);
ev.motion.yrel = int(dy);
m_stateManager->handleEvent(ev);
});
m_inputManager->registerWindowEventHandler([this](const SDL_WindowEvent& we){
if (!m_stateManager) return;
SDL_Event ev{};
ev.type = SDL_EVENT_WINDOW_RESIZED; // generic mapping; handlers can inspect inner fields
ev.window = we;
m_stateManager->handleEvent(ev);
});
m_inputManager->registerQuitHandler([this](){
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "InputManager quit handler invoked");
SDL_Event ev{}; ev.type = SDL_EVENT_QUIT; if (m_stateManager) m_stateManager->handleEvent(ev);
});
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Managers initialized successfully");
return true;
}
@ -167,8 +249,17 @@ bool ApplicationManager::initializeGame() {
AssetManager::LoadingTask blocksTask{AssetManager::LoadingTask::TEXTURE, "blocks", Config::Assets::BLOCKS_BMP};
AssetManager::LoadingTask fontTask{AssetManager::LoadingTask::FONT, "main_font", Config::Fonts::DEFAULT_FONT_PATH, Config::Fonts::DEFAULT_FONT_SIZE};
AssetManager::LoadingTask pixelFontTask{AssetManager::LoadingTask::FONT, "pixel_font", Config::Fonts::PIXEL_FONT_PATH, Config::Fonts::PIXEL_FONT_SIZE};
// Add tasks to AssetManager
// Pre-load the pixel (retro) font synchronously so the loading screen can render text immediately
if (!m_assetManager->getFont("pixel_font")) {
if (m_assetManager->loadFont("pixel_font", Config::Fonts::PIXEL_FONT_PATH, Config::Fonts::PIXEL_FONT_SIZE)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Preloaded pixel_font for loading screen");
} else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Failed to preload pixel_font; loading screen will fallback to main_font");
}
}
// Add tasks to AssetManager (pixel font task will be skipped if already loaded)
m_assetManager->addLoadingTask(logoTask);
m_assetManager->addLoadingTask(backgroundTask);
m_assetManager->addLoadingTask(blocksTask);
@ -180,7 +271,7 @@ bool ApplicationManager::initializeGame() {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Asset loading progress: %.1f%%", progress * 100.0f);
});
// Load sound effects with fallback
// Load sound effects with fallback (SoundEffectManager already initialized)
m_assetManager->loadSoundEffectWithFallback("clear_line", "clear_line");
m_assetManager->loadSoundEffectWithFallback("nice_combo", "nice_combo");
m_assetManager->loadSoundEffectWithFallback("amazing", "amazing");
@ -188,35 +279,559 @@ bool ApplicationManager::initializeGame() {
// Start background music loading
m_assetManager->startBackgroundMusicLoading();
// Initialize a basic test render handler that shows loaded assets
m_stateManager->registerRenderHandler(AppState::Loading,
[this](RenderManager& renderer) {
// Simple test render - just clear screen and show loaded assets
renderer.clear(20, 30, 40, 255);
// Try to render background if loaded
SDL_Texture* background = m_assetManager->getTexture("background");
if (background) {
SDL_FRect bgRect = { 0, 0, Config::Logical::WIDTH, Config::Logical::HEIGHT };
renderer.renderTexture(background, nullptr, &bgRect);
}
// Try to render logo if loaded
SDL_Texture* logo = m_assetManager->getTexture("logo");
if (logo) {
SDL_FRect logoRect = { 300, 200, 600, 200 };
renderer.renderTexture(logo, nullptr, &logoRect);
}
// Show asset loading status
SDL_FRect statusRect = { 50, 50, 400, 30 };
renderer.renderRect(statusRect, 0, 100, 200, 200);
// Create and populate shared StateContext similar to main.cpp so states like MenuState
// receive the same pointers and flags they expect.
// Create ScoreManager and load scores
m_scoreManager = std::make_unique<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>();
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();
m_currentTrackLoading = 1; // mark started
}
// Instantiate state objects and populate a StateContext similar to main.cpp
// so that existing state classes (MenuState, LoadingState, etc.) receive
// the resources they expect.
{
m_stateContext.stateManager = m_stateManager.get();
m_stateContext.game = m_game.get();
m_stateContext.scores = m_scoreManager.get();
m_stateContext.starfield = m_starfield.get();
m_stateContext.starfield3D = m_starfield3D.get();
m_stateContext.font = (FontAtlas*)m_assetManager->getFont("main_font");
m_stateContext.pixelFont = (FontAtlas*)m_assetManager->getFont("pixel_font");
m_stateContext.lineEffect = m_lineEffect.get();
m_stateContext.logoTex = m_assetManager->getTexture("logo");
// Attempt to load a small logo variant if present to match original UX
SDL_Texture* logoSmall = m_assetManager->getTexture("logo_small");
if (!logoSmall) {
// Try to load image from disk and register with AssetManager
if (m_assetManager->loadTexture("logo_small", "assets/images/logo_small.bmp")) {
logoSmall = m_assetManager->getTexture("logo_small");
}
}
m_stateContext.logoSmallTex = logoSmall;
if (logoSmall) {
int w = 0, h = 0; if (m_renderManager) m_renderManager->getTextureSize(logoSmall, w, h);
m_stateContext.logoSmallW = w; m_stateContext.logoSmallH = h;
} else { m_stateContext.logoSmallW = 0; m_stateContext.logoSmallH = 0; }
m_stateContext.backgroundTex = m_assetManager->getTexture("background");
m_stateContext.blocksTex = m_assetManager->getTexture("blocks");
m_stateContext.musicEnabled = &m_musicEnabled;
m_stateContext.startLevelSelection = &m_startLevelSelection;
m_stateContext.hoveredButton = &m_hoveredButton;
m_stateContext.showSettingsPopup = &m_showSettingsPopup;
m_stateContext.showExitConfirmPopup = &m_showExitConfirmPopup;
// Create state instances
m_loadingState = std::make_unique<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)
m_stateManager->registerRenderHandler(AppState::Loading,
[this, drawRect](RenderManager& renderer) {
// Clear background first
renderer.clear(0, 0, 0, 255);
// Use 3D starfield for loading screen (full screen)
// Ensure starfield uses actual window size so center and projection are correct
if (m_starfield3D) {
int winW_actual = 0, winH_actual = 0;
if (m_renderManager) {
m_renderManager->getWindowSize(winW_actual, winH_actual);
}
if (winW_actual > 0 && winH_actual > 0) {
m_starfield3D->resize(winW_actual, winH_actual);
}
m_starfield3D->draw(renderer.getSDLRenderer());
}
// Set viewport and scaling for content
int winW = Config::Window::DEFAULT_WIDTH;
int winH = Config::Window::DEFAULT_HEIGHT;
int LOGICAL_W = Config::Logical::WIDTH;
int LOGICAL_H = Config::Logical::HEIGHT;
// Calculate logical scaling and viewport
float scaleX = static_cast<float>(winW) / LOGICAL_W;
float scaleY = static_cast<float>(winH) / LOGICAL_H;
float logicalScale = std::min(scaleX, scaleY);
int vpW = static_cast<int>(LOGICAL_W * logicalScale);
int vpH = static_cast<int>(LOGICAL_H * logicalScale);
int vpX = (winW - vpW) / 2;
int vpY = (winH - vpH) / 2;
SDL_Rect logicalVP = { vpX, vpY, vpW, vpH };
SDL_SetRenderViewport(renderer.getSDLRenderer(), &logicalVP);
SDL_SetRenderScale(renderer.getSDLRenderer(), logicalScale, logicalScale);
// Calculate actual content area (centered within the window)
float contentScale = logicalScale;
float contentW = LOGICAL_W * contentScale;
float contentH = LOGICAL_H * contentScale;
float contentOffsetX = (winW - contentW) * 0.5f / contentScale;
float contentOffsetY = (winH - contentH) * 0.5f / contentScale;
auto drawRectOriginal = [&](float x, float y, float w, float h, SDL_Color c) {
SDL_SetRenderDrawColor(renderer.getSDLRenderer(), c.r, c.g, c.b, c.a);
SDL_FRect fr{x + contentOffsetX, y + contentOffsetY, w, h};
SDL_RenderFillRect(renderer.getSDLRenderer(), &fr);
};
// Calculate dimensions for perfect centering (like JavaScript version)
const bool isLimitedHeight = LOGICAL_H < 450;
SDL_Texture* logoTex = m_assetManager->getTexture("logo");
const float logoHeight = logoTex ? (isLimitedHeight ? LOGICAL_H * 0.25f : LOGICAL_H * 0.4f) : 0;
const float loadingTextHeight = 20; // Height of "LOADING" text (match JS)
const float barHeight = 20; // Loading bar height (match JS)
const float barPaddingVertical = isLimitedHeight ? 15 : 35;
const float percentTextHeight = 24; // Height of percentage text
const float spacingBetweenElements = isLimitedHeight ? 5 : 15;
// Total content height
const float totalContentHeight = logoHeight +
(logoHeight > 0 ? spacingBetweenElements : 0) +
loadingTextHeight +
barPaddingVertical +
barHeight +
spacingBetweenElements +
percentTextHeight;
// Start Y position for perfect vertical centering
float currentY = (LOGICAL_H - totalContentHeight) / 2.0f;
// Draw logo (centered, static like JavaScript version)
if (logoTex) {
// Use the same original large logo dimensions as JS (we used a half-size BMP previously)
const int lw = 872, lh = 273;
// Cap logo width similar to JS UI.MAX_LOGO_WIDTH (600) and available screen space
const float maxLogoWidth = std::min(LOGICAL_W * 0.9f, 600.0f);
const float availableHeight = isLimitedHeight ? LOGICAL_H * 0.25f : LOGICAL_H * 0.4f;
const float availableWidth = maxLogoWidth;
const float scaleFactorWidth = availableWidth / static_cast<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;
}
// Draw "LOADING" text (centered, using pixel font with fallback to main_font)
FontAtlas* pixelFont = (FontAtlas*)m_assetManager->getFont("pixel_font");
FontAtlas* fallbackFont = (FontAtlas*)m_assetManager->getFont("main_font");
FontAtlas* loadingFont = pixelFont ? pixelFont : fallbackFont;
if (loadingFont) {
const char* loadingText = "LOADING";
float textWidth = strlen(loadingText) * 12.0f; // Approximate width for pixel font
float textX = (LOGICAL_W - textWidth) / 2.0f;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Rendering LOADING text at (%f,%f)", textX + contentOffsetX, currentY + contentOffsetY);
loadingFont->draw(renderer.getSDLRenderer(), textX + contentOffsetX, currentY + contentOffsetY, loadingText, 1.0f, {255, 204, 0, 255});
} else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "No loading font available to render LOADING text");
}
currentY += loadingTextHeight + barPaddingVertical;
// Draw loading bar (like JavaScript version)
const int barW = 400, barH = 20;
const int bx = (LOGICAL_W - barW) / 2;
float loadingProgress = m_assetManager->getLoadingProgress();
// Bar border (dark gray) - using drawRect which adds content offset
drawRectOriginal(bx - 3, currentY - 3, barW + 6, barH + 6, {68, 68, 80, 255});
// Bar background (darker gray)
drawRectOriginal(bx, currentY, barW, barH, {34, 34, 34, 255});
// Progress bar (gold color)
drawRectOriginal(bx, currentY, int(barW * loadingProgress), barH, {255, 204, 0, 255});
currentY += barH + spacingBetweenElements;
// Draw percentage text (centered, using loadingFont)
if (loadingFont) {
int percentage = int(loadingProgress * 100);
char percentText[16];
std::snprintf(percentText, sizeof(percentText), "%d%%", percentage);
float percentWidth = strlen(percentText) * 12.0f; // Approximate width for pixel font
float percentX = (LOGICAL_W - percentWidth) / 2.0f;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Rendering percent text '%s' at (%f,%f)", percentText, percentX + contentOffsetX, currentY + contentOffsetY);
loadingFont->draw(renderer.getSDLRenderer(), percentX + contentOffsetX, currentY + contentOffsetY, percentText, 1.5f, {255, 204, 0, 255});
}
// Reset viewport and scale
SDL_SetRenderViewport(renderer.getSDLRenderer(), nullptr);
SDL_SetRenderScale(renderer.getSDLRenderer(), 1.0f, 1.0f);
});
m_stateManager->registerUpdateHandler(AppState::Loading,
[this](float deltaTime) {
// Update 3D starfield so stars move during loading
if (m_starfield3D) {
m_starfield3D->update(deltaTime);
}
// Check if loading is complete and transition to menu
if (m_assetManager->isLoadingComplete()) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loading complete, transitioning to Menu");
bool ok = m_stateManager->setState(AppState::Menu);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "setState(AppState::Menu) returned %d", ok ? 1 : 0);
traceFile("- to Menu returned");
}
});
// Menu State render: draw background full-screen, then delegate to MenuState::render
m_stateManager->registerRenderHandler(AppState::Menu,
[this](RenderManager& renderer) {
// Clear and draw background to full window
renderer.clear(0, 0, 20, 255);
int winW = 0, winH = 0;
if (m_renderManager) m_renderManager->getWindowSize(winW, winH);
SDL_Texture* background = m_assetManager->getTexture("background");
if (background && winW > 0 && winH > 0) {
SDL_FRect bgRect = { 0, 0, (float)winW, (float)winH };
renderer.renderTexture(background, nullptr, &bgRect);
}
// Compute logical scale and viewport
const int LOGICAL_W = Config::Logical::WIDTH;
const int LOGICAL_H = Config::Logical::HEIGHT;
float scaleX = winW > 0 ? (float)winW / LOGICAL_W : 1.0f;
float scaleY = winH > 0 ? (float)winH / LOGICAL_H : 1.0f;
float logicalScale = std::min(scaleX, scaleY);
int vpW = (int)(LOGICAL_W * logicalScale);
int vpH = (int)(LOGICAL_H * logicalScale);
int vpX = (winW - vpW) / 2;
int vpY = (winH - vpH) / 2;
SDL_Rect logicalVP{vpX, vpY, vpW, vpH};
// Apply viewport+scale then call MenuState::render (shows highscores, fireworks, bottom buttons)
SDL_SetRenderViewport(renderer.getSDLRenderer(), &logicalVP);
SDL_SetRenderScale(renderer.getSDLRenderer(), logicalScale, logicalScale);
if (m_menuState) {
m_menuState->render(renderer.getSDLRenderer(), logicalScale, logicalVP);
}
// Reset to defaults
SDL_SetRenderViewport(renderer.getSDLRenderer(), nullptr);
SDL_SetRenderScale(renderer.getSDLRenderer(), 1.0f, 1.0f);
});
// LevelSelector State render: draw background full-screen, then delegate to LevelSelectorState::render
m_stateManager->registerRenderHandler(AppState::LevelSelector,
[this](RenderManager& renderer) {
// Clear and draw background to full window
renderer.clear(0, 0, 20, 255);
int winW = 0, winH = 0;
if (m_renderManager) m_renderManager->getWindowSize(winW, winH);
SDL_Texture* background = m_assetManager->getTexture("background");
if (background && winW > 0 && winH > 0) {
SDL_FRect bgRect = { 0, 0, (float)winW, (float)winH };
renderer.renderTexture(background, nullptr, &bgRect);
}
// Compute logical scale and viewport
const int LOGICAL_W = Config::Logical::WIDTH;
const int LOGICAL_H = Config::Logical::HEIGHT;
float scaleX = winW > 0 ? (float)winW / LOGICAL_W : 1.0f;
float scaleY = winH > 0 ? (float)winH / LOGICAL_H : 1.0f;
float logicalScale = std::min(scaleX, scaleY);
int vpW = (int)(LOGICAL_W * logicalScale);
int vpH = (int)(LOGICAL_H * logicalScale);
int vpX = (winW - vpW) / 2;
int vpY = (winH - vpH) / 2;
SDL_Rect logicalVP{vpX, vpY, vpW, vpH};
// Apply viewport+scale then call LevelSelectorState::render (shows level selection popup)
SDL_SetRenderViewport(renderer.getSDLRenderer(), &logicalVP);
SDL_SetRenderScale(renderer.getSDLRenderer(), logicalScale, logicalScale);
if (m_levelSelectorState) {
m_levelSelectorState->render(renderer.getSDLRenderer(), logicalScale, logicalVP);
}
// Reset to defaults
SDL_SetRenderViewport(renderer.getSDLRenderer(), nullptr);
SDL_SetRenderScale(renderer.getSDLRenderer(), 1.0f, 1.0f);
});
m_stateManager->registerUpdateHandler(AppState::Menu,
[this](float deltaTime) {
// Update logo animation counter
m_logoAnimCounter += deltaTime;
// Update fireworks effect
GlobalState& globalState = GlobalState::instance();
globalState.updateFireworks(deltaTime);
// Start background music once tracks are available and not yet started
if (m_musicEnabled && !m_musicStarted) {
if (Audio::instance().getLoadedTrackCount() > 0) {
Audio::instance().shuffle();
Audio::instance().start();
m_musicStarted = true;
}
}
});
m_stateManager->registerEventHandler(AppState::Menu,
[this](const SDL_Event& event) {
// Forward keyboard events (Enter/Escape) to trigger actions, match original main.cpp
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) {
if (event.key.scancode == SDL_SCANCODE_RETURN || event.key.scancode == SDL_SCANCODE_RETURN2 || event.key.scancode == SDL_SCANCODE_KP_ENTER) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Starting game from menu (Enter)");
// Reset start level and transition
// In the original main, game.reset(...) was called; here we only switch state.
m_stateManager->setState(AppState::Playing);
return;
}
if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
// If an exit confirmation is already showing, accept it and quit.
if (m_showExitConfirmPopup) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Quitting from menu (Escape confirmed)");
m_running = false;
return;
}
// Otherwise, show the exit confirmation popup instead of quitting immediately.
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Showing exit confirmation (Escape)");
m_showExitConfirmPopup = true;
return;
}
// Global toggles
if (event.key.scancode == SDL_SCANCODE_M) {
Audio::instance().toggleMute();
m_musicEnabled = !m_musicEnabled;
}
if (event.key.scancode == SDL_SCANCODE_S) {
SoundEffectManager::instance().setEnabled(!SoundEffectManager::instance().isEnabled());
}
if (event.key.scancode == SDL_SCANCODE_N) {
SoundEffectManager::instance().playSound("lets_go", 1.0f);
}
}
// Mouse handling: map SDL mouse coords into logical content coords and
// perform hit-tests for menu buttons similar to main.cpp.
if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
float mx = (float)event.button.x;
float my = (float)event.button.y;
int winW = 0, winH = 0;
if (m_renderManager) m_renderManager->getWindowSize(winW, winH);
float logicalScale = std::min(winW / (float)Config::Logical::WIDTH, winH / (float)Config::Logical::HEIGHT);
if (logicalScale <= 0) logicalScale = 1.0f;
SDL_Rect logicalVP{0,0,winW,winH};
// Check bounds and compute content-local coords
if (mx >= logicalVP.x && my >= logicalVP.y && mx <= logicalVP.x + logicalVP.w && my <= logicalVP.y + logicalVP.h) {
float lx = (mx - logicalVP.x) / logicalScale;
float ly = (my - logicalVP.y) / logicalScale;
// Respect settings popup
if (m_showSettingsPopup) {
m_showSettingsPopup = false;
} else {
bool isSmall = ((Config::Logical::WIDTH * logicalScale) < 700.0f);
float btnW = isSmall ? (Config::Logical::WIDTH * 0.4f) : 300.0f;
float btnH = isSmall ? 60.0f : 70.0f;
float btnCX = Config::Logical::WIDTH * 0.5f;
const float btnYOffset = 40.0f;
float btnCY = Config::Logical::HEIGHT * 0.86f + btnYOffset;
SDL_FRect playBtn{btnCX - btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH};
SDL_FRect levelBtn{btnCX + btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH};
if (lx >= playBtn.x && lx <= playBtn.x + playBtn.w && ly >= playBtn.y && ly <= playBtn.y + playBtn.h) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Menu: Play button clicked");
m_stateManager->setState(AppState::Playing);
} else if (lx >= levelBtn.x && lx <= levelBtn.x + levelBtn.w && ly >= levelBtn.y && ly <= levelBtn.y + levelBtn.h) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Menu: Level button clicked");
m_stateManager->setState(AppState::LevelSelector);
} else {
// Settings area detection (top-right small area)
SDL_FRect settingsBtn{Config::Logical::WIDTH - 60, 10, 50, 30};
if (lx >= settingsBtn.x && lx <= settingsBtn.x + settingsBtn.w && ly >= settingsBtn.y && ly <= settingsBtn.y + settingsBtn.h) {
m_showSettingsPopup = true;
}
}
}
}
}
// Mouse motion handling for hover
if (event.type == SDL_EVENT_MOUSE_MOTION) {
float mx = (float)event.motion.x;
float my = (float)event.motion.y;
int winW = 0, winH = 0;
if (m_renderManager) m_renderManager->getWindowSize(winW, winH);
float logicalScale = std::min(winW / (float)Config::Logical::WIDTH, winH / (float)Config::Logical::HEIGHT);
if (logicalScale <= 0) logicalScale = 1.0f;
SDL_Rect logicalVP{0,0,winW,winH};
if (mx >= logicalVP.x && my >= logicalVP.y && mx <= logicalVP.x + logicalVP.w && my <= logicalVP.y + logicalVP.h) {
float lx = (mx - logicalVP.x) / logicalScale;
float ly = (my - logicalVP.y) / logicalScale;
if (!m_showSettingsPopup) {
bool isSmall = ((Config::Logical::WIDTH * logicalScale) < 700.0f);
float btnW = isSmall ? (Config::Logical::WIDTH * 0.4f) : 300.0f;
float btnH = isSmall ? 60.0f : 70.0f;
float btnCX = Config::Logical::WIDTH * 0.5f;
const float btnYOffset = 40.0f;
float btnCY = Config::Logical::HEIGHT * 0.86f + btnYOffset;
SDL_FRect playBtn{btnCX - btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH};
SDL_FRect levelBtn{btnCX + btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH};
m_hoveredButton = -1;
if (lx >= playBtn.x && lx <= playBtn.x + playBtn.w && ly >= playBtn.y && ly <= playBtn.y + playBtn.h) {
m_hoveredButton = 0;
} else if (lx >= levelBtn.x && lx <= levelBtn.x + levelBtn.w && ly >= levelBtn.y && ly <= levelBtn.y + levelBtn.h) {
m_hoveredButton = 1;
}
}
}
}
});
// Playing State - Placeholder for now
m_stateManager->registerRenderHandler(AppState::Playing,
[this](RenderManager& renderer) {
renderer.clear(0, 0, 0, 255);
// For now, just show a placeholder
FontAtlas* font = (FontAtlas*)m_assetManager->getFont("main_font");
if (font) {
float centerX = Config::Window::DEFAULT_WIDTH / 2.0f;
float centerY = Config::Window::DEFAULT_HEIGHT / 2.0f;
std::string playingText = "TETRIS GAME PLAYING STATE";
float textX = centerX - (playingText.length() * 12.0f) / 2.0f;
font->draw(renderer.getSDLRenderer(), textX, centerY, playingText, 2.0f, {255, 255, 255, 255});
std::string instruction = "Press ESC to return to menu";
float instrX = centerX - (instruction.length() * 8.0f) / 2.0f;
font->draw(renderer.getSDLRenderer(), instrX, centerY + 60, instruction, 1.0f, {200, 200, 200, 255});
}
});
m_stateManager->registerEventHandler(AppState::Playing,
[this](const SDL_Event& event) {
if (event.type == SDL_EVENT_KEY_DOWN) {
if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Returning to menu from playing state");
m_stateManager->setState(AppState::Menu);
}
}
});
}
void ApplicationManager::processEvents() {
// Let InputManager process all SDL events
if (m_inputManager) {
@ -224,6 +839,7 @@ void ApplicationManager::processEvents() {
// Check if InputManager detected a quit request
if (m_inputManager->shouldQuit()) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "InputManager reports shouldQuit() == true — requesting shutdown");
requestShutdown();
return;
}
@ -234,14 +850,26 @@ void ApplicationManager::processEvents() {
}
void ApplicationManager::update(float deltaTime) {
// Update AssetManager for progressive loading
if (m_assetManager) {
m_assetManager->update(deltaTime);
}
// Update InputManager
if (m_inputManager) {
m_inputManager->update(deltaTime);
}
// Always update 3D starfield so background animates even during loading/menu
if (m_starfield3D) {
m_starfield3D->update(deltaTime);
}
// Update StateManager
if (m_stateManager) {
m_stateManager->update(deltaTime);
SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "ApplicationManager::update - state update completed for state %s", m_stateManager->getStateName(m_stateManager->getState()));
traceFile("update completed");
}
}
@ -249,15 +877,21 @@ void ApplicationManager::render() {
if (!m_renderManager) {
return;
}
SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "ApplicationManager::render - about to begin frame");
// Trace render begin
traceFile("render begin");
m_renderManager->beginFrame();
SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "ApplicationManager::render - beginFrame complete");
// Delegate rendering to StateManager
if (m_stateManager) {
m_stateManager->render(*m_renderManager);
SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "ApplicationManager::render - state render completed for state %s", m_stateManager->getStateName(m_stateManager->getState()));
}
m_renderManager->endFrame();
SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "ApplicationManager::render - endFrame complete");
traceFile("render endFrame complete");
}
void ApplicationManager::cleanupManagers() {

View File

@ -1,6 +1,7 @@
#pragma once
#include "Config.h"
#include "../states/State.h"
#include <memory>
#include <string>
@ -16,6 +17,12 @@ class Starfield3D;
class FontAtlas;
class LineEffect;
// Forward declare state classes (top-level, defined under src/states)
class LoadingState;
class MenuState;
class LevelSelectorState;
class PlayingState;
/**
* ApplicationManager - Central coordinator for the entire application lifecycle
*
@ -50,6 +57,7 @@ private:
bool initializeSDL();
bool initializeManagers();
bool initializeGame();
void setupStateHandlers();
// Main loop methods
void processEvents();
@ -66,6 +74,37 @@ private:
std::unique_ptr<AssetManager> m_assetManager;
std::unique_ptr<StateManager> m_stateManager;
// Visual effects
std::unique_ptr<Starfield3D> m_starfield3D;
std::unique_ptr<Starfield> m_starfield;
// Menu / UI state pieces mirrored from main.cpp
bool m_musicEnabled = true;
int m_startLevelSelection = 0;
int m_hoveredButton = -1;
bool m_showSettingsPopup = false;
bool m_showExitConfirmPopup = false;
uint64_t m_loadStartTicks = 0;
bool m_musicStarted = false;
bool m_musicLoaded = false;
int m_currentTrackLoading = 0;
int m_totalTracks = 0;
// Persistence (ScoreManager declared at top-level)
std::unique_ptr<ScoreManager> m_scoreManager;
// Gameplay pieces
std::unique_ptr<Game> m_game;
std::unique_ptr<LineEffect> m_lineEffect;
// State context (must be a member to ensure lifetime)
StateContext m_stateContext;
// State objects (mirror main.cpp pattern)
std::unique_ptr<LoadingState> m_loadingState;
std::unique_ptr<MenuState> m_menuState;
std::unique_ptr<LevelSelectorState> m_levelSelectorState;
std::unique_ptr<PlayingState> m_playingState;
// Application state
bool m_running = false;
bool m_initialized = false;
@ -77,4 +116,7 @@ private:
int m_windowWidth = Config::Window::DEFAULT_WIDTH;
int m_windowHeight = Config::Window::DEFAULT_HEIGHT;
std::string m_windowTitle = Config::Window::DEFAULT_TITLE;
// Animation state
float m_logoAnimCounter = 0.0f;
};

View File

@ -10,6 +10,9 @@ AssetManager::AssetManager()
: m_renderer(nullptr)
, m_audioSystem(nullptr)
, m_soundSystem(nullptr)
, m_totalLoadingTasks(0)
, m_completedLoadingTasks(0)
, m_loadingComplete(false)
, m_defaultTexturePath("assets/images/")
, m_defaultFontPath("assets/fonts/")
, m_initialized(false) {
@ -260,49 +263,87 @@ void AssetManager::addLoadingTask(const LoadingTask& task) {
void AssetManager::executeLoadingTasks(std::function<void(float)> progressCallback) {
if (m_loadingTasks.empty()) {
m_loadingComplete = true;
if (progressCallback) progressCallback(1.0f);
return;
}
logInfo("Executing " + std::to_string(m_loadingTasks.size()) + " loading tasks...");
logInfo("Starting progressive loading of " + std::to_string(m_loadingTasks.size()) + " loading tasks...");
size_t totalTasks = m_loadingTasks.size();
size_t completedTasks = 0;
m_totalLoadingTasks = m_loadingTasks.size();
m_completedLoadingTasks = 0;
m_currentTaskIndex = 0;
m_loadingComplete = false;
m_isProgressiveLoading = true;
m_lastLoadTime = SDL_GetTicks();
m_musicLoadingStarted = false;
m_musicLoadingProgress = 0.0f;
// Don't execute tasks immediately - let update() handle them progressively
}
for (const auto& task : m_loadingTasks) {
void AssetManager::update(float deltaTime) {
if (!m_isProgressiveLoading || m_loadingTasks.empty()) {
// Handle music loading progress simulation if assets are done
if (m_musicLoadingStarted && !m_loadingComplete) {
m_musicLoadingProgress += deltaTime * 0.4f; // Simulate music loading progress
if (m_musicLoadingProgress >= 1.0f) {
m_musicLoadingProgress = 1.0f;
m_loadingComplete = true;
logInfo("Background music loading simulation complete");
}
}
return;
}
Uint64 currentTime = SDL_GetTicks();
// Add minimum delay between loading items (600ms per item for visual effect)
if (currentTime - m_lastLoadTime < 600) {
return;
}
// Load one item at a time
if (m_currentTaskIndex < m_loadingTasks.size()) {
const auto& task = m_loadingTasks[m_currentTaskIndex];
bool success = false;
switch (task.type) {
case LoadingTask::TEXTURE:
success = (loadTexture(task.id, task.filepath) != nullptr);
break;
case LoadingTask::FONT:
success = loadFont(task.id, task.filepath, task.fontSize);
break;
case LoadingTask::MUSIC:
success = loadMusicTrack(task.filepath);
break;
case LoadingTask::SOUND_EFFECT:
success = loadSoundEffect(task.id, task.filepath);
break;
}
if (!success) {
logError("Failed to load asset: " + task.id + " (" + task.filepath + ")");
}
completedTasks++;
if (progressCallback) {
float progress = static_cast<float>(completedTasks) / static_cast<float>(totalTasks);
progressCallback(progress);
m_currentTaskIndex++;
m_completedLoadingTasks = m_currentTaskIndex;
m_lastLoadTime = currentTime;
logInfo("Asset loading progress: " + std::to_string((float)m_completedLoadingTasks / m_totalLoadingTasks * 100.0f) + "%");
// Check if all asset tasks are complete
if (m_currentTaskIndex >= m_loadingTasks.size()) {
m_isProgressiveLoading = false;
logInfo("Completed " + std::to_string(m_completedLoadingTasks) + "/" + std::to_string(m_totalLoadingTasks) + " loading tasks");
// Start background music loading simulation
m_musicLoadingStarted = true;
m_musicLoadingProgress = 0.0f;
startBackgroundMusicLoading();
}
}
logInfo("Completed " + std::to_string(completedTasks) + "/" + std::to_string(totalTasks) + " loading tasks");
}
void AssetManager::clearLoadingTasks() {
@ -382,3 +423,23 @@ void AssetManager::logInfo(const std::string& message) const {
void AssetManager::logError(const std::string& message) const {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[AssetManager] %s", message.c_str());
}
// Loading progress tracking methods
bool AssetManager::isLoadingComplete() const {
// Loading is complete when both asset tasks and music loading are done
return m_loadingComplete && (!m_musicLoadingStarted || m_musicLoadingProgress >= 1.0f);
}
float AssetManager::getLoadingProgress() const {
if (m_totalLoadingTasks == 0) {
return 1.0f; // No tasks = complete
}
// Asset loading progress (80% of total progress)
float assetProgress = static_cast<float>(m_completedLoadingTasks) / static_cast<float>(m_totalLoadingTasks) * 0.8f;
// Music loading progress (20% of total progress)
float musicProgress = m_musicLoadingStarted ? m_musicLoadingProgress * 0.2f : 0.0f;
return assetProgress + musicProgress;
}

View File

@ -69,6 +69,13 @@ public:
void addLoadingTask(const LoadingTask& task);
void executeLoadingTasks(std::function<void(float)> progressCallback = nullptr);
void clearLoadingTasks();
void update(float deltaTime); // New: Progressive loading update
// Loading progress tracking
bool isLoadingComplete() const;
float getLoadingProgress() const;
size_t getTotalLoadingTasks() const { return m_totalLoadingTasks; }
size_t getCompletedLoadingTasks() const { return m_completedLoadingTasks; }
// Resource queries
size_t getTextureCount() const { return m_textures.size(); }
@ -88,6 +95,18 @@ private:
std::unordered_map<std::string, SDL_Texture*> m_textures;
std::unordered_map<std::string, std::unique_ptr<FontAtlas>> m_fonts;
std::vector<LoadingTask> m_loadingTasks;
// Loading progress tracking
size_t m_totalLoadingTasks = 0;
size_t m_completedLoadingTasks = 0;
bool m_loadingComplete = false;
// Progressive loading state
bool m_isProgressiveLoading = false;
size_t m_currentTaskIndex = 0;
Uint64 m_lastLoadTime = 0;
bool m_musicLoadingStarted = false;
float m_musicLoadingProgress = 0.0f;
// System references
SDL_Renderer* m_renderer;

View File

@ -16,11 +16,17 @@ void InputManager::processEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
// Trace every polled event type for debugging abrupt termination
{
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "InputManager: polled event type=%d\n", (int)event.type); fclose(f); }
}
switch (event.type) {
case SDL_EVENT_QUIT:
m_shouldQuit = true;
for (auto& handler : m_quitHandlers) {
for (auto& handler : m_quitHandlers) {
try {
// Trace quit event handling
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "InputManager: SDL_EVENT_QUIT polled\n"); fclose(f); }
handler();
} catch (const std::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_INPUT, "Exception in quit handler: %s", e.what());

View File

@ -84,6 +84,10 @@ bool StateManager::setState(AppState newState) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "State transition: %s -> %s",
getStateName(m_currentState), getStateName(newState));
// Persistent trace for debugging abrupt exits
{
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "setState start %s -> %s\n", getStateName(m_currentState), getStateName(newState)); fclose(f); }
}
// Execute exit hooks for current state
executeExitHooks(m_currentState);
@ -95,6 +99,11 @@ bool StateManager::setState(AppState newState) {
// Execute enter hooks for new state
executeEnterHooks(m_currentState);
// Trace completion
{
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "setState end %s\n", getStateName(m_currentState)); fclose(f); }
}
return true;
}
@ -175,13 +184,20 @@ void StateManager::executeEnterHooks(AppState state) {
return;
}
int idx = 0;
for (auto& hook : it->second) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Executing enter hook %d for state %s", idx, getStateName(state));
// Also write to trace file for persistent record
{
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "executeEnterHook %d %s\n", idx, getStateName(state)); fclose(f); }
}
try {
hook();
} catch (const std::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Exception in enter hook for state %s: %s",
getStateName(state), e.what());
}
++idx;
}
}
@ -191,12 +207,18 @@ void StateManager::executeExitHooks(AppState state) {
return;
}
int idx = 0;
for (auto& hook : it->second) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Executing exit hook %d for state %s", idx, getStateName(state));
{
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "executeExitHook %d %s\n", idx, getStateName(state)); fclose(f); }
}
try {
hook();
} catch (const std::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Exception in exit hook for state %s: %s",
getStateName(state), e.what());
}
++idx;
}
}