fixed progress bar
This commit is contained in:
@ -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
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
Fullscreen=1
|
||||
|
||||
[Audio]
|
||||
Music=1
|
||||
Music=0
|
||||
Sound=1
|
||||
|
||||
[Gameplay]
|
||||
|
||||
@ -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());
|
||||
|
||||
435
src/main.cpp
435
src/main.cpp
@ -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,39 +680,13 @@ 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");
|
||||
|
||||
// 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.
|
||||
SDL_Texture* logoSmallTex = nullptr;
|
||||
SDL_Texture* backgroundTex = nullptr; // No static background texture is used
|
||||
int mainScreenW = 0, mainScreenH = 0;
|
||||
SDL_Texture* mainScreenTex = nullptr;
|
||||
|
||||
// Level background caching system
|
||||
LevelBackgroundFader levelBackgrounds;
|
||||
@ -679,96 +694,110 @@ int main(int, char **)
|
||||
// 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.
|
||||
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) {
|
||||
// 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);
|
||||
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;
|
||||
|
||||
SDL_Texture* scorePanelTex = loadTextureFromImage(renderer, "assets/images/panel_score.png");
|
||||
if (scorePanelTex) {
|
||||
SDL_SetTextureBlendMode(scorePanelTex, SDL_BLENDMODE_BLEND);
|
||||
// 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;
|
||||
}
|
||||
SDL_Texture* statisticsPanelTex = loadTextureFromImage(renderer, "assets/images/statistics_panel.png");
|
||||
if (statisticsPanelTex) {
|
||||
SDL_SetTextureBlendMode(statisticsPanelTex, SDL_BLENDMODE_BLEND);
|
||||
std::string resolved = AssetPath::resolveWithExtensions(basePath, { ".wav", ".mp3" });
|
||||
if (!resolved.empty()) {
|
||||
SoundEffectManager::instance().loadSound(id, resolved);
|
||||
}
|
||||
SDL_Texture* nextPanelTex = loadTextureFromImage(renderer, "assets/images/next_panel.png");
|
||||
if (nextPanelTex) {
|
||||
SDL_SetTextureBlendMode(nextPanelTex, SDL_BLENDMODE_BLEND);
|
||||
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();
|
||||
// Sound effects system already initialized; audio loads are handled by loader thread
|
||||
|
||||
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");
|
||||
|
||||
// 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;
|
||||
// Execute one loading step per frame on main thread
|
||||
if (g_loadingStarted.load() && !g_loadingComplete.load()) {
|
||||
if (performLoadingStep()) {
|
||||
g_loadingComplete.store(true);
|
||||
}
|
||||
trackPaths.push_back(path);
|
||||
}
|
||||
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,36 +1448,46 @@ 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%
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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...)
|
||||
}
|
||||
}
|
||||
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 (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)
|
||||
{
|
||||
if (!musicStarted && musicLoaded)
|
||||
@ -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
41
src/ui/MenuLayout.cpp
Normal 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
26
src/ui/MenuLayout.h
Normal 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
18
src/ui/UIConstants.h
Normal 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;
|
||||
Reference in New Issue
Block a user