fixed progress bar

This commit is contained in:
2025-12-16 12:09:33 +01:00
parent ec086b2cd4
commit 81586aa768
7 changed files with 372 additions and 208 deletions

View File

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

View File

@ -17,6 +17,8 @@
#include <filesystem>
#include <thread>
#include <atomic>
#include <mutex>
#include <sstream>
#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<SDL_Color, PIECE_COUNT + 1> COLORS = {{
SDL_Color{255, 160, 0, 255}, // L
}};
// Global collector for asset loading errors shown on the loading screen
static std::vector<std::string> g_assetLoadErrors;
static std::mutex g_assetLoadErrorsMutex;
// Loading counters for progress UI and debug overlay
static std::atomic<int> g_totalLoadingTasks{0};
static std::atomic<int> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<bool> 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<size_t> 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<std::mutex> lk(g_assetLoadErrorsMutex);
g_assetLoadErrors.clear();
}
{
std::lock_guard<std::mutex> 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<std::string> 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<std::mutex> 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<std::mutex> 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<std::string> singleSounds = {"well_played", "smooth_clear", "great_move"};
std::vector<std::string> doubleSounds = {"nice_combo", "you_fire", "keep_that_ryhtm"};
std::vector<std::string> tripleSounds = {"impressive", "triple_strike"};
std::vector<std::string> tetrisSounds = {"amazing", "you_re_unstoppable", "boom_tetris", "wonderful"};
std::vector<std::string> allVoiceSounds;
auto appendVoices = [&allVoiceSounds](const std::vector<std::string>& 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<SDL_FRect, 5> buttonRects{};
for (int i = 0; i < 5; ++i) {
float center = btnCX + (static_cast<float>(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<SDL_FRect, MENU_BTN_COUNT> buttonRects{};
for (int i = 0; i < MENU_BTN_COUNT; ++i) {
float center = btnCX + (static_cast<float>(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<float>(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<std::string> 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<int>(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<Uint32>(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<std::mutex> lk(g_assetLoadErrorsMutex);
const int maxShow = 5;
int count = static_cast<int>(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<std::mutex> 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);

41
src/ui/MenuLayout.cpp Normal file
View File

@ -0,0 +1,41 @@
#include "ui/MenuLayout.h"
#include "ui/UIConstants.h"
#include <cmath>
#include <array>
namespace ui {
std::array<SDL_FRect, 5> computeMenuButtonRects(const MenuLayoutParams& p) {
const float LOGICAL_W = static_cast<float>(p.logicalW);
const float LOGICAL_H = static_cast<float>(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<SDL_FRect, MENU_BTN_COUNT> rects{};
for (int i = 0; i < MENU_BTN_COUNT; ++i) {
float center = btnCX + (static_cast<float>(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

26
src/ui/MenuLayout.h Normal file
View File

@ -0,0 +1,26 @@
#pragma once
#include <array>
#include "ui/UIConstants.h"
#include <SDL3/SDL.h>
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<SDL_FRect, MENU_BTN_COUNT> 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

18
src/ui/UIConstants.h Normal file
View File

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