diff --git a/CMakeLists.txt b/CMakeLists.txt index ba39b6f..1a6174a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,6 +52,7 @@ set(TETRIS_SOURCES src/audio/Audio.cpp src/gameplay/effects/LineEffect.cpp src/audio/SoundEffect.cpp + src/ui/MenuLayout.cpp # State implementations (new) src/states/LoadingState.cpp src/states/MenuState.cpp diff --git a/settings.ini b/settings.ini index cf57a4c..d28b9c7 100644 --- a/settings.ini +++ b/settings.ini @@ -5,7 +5,7 @@ Fullscreen=1 [Audio] -Music=1 +Music=0 Sound=1 [Gameplay] diff --git a/src/audio/Audio.cpp b/src/audio/Audio.cpp index d547585..582f7b5 100644 --- a/src/audio/Audio.cpp +++ b/src/audio/Audio.cpp @@ -137,6 +137,11 @@ void Audio::shuffle(){ bool Audio::ensureStream(){ if(audioStream) return true; + // Ensure audio spec is initialized + if (!init()) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] Failed to initialize audio spec before opening device stream"); + return false; + } audioStream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &outSpec, &Audio::streamCallback, this); if(!audioStream){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] SDL_OpenAudioDeviceStream failed: %s", SDL_GetError()); diff --git a/src/main.cpp b/src/main.cpp index 1971586..a507aa9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -17,6 +17,8 @@ #include #include #include +#include +#include #include "audio/Audio.h" #include "audio/SoundEffect.h" @@ -40,6 +42,7 @@ #include "graphics/renderers/GameRenderer.h" #include "core/Config.h" #include "core/Settings.h" +#include "ui/MenuLayout.h" // Debug logging removed: no-op in this build (previously LOG_DEBUG) @@ -50,6 +53,7 @@ static constexpr int LOGICAL_W = 1200; static constexpr int LOGICAL_H = 1000; static constexpr int WELL_W = Game::COLS * Game::TILE; static constexpr int WELL_H = Game::ROWS * Game::TILE; +#include "ui/UIConstants.h" // Piece types now declared in Game.h @@ -76,6 +80,15 @@ static const std::array COLORS = {{ SDL_Color{255, 160, 0, 255}, // L }}; +// Global collector for asset loading errors shown on the loading screen +static std::vector g_assetLoadErrors; +static std::mutex g_assetLoadErrorsMutex; +// Loading counters for progress UI and debug overlay +static std::atomic g_totalLoadingTasks{0}; +static std::atomic g_loadedTasks{0}; +static std::string g_currentLoadingFile; +static std::mutex g_currentLoadingMutex; + static void drawRect(SDL_Renderer *r, float x, float y, float w, float h, SDL_Color c) { SDL_SetRenderDrawColor(r, c.r, c.g, c.b, c.a); @@ -89,8 +102,24 @@ static SDL_Texture* loadTextureFromImage(SDL_Renderer* renderer, const std::stri } const std::string resolvedPath = AssetPath::resolveImagePath(path); + { + std::lock_guard lk(g_currentLoadingMutex); + g_currentLoadingFile = resolvedPath.empty() ? path : resolvedPath; + } SDL_Surface* surface = IMG_Load(resolvedPath.c_str()); if (!surface) { + // Record the error for display on the loading screen + { + std::lock_guard lk(g_assetLoadErrorsMutex); + std::ostringstream ss; + ss << "Image load failed: " << path << " (" << resolvedPath << "): " << SDL_GetError(); + g_assetLoadErrors.emplace_back(ss.str()); + } + g_loadedTasks.fetch_add(1); + { + std::lock_guard lk(g_currentLoadingMutex); + g_currentLoadingFile.clear(); + } SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load image %s (resolved: %s): %s", path.c_str(), resolvedPath.c_str(), SDL_GetError()); return nullptr; } @@ -102,10 +131,26 @@ static SDL_Texture* loadTextureFromImage(SDL_Renderer* renderer, const std::stri SDL_DestroySurface(surface); if (!texture) { + { + std::lock_guard lk(g_assetLoadErrorsMutex); + std::ostringstream ss; + ss << "Texture create failed: " << resolvedPath << ": " << SDL_GetError(); + g_assetLoadErrors.emplace_back(ss.str()); + } + g_loadedTasks.fetch_add(1); + { + std::lock_guard lk(g_currentLoadingMutex); + g_currentLoadingFile.clear(); + } SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create texture from %s: %s", resolvedPath.c_str(), SDL_GetError()); return nullptr; } - + // Mark this task as completed + g_loadedTasks.fetch_add(1); + { + std::lock_guard lk(g_currentLoadingMutex); + g_currentLoadingFile.clear(); + } if (resolvedPath != path) { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loaded %s via %s", path.c_str(), resolvedPath.c_str()); } @@ -608,13 +653,9 @@ int main(int, char **) SDL_GetError()); } - // Primary UI font (Orbitron) used for major UI text: buttons, loading, HUD + // Font and UI asset handles (actual loading deferred until Loading state) FontAtlas pixelFont; - pixelFont.init(AssetPath::resolveWithBase("assets/fonts/Orbitron.ttf"), 22); - - // Secondary font (Exo2) used for longer descriptions, settings, credits FontAtlas font; - font.init(AssetPath::resolveWithBase("assets/fonts/Exo2.ttf"), 20); ScoreManager scores; std::atomic scoresLoadComplete{false}; @@ -639,136 +680,124 @@ int main(int, char **) LineEffect lineEffect; lineEffect.init(renderer); - // Load logo assets via SDL_image so we can use compressed formats - SDL_Texture* logoTex = loadTextureFromImage(renderer, "assets/images/spacetris.png"); - - // Load small logo (used by Menu to show whole logo) + // Asset handles (textures initialized by loader thread when Loading state starts) + SDL_Texture* logoTex = nullptr; int logoSmallW = 0, logoSmallH = 0; - SDL_Texture* logoSmallTex = loadTextureFromImage(renderer, "assets/images/spacetris.png", &logoSmallW, &logoSmallH); - - // Load menu background using SDL_image (prefers JPEG) - SDL_Texture* backgroundTex = loadTextureFromImage(renderer, "assets/images/main_background.bmp"); + SDL_Texture* logoSmallTex = nullptr; + SDL_Texture* backgroundTex = nullptr; // No static background texture is used + int mainScreenW = 0, mainScreenH = 0; + SDL_Texture* mainScreenTex = nullptr; - // Load the new main screen overlay that sits above the background but below buttons - int mainScreenW = 0; - int mainScreenH = 0; - SDL_Texture* mainScreenTex = loadTextureFromImage(renderer, "assets/images/main_screen.png", &mainScreenW, &mainScreenH); - if (mainScreenTex) { - SDL_SetTextureBlendMode(mainScreenTex, SDL_BLENDMODE_BLEND); - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loaded main_screen overlay %dx%d (tex=%p)", mainScreenW, mainScreenH, (void*)mainScreenTex); - FILE* f = fopen("tetris_trace.log", "a"); - if (f) { - fprintf(f, "main.cpp: loaded main_screen.bmp %dx%d tex=%p\n", mainScreenW, mainScreenH, (void*)mainScreenTex); - fclose(f); - } - } else { - SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Failed to load assets/images/main_screen.bmp (overlay will be skipped)"); - FILE* f = fopen("tetris_trace.log", "a"); - if (f) { - fprintf(f, "main.cpp: failed to load main_screen.bmp\n"); - fclose(f); - } - } - - // Note: `backgroundTex` is owned by main and passed into `StateContext::backgroundTex` below. - // States should render using `ctx.backgroundTex` rather than accessing globals. - // Level background caching system LevelBackgroundFader levelBackgrounds; // Default start level selection: 0 (declare here so it's in scope for all handlers) int startLevelSelection = 0; - - // Load blocks texture via SDL_image (falls back to procedural blocks if missing) - SDL_Texture* blocksTex = loadTextureFromImage(renderer, "assets/images/blocks90px_001.bmp"); - // No global exposure of blocksTex; states receive textures via StateContext. - - if (!blocksTex) { - // Create a 630x90 texture (7 blocks * 90px each) - blocksTex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, 630, 90); - - // Generate blocks by drawing colored rectangles to texture - SDL_SetRenderTarget(renderer, blocksTex); - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0); - SDL_RenderClear(renderer); - - for (int i = 0; i < PIECE_COUNT; ++i) { - SDL_Color c = COLORS[i + 1]; - SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a); - SDL_FRect rect{(float)(i * 90), 0, 90, 90}; - SDL_RenderFillRect(renderer, &rect); - } - - SDL_SetRenderTarget(renderer, nullptr); - } - SDL_Texture* scorePanelTex = loadTextureFromImage(renderer, "assets/images/panel_score.png"); - if (scorePanelTex) { - SDL_SetTextureBlendMode(scorePanelTex, SDL_BLENDMODE_BLEND); - } - SDL_Texture* statisticsPanelTex = loadTextureFromImage(renderer, "assets/images/statistics_panel.png"); - if (statisticsPanelTex) { - SDL_SetTextureBlendMode(statisticsPanelTex, SDL_BLENDMODE_BLEND); - } - SDL_Texture* nextPanelTex = loadTextureFromImage(renderer, "assets/images/next_panel.png"); - if (nextPanelTex) { - SDL_SetTextureBlendMode(nextPanelTex, SDL_BLENDMODE_BLEND); - } + SDL_Texture* blocksTex = nullptr; + SDL_Texture* scorePanelTex = nullptr; + SDL_Texture* statisticsPanelTex = nullptr; + SDL_Texture* nextPanelTex = nullptr; + + // Loader control: execute incrementally on main thread to avoid SDL threading issues + std::atomic_bool g_loadingStarted{false}; + std::atomic_bool g_loadingComplete{false}; + std::atomic g_loadingStep{0}; + + // Define performLoadingStep to execute one load operation per frame on main thread + auto performLoadingStep = [&]() -> bool { + size_t step = g_loadingStep.fetch_add(1); + + // Initialize counters on first step + if (step == 0) { + g_totalLoadingTasks.store(25); // Total: 2 fonts + 2 logos + 1 main + 1 blocks + 3 panels + 16 audio + g_loadedTasks.store(0); + { + std::lock_guard lk(g_assetLoadErrorsMutex); + g_assetLoadErrors.clear(); + } + { + std::lock_guard lk(g_currentLoadingMutex); + g_currentLoadingFile.clear(); + } + } + + // Execute one load operation per step + switch (step) { + case 0: return false; // Init step + case 1: pixelFont.init(AssetPath::resolveWithBase("assets/fonts/Orbitron.ttf"), 22); g_loadedTasks.fetch_add(1); break; + case 2: font.init(AssetPath::resolveWithBase("assets/fonts/Exo2.ttf"), 20); g_loadedTasks.fetch_add(1); break; + case 3: logoTex = loadTextureFromImage(renderer, "assets/images/spacetris.png"); break; + case 4: logoSmallTex = loadTextureFromImage(renderer, "assets/images/spacetris.png", &logoSmallW, &logoSmallH); break; + case 5: mainScreenTex = loadTextureFromImage(renderer, "assets/images/main_screen.png", &mainScreenW, &mainScreenH); + if (mainScreenTex) SDL_SetTextureBlendMode(mainScreenTex, SDL_BLENDMODE_BLEND); break; + case 6: + blocksTex = loadTextureFromImage(renderer, "assets/images/blocks90px_001.bmp"); + if (!blocksTex) { + blocksTex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, 630, 90); + SDL_SetRenderTarget(renderer, blocksTex); + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0); + SDL_RenderClear(renderer); + for (int i = 0; i < PIECE_COUNT; ++i) { + SDL_Color c = COLORS[i + 1]; + SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a); + SDL_FRect rect{(float)(i * 90), 0, 90, 90}; + SDL_RenderFillRect(renderer, &rect); + } + SDL_SetRenderTarget(renderer, nullptr); + g_loadedTasks.fetch_add(1); + } + break; + case 7: scorePanelTex = loadTextureFromImage(renderer, "assets/images/panel_score.png"); + if (scorePanelTex) SDL_SetTextureBlendMode(scorePanelTex, SDL_BLENDMODE_BLEND); break; + case 8: statisticsPanelTex = loadTextureFromImage(renderer, "assets/images/statistics_panel.png"); + if (statisticsPanelTex) SDL_SetTextureBlendMode(statisticsPanelTex, SDL_BLENDMODE_BLEND); break; + case 9: nextPanelTex = loadTextureFromImage(renderer, "assets/images/next_panel.png"); + if (nextPanelTex) SDL_SetTextureBlendMode(nextPanelTex, SDL_BLENDMODE_BLEND); break; + case 10: SoundEffectManager::instance().init(); g_loadedTasks.fetch_add(1); break; + + // Audio loading steps + default: { + const std::vector audioIds = {"clear_line","nice_combo","you_fire","well_played","keep_that_ryhtm","great_move","smooth_clear","impressive","triple_strike","amazing","you_re_unstoppable","boom_tetris","wonderful","lets_go","hard_drop","new_level"}; + size_t audioIdx = step - 11; + if (audioIdx < audioIds.size()) { + std::string id = audioIds[audioIdx]; + std::string basePath = "assets/music/" + (id == "hard_drop" ? "hard_drop_001" : id); + { + std::lock_guard lk(g_currentLoadingMutex); + g_currentLoadingFile = basePath; + } + std::string resolved = AssetPath::resolveWithExtensions(basePath, { ".wav", ".mp3" }); + if (!resolved.empty()) { + SoundEffectManager::instance().loadSound(id, resolved); + } + g_loadedTasks.fetch_add(1); + { + std::lock_guard lk(g_currentLoadingMutex); + g_currentLoadingFile.clear(); + } + } else { + // All done + return true; + } + break; + } + } + return false; // More steps remaining + }; Game game(startLevelSelection); // Apply global gravity speed multiplier from config game.setGravityGlobalMultiplier(Config::Gameplay::GRAVITY_SPEED_MULTIPLIER); game.reset(startLevelSelection); - // Initialize sound effects system - SoundEffectManager::instance().init(); - - auto loadAudioAsset = [](const std::string& basePath, const std::string& id) { - std::string resolved = AssetPath::resolveWithExtensions(basePath, { ".wav", ".mp3" }); - if (resolved.empty()) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Missing audio asset for %s (base %s)", id.c_str(), basePath.c_str()); - return; - } - if (!SoundEffectManager::instance().loadSound(id, resolved)) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load %s from %s", id.c_str(), resolved.c_str()); - } - }; - - loadAudioAsset("assets/music/clear_line", "clear_line"); + // Sound effects system already initialized; audio loads are handled by loader thread - // Load voice lines for line clears using WAV files (with MP3 fallback) + // Define voice line banks for gameplay callbacks std::vector singleSounds = {"well_played", "smooth_clear", "great_move"}; std::vector doubleSounds = {"nice_combo", "you_fire", "keep_that_ryhtm"}; std::vector tripleSounds = {"impressive", "triple_strike"}; std::vector tetrisSounds = {"amazing", "you_re_unstoppable", "boom_tetris", "wonderful"}; - std::vector allVoiceSounds; - auto appendVoices = [&allVoiceSounds](const std::vector& src) { - allVoiceSounds.insert(allVoiceSounds.end(), src.begin(), src.end()); - }; - appendVoices(singleSounds); - appendVoices(doubleSounds); - appendVoices(tripleSounds); - appendVoices(tetrisSounds); - - auto loadVoice = [&](const std::string& id, const std::string& baseName) { - loadAudioAsset("assets/music/" + baseName, id); - }; - - loadVoice("nice_combo", "nice_combo"); - loadVoice("you_fire", "you_fire"); - loadVoice("well_played", "well_played"); - loadVoice("keep_that_ryhtm", "keep_that_ryhtm"); - loadVoice("great_move", "great_move"); - loadVoice("smooth_clear", "smooth_clear"); - loadVoice("impressive", "impressive"); - loadVoice("triple_strike", "triple_strike"); - loadVoice("amazing", "amazing"); - loadVoice("you_re_unstoppable", "you_re_unstoppable"); - loadVoice("boom_tetris", "boom_tetris"); - loadVoice("wonderful", "wonderful"); - loadVoice("lets_go", "lets_go"); - loadVoice("hard_drop", "hard_drop_001"); - loadVoice("new_level", "new_level"); bool suppressLineVoiceForLevelUp = false; @@ -859,7 +888,7 @@ int main(int, char **) ctx.logoSmallTex = logoSmallTex; ctx.logoSmallW = logoSmallW; ctx.logoSmallH = logoSmallH; - ctx.backgroundTex = backgroundTex; + ctx.backgroundTex = nullptr; ctx.blocksTex = blocksTex; ctx.scorePanelTex = scorePanelTex; ctx.statisticsPanelTex = statisticsPanelTex; @@ -957,7 +986,7 @@ int main(int, char **) // Register handlers and lifecycle hooks stateMgr.registerHandler(AppState::Loading, [&](const SDL_Event& e){ loadingState->handleEvent(e); }); - stateMgr.registerOnEnter(AppState::Loading, [&](){ loadingState->onEnter(); }); + stateMgr.registerOnEnter(AppState::Loading, [&](){ loadingState->onEnter(); g_loadingStarted.store(true); }); stateMgr.registerOnExit(AppState::Loading, [&](){ loadingState->onExit(); }); stateMgr.registerHandler(AppState::Menu, [&](const SDL_Event& e){ menuState->handleEvent(e); }); @@ -980,6 +1009,10 @@ int main(int, char **) stateMgr.registerOnEnter(AppState::Playing, [&](){ playingState->onEnter(); }); stateMgr.registerOnExit(AppState::Playing, [&](){ playingState->onExit(); }); + // Manually trigger the initial Loading state's onEnter + loadingState->onEnter(); + g_loadingStarted.store(true); + // Playing, LevelSelect and GameOver currently use inline logic in main; we'll migrate later while (running) { @@ -1182,16 +1215,15 @@ int main(int, char **) showSettingsPopup = false; } else { // Responsive Main menu buttons (match MenuState layout) - bool isSmall = ((LOGICAL_W * logicalScale) < 700.0f); - float btnW = isSmall ? (LOGICAL_W * 0.4f) : 300.0f; - float btnH = isSmall ? 60.0f : 70.0f; + bool isSmall = ((LOGICAL_W * logicalScale) < MENU_SMALL_THRESHOLD); + float btnW = isSmall ? (LOGICAL_W * MENU_BTN_WIDTH_SMALL_FACTOR) : MENU_BTN_WIDTH_LARGE; + float btnH = isSmall ? MENU_BTN_HEIGHT_SMALL : MENU_BTN_HEIGHT_LARGE; float btnCX = LOGICAL_W * 0.5f + contentOffsetX; - const float btnYOffset = 40.0f; // must match MenuState offset - float btnCY = LOGICAL_H * 0.86f + contentOffsetY + btnYOffset; - float spacing = isSmall ? btnW * 1.15f : btnW * 1.05f; - std::array buttonRects{}; - for (int i = 0; i < 5; ++i) { - float center = btnCX + (static_cast(i) - 2.0f) * spacing; + float btnCY = LOGICAL_H * 0.86f + contentOffsetY + MENU_BTN_Y_OFFSET; + float spacing = isSmall ? btnW * MENU_BTN_SPACING_FACTOR_SMALL : btnW * MENU_BTN_SPACING_FACTOR_LARGE; + std::array buttonRects{}; + for (int i = 0; i < MENU_BTN_COUNT; ++i) { + float center = btnCX + (static_cast(i) - MENU_BTN_CENTER) * spacing; buttonRects[i] = SDL_FRect{center - btnW / 2.0f, btnCY - btnH / 2.0f, btnW, btnH}; } @@ -1214,7 +1246,7 @@ int main(int, char **) } // Settings button (gear icon area - top right) - SDL_FRect settingsBtn{LOGICAL_W - 60, 10, 50, 30}; + SDL_FRect settingsBtn{SETTINGS_BTN_X, SETTINGS_BTN_Y, SETTINGS_BTN_W, SETTINGS_BTN_H}; if (lx >= settingsBtn.x && lx <= settingsBtn.x + settingsBtn.w && ly >= settingsBtn.y && ly <= settingsBtn.y + settingsBtn.h) { showSettingsPopup = true; @@ -1310,22 +1342,8 @@ int main(int, char **) float contentH = LOGICAL_H * logicalScale; float contentOffsetX = (winW - contentW) * 0.5f / logicalScale; float contentOffsetY = (winH - contentH) * 0.5f / logicalScale; - bool isSmall = ((LOGICAL_W * logicalScale) < 700.0f); - float btnW = isSmall ? (LOGICAL_W * 0.4f) : 300.0f; - float btnH = isSmall ? 60.0f : 70.0f; - float btnCX = LOGICAL_W * 0.5f + contentOffsetX; - const float btnYOffset = 40.0f; // must match MenuState offset - float btnCY = LOGICAL_H * 0.86f + contentOffsetY + btnYOffset; - float spacing = isSmall ? btnW * 1.15f : btnW * 1.05f; - hoveredButton = -1; - for (int i = 0; i < 4; ++i) { - float center = btnCX + (static_cast(i) - 1.5f) * spacing; - SDL_FRect rect{center - btnW / 2.0f, btnCY - btnH / 2.0f, btnW, btnH}; - if (lx >= rect.x && lx <= rect.x + rect.w && ly >= rect.y && ly <= rect.y + rect.h) { - hoveredButton = i; - break; - } - } + ui::MenuLayoutParams params{ LOGICAL_W, LOGICAL_H, winW, winH, logicalScale }; + hoveredButton = ui::hitTestMenuButtons(params, lx, ly); } } } @@ -1413,35 +1431,11 @@ int main(int, char **) } else if (state == AppState::Loading) { - // Initialize audio system and start background loading on first frame - if (!musicLoaded && currentTrackLoading == 0) { - Audio::instance().init(); - // Apply audio settings - Audio::instance().setMuted(!Settings::instance().isMusicEnabled()); - // Note: SoundEffectManager doesn't have a global mute yet, but we can add it or handle it in playSound - - // Count actual music files first - totalTracks = 0; - std::vector trackPaths; - trackPaths.reserve(100); - for (int i = 1; i <= 100; ++i) { - char base[64]; - std::snprintf(base, sizeof(base), "assets/music/music%03d", i); - std::string path = AssetPath::resolveWithExtensions(base, { ".mp3" }); - if (path.empty()) { - break; - } - trackPaths.push_back(path); + // Execute one loading step per frame on main thread + if (g_loadingStarted.load() && !g_loadingComplete.load()) { + if (performLoadingStep()) { + g_loadingComplete.store(true); } - totalTracks = static_cast(trackPaths.size()); - - for (const auto& track : trackPaths) { - Audio::instance().addTrackAsync(track); - } - - // Start background loading thread - Audio::instance().startBackgroundLoading(); - currentTrackLoading = 1; // Mark as started } // Update progress based on background loading @@ -1454,34 +1448,44 @@ int main(int, char **) } } - // Calculate comprehensive loading progress - // Phase 1: Initial assets (textures, fonts) - 20% - double assetProgress = 0.2; // Assets are loaded at startup - - // Phase 2: Music loading - 70% - double musicProgress = 0.0; - if (totalTracks > 0) { - musicProgress = musicLoaded ? 0.7 : std::min(0.7, (double)currentTrackLoading / totalTracks * 0.7); - } - - // Phase 3: Final initialization - 10% - double timeProgress = std::min(0.1, (now - loadStart) / 500.0); // Faster final phase - - loadingProgress = assetProgress + musicProgress + timeProgress; - - // Ensure we never exceed 100% and reach exactly 100% when everything is loaded - loadingProgress = std::min(1.0, loadingProgress); - - // Fix floating point precision issues (0.2 + 0.7 + 0.1 can be 0.9999...) - if (loadingProgress > 0.99) loadingProgress = 1.0; - - if (musicLoaded && timeProgress >= 0.1) { - loadingProgress = 1.0; - } - - if (loadingProgress >= 1.0 && musicLoaded) { - state = AppState::Menu; - stateMgr.setState(state); + // Prefer task-based progress if we have tasks registered + int totalTasks = g_totalLoadingTasks.load(std::memory_order_acquire); + int doneTasks = g_loadedTasks.load(std::memory_order_acquire); + if (totalTasks > 0) { + loadingProgress = std::min(1.0, double(doneTasks) / double(totalTasks)); + if (loadingProgress >= 1.0) { + state = AppState::Menu; + stateMgr.setState(state); + } + } else { + // Fallback: time + audio heuristics (legacy behavior) + double assetProgress = 0.2; + double musicProgress = 0.0; + if (totalTracks > 0) { + musicProgress = musicLoaded ? 0.7 : std::min(0.7, (double)currentTrackLoading / totalTracks * 0.7); + } else { + if (Audio::instance().isLoadingComplete()) { + musicProgress = 0.7; + } else if (Audio::instance().getLoadedTrackCount() > 0) { + musicProgress = 0.35; + } else { + Uint32 elapsedMs = SDL_GetTicks() - static_cast(loadStart); + if (elapsedMs > 1500) { + musicProgress = 0.7; + musicLoaded = true; + } else { + musicProgress = 0.0; + } + } + } + double timeProgress = std::min(0.1, (now - loadStart) / 500.0); + loadingProgress = std::min(1.0, assetProgress + musicProgress + timeProgress); + if (loadingProgress > 0.99) loadingProgress = 1.0; + if (musicLoaded && timeProgress >= 0.1) loadingProgress = 1.0; + if (loadingProgress >= 1.0 && musicLoaded) { + state = AppState::Menu; + stateMgr.setState(state); + } } } if (state == AppState::Menu || state == AppState::Playing) @@ -1570,6 +1574,20 @@ int main(int, char **) break; } + // Keep context asset pointers in sync with assets loaded by the loader thread + ctx.logoTex = logoTex; + ctx.logoSmallTex = logoSmallTex; + ctx.logoSmallW = logoSmallW; + ctx.logoSmallH = logoSmallH; + ctx.backgroundTex = backgroundTex; + ctx.blocksTex = blocksTex; + ctx.scorePanelTex = scorePanelTex; + ctx.statisticsPanelTex = statisticsPanelTex; + ctx.nextPanelTex = nextPanelTex; + ctx.mainScreenTex = mainScreenTex; + ctx.mainScreenW = mainScreenW; + ctx.mainScreenH = mainScreenH; + if (menuFadePhase == MenuFadePhase::FadeOut) { menuFadeClockMs += frameMs; menuFadeAlpha = std::min(1.0f, float(menuFadeClockMs / MENU_PLAY_FADE_DURATION_MS)); @@ -1655,10 +1673,7 @@ int main(int, char **) // `mainScreenTex` is rendered as a top layer just before presenting // so we don't draw it here. Keep the space warp background only. } else if (state == AppState::LevelSelector || state == AppState::Options) { - if (backgroundTex) { - SDL_FRect fullRect = { 0, 0, (float)winW, (float)winH }; - SDL_RenderTexture(renderer, backgroundTex, nullptr, &fullRect); - } + // No static background texture to draw (background image removed). } else { // Use regular starfield for other states (not gameplay) starfield.draw(renderer); @@ -1757,6 +1772,66 @@ int main(int, char **) float percentWidth = strlen(percentText) * 12.0f; // Approximate width for pixel font float percentX = (LOGICAL_W - percentWidth) / 2.0f; pixelFont.draw(renderer, percentX + contentOffsetX, currentY + contentOffsetY, percentText, 1.5f, {255, 204, 0, 255}); + + // If any asset/audio errors occurred during startup, display recent ones in red + { + std::lock_guard lk(g_assetLoadErrorsMutex); + const int maxShow = 5; + int count = static_cast(g_assetLoadErrors.size()); + if (count > 0) { + int start = std::max(0, count - maxShow); + float errY = currentY + spacingBetweenElements + 8.0f; + + // Also make a visible window title change so users notice missing assets + std::string latest = g_assetLoadErrors.back(); + std::string shortTitle = "Tetris - Missing assets"; + if (!latest.empty()) { + std::string trimmed = latest; + if (trimmed.size() > 48) trimmed = trimmed.substr(0, 45) + "..."; + shortTitle += ": "; + shortTitle += trimmed; + } + SDL_SetWindowTitle(window, shortTitle.c_str()); + + // Also append a trace log entry for visibility outside the SDL window + FILE* tf = fopen("tetris_trace.log", "a"); + if (tf) { + fprintf(tf, "Loading error: %s\n", g_assetLoadErrors.back().c_str()); + fclose(tf); + } + + for (int i = start; i < count; ++i) { + const std::string& msg = g_assetLoadErrors[i]; + // Truncate long messages to fit reasonably + std::string display = msg; + if (display.size() > 80) display = display.substr(0, 77) + "..."; + pixelFont.draw(renderer, 80 + contentOffsetX, errY + contentOffsetY, display.c_str(), 0.85f, {255, 100, 100, 255}); + errY += 20.0f; + } + } + } + + // Debug overlay: show current loading file and counters when enabled in settings + if (Settings::instance().isDebugEnabled()) { + std::string cur; + { + std::lock_guard lk(g_currentLoadingMutex); + cur = g_currentLoadingFile; + } + char buf[128]; + int loaded = g_loadedTasks.load(); + int total = g_totalLoadingTasks.load(); + std::snprintf(buf, sizeof(buf), "Loaded: %d / %d", loaded, total); + float debugX = 20.0f + contentOffsetX; + float debugY = LOGICAL_H - 48.0f + contentOffsetY; + pixelFont.draw(renderer, debugX, debugY, buf, 0.9f, SDL_Color{200,200,200,255}); + if (!cur.empty()) { + std::string display = "Loading: "; + display += cur; + if (display.size() > 80) display = display.substr(0,77) + "..."; + pixelFont.draw(renderer, debugX, debugY + 18.0f, display.c_str(), 0.85f, SDL_Color{200,180,120,255}); + } + } } break; case AppState::Menu: @@ -2030,8 +2105,6 @@ int main(int, char **) } if (logoTex) SDL_DestroyTexture(logoTex); - if (backgroundTex) - SDL_DestroyTexture(backgroundTex); if (mainScreenTex) SDL_DestroyTexture(mainScreenTex); resetLevelBackgrounds(levelBackgrounds); diff --git a/src/ui/MenuLayout.cpp b/src/ui/MenuLayout.cpp new file mode 100644 index 0000000..83bc594 --- /dev/null +++ b/src/ui/MenuLayout.cpp @@ -0,0 +1,41 @@ +#include "ui/MenuLayout.h" +#include "ui/UIConstants.h" +#include +#include + +namespace ui { + +std::array computeMenuButtonRects(const MenuLayoutParams& p) { + const float LOGICAL_W = static_cast(p.logicalW); + const float LOGICAL_H = static_cast(p.logicalH); + bool isSmall = ((LOGICAL_W * p.logicalScale) < MENU_SMALL_THRESHOLD); + float btnW = isSmall ? (LOGICAL_W * MENU_BTN_WIDTH_SMALL_FACTOR) : MENU_BTN_WIDTH_LARGE; + float btnH = isSmall ? MENU_BTN_HEIGHT_SMALL : MENU_BTN_HEIGHT_LARGE; + float contentOffsetX = (p.winW - LOGICAL_W * p.logicalScale) * 0.5f / p.logicalScale; + float contentOffsetY = (p.winH - LOGICAL_H * p.logicalScale) * 0.5f / p.logicalScale; + float btnCX = LOGICAL_W * 0.5f + contentOffsetX; + float btnCY = LOGICAL_H * 0.86f + contentOffsetY + MENU_BTN_Y_OFFSET; + float spacing = isSmall ? btnW * MENU_BTN_SPACING_FACTOR_SMALL : btnW * MENU_BTN_SPACING_FACTOR_LARGE; + std::array rects{}; + for (int i = 0; i < MENU_BTN_COUNT; ++i) { + float center = btnCX + (static_cast(i) - MENU_BTN_CENTER) * spacing; + rects[i] = SDL_FRect{center - btnW / 2.0f, btnCY - btnH / 2.0f, btnW, btnH}; + } + return rects; +} + +int hitTestMenuButtons(const MenuLayoutParams& p, float localX, float localY) { + auto rects = computeMenuButtonRects(p); + for (int i = 0; i < MENU_BTN_COUNT; ++i) { + const auto &r = rects[i]; + if (localX >= r.x && localX <= r.x + r.w && localY >= r.y && localY <= r.y + r.h) + return i; + } + return -1; +} + +SDL_FRect settingsButtonRect(const MenuLayoutParams& p) { + return SDL_FRect{SETTINGS_BTN_X, SETTINGS_BTN_Y, SETTINGS_BTN_W, SETTINGS_BTN_H}; +} + +} // namespace ui diff --git a/src/ui/MenuLayout.h b/src/ui/MenuLayout.h new file mode 100644 index 0000000..d185860 --- /dev/null +++ b/src/ui/MenuLayout.h @@ -0,0 +1,26 @@ +#pragma once +#include +#include "ui/UIConstants.h" +#include + +namespace ui { + +struct MenuLayoutParams { + int logicalW; + int logicalH; + int winW; + int winH; + float logicalScale; +}; + +// Compute menu button rects in logical coordinates (content-local) +std::array computeMenuButtonRects(const MenuLayoutParams& p); + +// Hit test a point given in logical content-local coordinates against menu buttons +// Returns index 0..4 or -1 if none +int hitTestMenuButtons(const MenuLayoutParams& p, float localX, float localY); + +// Return settings button rect (logical coords) +SDL_FRect settingsButtonRect(const MenuLayoutParams& p); + +} // namespace ui diff --git a/src/ui/UIConstants.h b/src/ui/UIConstants.h new file mode 100644 index 0000000..27c6d8e --- /dev/null +++ b/src/ui/UIConstants.h @@ -0,0 +1,18 @@ +#pragma once + +static constexpr int MENU_BTN_COUNT = 5; +static constexpr float MENU_SMALL_THRESHOLD = 700.0f; +static constexpr float MENU_BTN_WIDTH_LARGE = 300.0f; +static constexpr float MENU_BTN_WIDTH_SMALL_FACTOR = 0.4f; // multiplied by LOGICAL_W +static constexpr float MENU_BTN_HEIGHT_LARGE = 70.0f; +static constexpr float MENU_BTN_HEIGHT_SMALL = 60.0f; +static constexpr float MENU_BTN_Y_OFFSET = 40.0f; // matches MenuState offset +static constexpr float MENU_BTN_SPACING_FACTOR_SMALL = 1.15f; +static constexpr float MENU_BTN_SPACING_FACTOR_LARGE = 1.05f; +static constexpr float MENU_BTN_CENTER = (MENU_BTN_COUNT - 1) / 2.0f; +// Settings button metrics +static constexpr float SETTINGS_BTN_OFFSET_X = 60.0f; +static constexpr float SETTINGS_BTN_X = 1200 - SETTINGS_BTN_OFFSET_X; // LOGICAL_W is 1200 +static constexpr float SETTINGS_BTN_Y = 10.0f; +static constexpr float SETTINGS_BTN_W = 50.0f; +static constexpr float SETTINGS_BTN_H = 30.0f;