fixed progress bar
This commit is contained in:
@ -52,6 +52,7 @@ set(TETRIS_SOURCES
|
|||||||
src/audio/Audio.cpp
|
src/audio/Audio.cpp
|
||||||
src/gameplay/effects/LineEffect.cpp
|
src/gameplay/effects/LineEffect.cpp
|
||||||
src/audio/SoundEffect.cpp
|
src/audio/SoundEffect.cpp
|
||||||
|
src/ui/MenuLayout.cpp
|
||||||
# State implementations (new)
|
# State implementations (new)
|
||||||
src/states/LoadingState.cpp
|
src/states/LoadingState.cpp
|
||||||
src/states/MenuState.cpp
|
src/states/MenuState.cpp
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
Fullscreen=1
|
Fullscreen=1
|
||||||
|
|
||||||
[Audio]
|
[Audio]
|
||||||
Music=1
|
Music=0
|
||||||
Sound=1
|
Sound=1
|
||||||
|
|
||||||
[Gameplay]
|
[Gameplay]
|
||||||
|
|||||||
@ -137,6 +137,11 @@ void Audio::shuffle(){
|
|||||||
|
|
||||||
bool Audio::ensureStream(){
|
bool Audio::ensureStream(){
|
||||||
if(audioStream) return true;
|
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);
|
audioStream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &outSpec, &Audio::streamCallback, this);
|
||||||
if(!audioStream){
|
if(!audioStream){
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] SDL_OpenAudioDeviceStream failed: %s", SDL_GetError());
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] SDL_OpenAudioDeviceStream failed: %s", SDL_GetError());
|
||||||
|
|||||||
487
src/main.cpp
487
src/main.cpp
@ -17,6 +17,8 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <mutex>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
#include "audio/Audio.h"
|
#include "audio/Audio.h"
|
||||||
#include "audio/SoundEffect.h"
|
#include "audio/SoundEffect.h"
|
||||||
@ -40,6 +42,7 @@
|
|||||||
#include "graphics/renderers/GameRenderer.h"
|
#include "graphics/renderers/GameRenderer.h"
|
||||||
#include "core/Config.h"
|
#include "core/Config.h"
|
||||||
#include "core/Settings.h"
|
#include "core/Settings.h"
|
||||||
|
#include "ui/MenuLayout.h"
|
||||||
|
|
||||||
// Debug logging removed: no-op in this build (previously LOG_DEBUG)
|
// 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 LOGICAL_H = 1000;
|
||||||
static constexpr int WELL_W = Game::COLS * Game::TILE;
|
static constexpr int WELL_W = Game::COLS * Game::TILE;
|
||||||
static constexpr int WELL_H = Game::ROWS * Game::TILE;
|
static constexpr int WELL_H = Game::ROWS * Game::TILE;
|
||||||
|
#include "ui/UIConstants.h"
|
||||||
|
|
||||||
// Piece types now declared in Game.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
|
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)
|
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);
|
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);
|
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());
|
SDL_Surface* surface = IMG_Load(resolvedPath.c_str());
|
||||||
if (!surface) {
|
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());
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load image %s (resolved: %s): %s", path.c_str(), resolvedPath.c_str(), SDL_GetError());
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@ -102,10 +131,26 @@ static SDL_Texture* loadTextureFromImage(SDL_Renderer* renderer, const std::stri
|
|||||||
SDL_DestroySurface(surface);
|
SDL_DestroySurface(surface);
|
||||||
|
|
||||||
if (!texture) {
|
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());
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create texture from %s: %s", resolvedPath.c_str(), SDL_GetError());
|
||||||
return nullptr;
|
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) {
|
if (resolvedPath != path) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loaded %s via %s", path.c_str(), resolvedPath.c_str());
|
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());
|
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;
|
FontAtlas pixelFont;
|
||||||
pixelFont.init(AssetPath::resolveWithBase("assets/fonts/Orbitron.ttf"), 22);
|
|
||||||
|
|
||||||
// Secondary font (Exo2) used for longer descriptions, settings, credits
|
|
||||||
FontAtlas font;
|
FontAtlas font;
|
||||||
font.init(AssetPath::resolveWithBase("assets/fonts/Exo2.ttf"), 20);
|
|
||||||
|
|
||||||
ScoreManager scores;
|
ScoreManager scores;
|
||||||
std::atomic<bool> scoresLoadComplete{false};
|
std::atomic<bool> scoresLoadComplete{false};
|
||||||
@ -639,136 +680,124 @@ int main(int, char **)
|
|||||||
LineEffect lineEffect;
|
LineEffect lineEffect;
|
||||||
lineEffect.init(renderer);
|
lineEffect.init(renderer);
|
||||||
|
|
||||||
// Load logo assets via SDL_image so we can use compressed formats
|
// Asset handles (textures initialized by loader thread when Loading state starts)
|
||||||
SDL_Texture* logoTex = loadTextureFromImage(renderer, "assets/images/spacetris.png");
|
SDL_Texture* logoTex = nullptr;
|
||||||
|
|
||||||
// Load small logo (used by Menu to show whole logo)
|
|
||||||
int logoSmallW = 0, logoSmallH = 0;
|
int logoSmallW = 0, logoSmallH = 0;
|
||||||
SDL_Texture* logoSmallTex = loadTextureFromImage(renderer, "assets/images/spacetris.png", &logoSmallW, &logoSmallH);
|
SDL_Texture* logoSmallTex = nullptr;
|
||||||
|
SDL_Texture* backgroundTex = nullptr; // No static background texture is used
|
||||||
// Load menu background using SDL_image (prefers JPEG)
|
int mainScreenW = 0, mainScreenH = 0;
|
||||||
SDL_Texture* backgroundTex = loadTextureFromImage(renderer, "assets/images/main_background.bmp");
|
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
|
// Level background caching system
|
||||||
LevelBackgroundFader levelBackgrounds;
|
LevelBackgroundFader levelBackgrounds;
|
||||||
|
|
||||||
// Default start level selection: 0 (declare here so it's in scope for all handlers)
|
// Default start level selection: 0 (declare here so it's in scope for all handlers)
|
||||||
int startLevelSelection = 0;
|
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");
|
SDL_Texture* blocksTex = nullptr;
|
||||||
if (scorePanelTex) {
|
SDL_Texture* scorePanelTex = nullptr;
|
||||||
SDL_SetTextureBlendMode(scorePanelTex, SDL_BLENDMODE_BLEND);
|
SDL_Texture* statisticsPanelTex = nullptr;
|
||||||
}
|
SDL_Texture* nextPanelTex = nullptr;
|
||||||
SDL_Texture* statisticsPanelTex = loadTextureFromImage(renderer, "assets/images/statistics_panel.png");
|
|
||||||
if (statisticsPanelTex) {
|
// Loader control: execute incrementally on main thread to avoid SDL threading issues
|
||||||
SDL_SetTextureBlendMode(statisticsPanelTex, SDL_BLENDMODE_BLEND);
|
std::atomic_bool g_loadingStarted{false};
|
||||||
}
|
std::atomic_bool g_loadingComplete{false};
|
||||||
SDL_Texture* nextPanelTex = loadTextureFromImage(renderer, "assets/images/next_panel.png");
|
std::atomic<size_t> g_loadingStep{0};
|
||||||
if (nextPanelTex) {
|
|
||||||
SDL_SetTextureBlendMode(nextPanelTex, SDL_BLENDMODE_BLEND);
|
// 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);
|
Game game(startLevelSelection);
|
||||||
// Apply global gravity speed multiplier from config
|
// Apply global gravity speed multiplier from config
|
||||||
game.setGravityGlobalMultiplier(Config::Gameplay::GRAVITY_SPEED_MULTIPLIER);
|
game.setGravityGlobalMultiplier(Config::Gameplay::GRAVITY_SPEED_MULTIPLIER);
|
||||||
game.reset(startLevelSelection);
|
game.reset(startLevelSelection);
|
||||||
|
|
||||||
// Initialize sound effects system
|
// Sound effects system already initialized; audio loads are handled by loader thread
|
||||||
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");
|
|
||||||
|
|
||||||
// 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> singleSounds = {"well_played", "smooth_clear", "great_move"};
|
||||||
std::vector<std::string> doubleSounds = {"nice_combo", "you_fire", "keep_that_ryhtm"};
|
std::vector<std::string> doubleSounds = {"nice_combo", "you_fire", "keep_that_ryhtm"};
|
||||||
std::vector<std::string> tripleSounds = {"impressive", "triple_strike"};
|
std::vector<std::string> tripleSounds = {"impressive", "triple_strike"};
|
||||||
std::vector<std::string> tetrisSounds = {"amazing", "you_re_unstoppable", "boom_tetris", "wonderful"};
|
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;
|
bool suppressLineVoiceForLevelUp = false;
|
||||||
|
|
||||||
@ -859,7 +888,7 @@ int main(int, char **)
|
|||||||
ctx.logoSmallTex = logoSmallTex;
|
ctx.logoSmallTex = logoSmallTex;
|
||||||
ctx.logoSmallW = logoSmallW;
|
ctx.logoSmallW = logoSmallW;
|
||||||
ctx.logoSmallH = logoSmallH;
|
ctx.logoSmallH = logoSmallH;
|
||||||
ctx.backgroundTex = backgroundTex;
|
ctx.backgroundTex = nullptr;
|
||||||
ctx.blocksTex = blocksTex;
|
ctx.blocksTex = blocksTex;
|
||||||
ctx.scorePanelTex = scorePanelTex;
|
ctx.scorePanelTex = scorePanelTex;
|
||||||
ctx.statisticsPanelTex = statisticsPanelTex;
|
ctx.statisticsPanelTex = statisticsPanelTex;
|
||||||
@ -957,7 +986,7 @@ int main(int, char **)
|
|||||||
|
|
||||||
// Register handlers and lifecycle hooks
|
// Register handlers and lifecycle hooks
|
||||||
stateMgr.registerHandler(AppState::Loading, [&](const SDL_Event& e){ loadingState->handleEvent(e); });
|
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.registerOnExit(AppState::Loading, [&](){ loadingState->onExit(); });
|
||||||
|
|
||||||
stateMgr.registerHandler(AppState::Menu, [&](const SDL_Event& e){ menuState->handleEvent(e); });
|
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.registerOnEnter(AppState::Playing, [&](){ playingState->onEnter(); });
|
||||||
stateMgr.registerOnExit(AppState::Playing, [&](){ playingState->onExit(); });
|
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
|
// Playing, LevelSelect and GameOver currently use inline logic in main; we'll migrate later
|
||||||
while (running)
|
while (running)
|
||||||
{
|
{
|
||||||
@ -1182,16 +1215,15 @@ int main(int, char **)
|
|||||||
showSettingsPopup = false;
|
showSettingsPopup = false;
|
||||||
} else {
|
} else {
|
||||||
// Responsive Main menu buttons (match MenuState layout)
|
// Responsive Main menu buttons (match MenuState layout)
|
||||||
bool isSmall = ((LOGICAL_W * logicalScale) < 700.0f);
|
bool isSmall = ((LOGICAL_W * logicalScale) < MENU_SMALL_THRESHOLD);
|
||||||
float btnW = isSmall ? (LOGICAL_W * 0.4f) : 300.0f;
|
float btnW = isSmall ? (LOGICAL_W * MENU_BTN_WIDTH_SMALL_FACTOR) : MENU_BTN_WIDTH_LARGE;
|
||||||
float btnH = isSmall ? 60.0f : 70.0f;
|
float btnH = isSmall ? MENU_BTN_HEIGHT_SMALL : MENU_BTN_HEIGHT_LARGE;
|
||||||
float btnCX = LOGICAL_W * 0.5f + contentOffsetX;
|
float btnCX = LOGICAL_W * 0.5f + contentOffsetX;
|
||||||
const float btnYOffset = 40.0f; // must match MenuState offset
|
float btnCY = LOGICAL_H * 0.86f + contentOffsetY + MENU_BTN_Y_OFFSET;
|
||||||
float btnCY = LOGICAL_H * 0.86f + contentOffsetY + btnYOffset;
|
float spacing = isSmall ? btnW * MENU_BTN_SPACING_FACTOR_SMALL : btnW * MENU_BTN_SPACING_FACTOR_LARGE;
|
||||||
float spacing = isSmall ? btnW * 1.15f : btnW * 1.05f;
|
std::array<SDL_FRect, MENU_BTN_COUNT> buttonRects{};
|
||||||
std::array<SDL_FRect, 5> buttonRects{};
|
for (int i = 0; i < MENU_BTN_COUNT; ++i) {
|
||||||
for (int i = 0; i < 5; ++i) {
|
float center = btnCX + (static_cast<float>(i) - MENU_BTN_CENTER) * spacing;
|
||||||
float center = btnCX + (static_cast<float>(i) - 2.0f) * spacing;
|
|
||||||
buttonRects[i] = SDL_FRect{center - btnW / 2.0f, btnCY - btnH / 2.0f, btnW, btnH};
|
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)
|
// 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)
|
if (lx >= settingsBtn.x && lx <= settingsBtn.x + settingsBtn.w && ly >= settingsBtn.y && ly <= settingsBtn.y + settingsBtn.h)
|
||||||
{
|
{
|
||||||
showSettingsPopup = true;
|
showSettingsPopup = true;
|
||||||
@ -1310,22 +1342,8 @@ int main(int, char **)
|
|||||||
float contentH = LOGICAL_H * logicalScale;
|
float contentH = LOGICAL_H * logicalScale;
|
||||||
float contentOffsetX = (winW - contentW) * 0.5f / logicalScale;
|
float contentOffsetX = (winW - contentW) * 0.5f / logicalScale;
|
||||||
float contentOffsetY = (winH - contentH) * 0.5f / logicalScale;
|
float contentOffsetY = (winH - contentH) * 0.5f / logicalScale;
|
||||||
bool isSmall = ((LOGICAL_W * logicalScale) < 700.0f);
|
ui::MenuLayoutParams params{ LOGICAL_W, LOGICAL_H, winW, winH, logicalScale };
|
||||||
float btnW = isSmall ? (LOGICAL_W * 0.4f) : 300.0f;
|
hoveredButton = ui::hitTestMenuButtons(params, lx, ly);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1413,35 +1431,11 @@ int main(int, char **)
|
|||||||
}
|
}
|
||||||
else if (state == AppState::Loading)
|
else if (state == AppState::Loading)
|
||||||
{
|
{
|
||||||
// Initialize audio system and start background loading on first frame
|
// Execute one loading step per frame on main thread
|
||||||
if (!musicLoaded && currentTrackLoading == 0) {
|
if (g_loadingStarted.load() && !g_loadingComplete.load()) {
|
||||||
Audio::instance().init();
|
if (performLoadingStep()) {
|
||||||
// Apply audio settings
|
g_loadingComplete.store(true);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
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
|
// Update progress based on background loading
|
||||||
@ -1454,34 +1448,44 @@ int main(int, char **)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate comprehensive loading progress
|
// Prefer task-based progress if we have tasks registered
|
||||||
// Phase 1: Initial assets (textures, fonts) - 20%
|
int totalTasks = g_totalLoadingTasks.load(std::memory_order_acquire);
|
||||||
double assetProgress = 0.2; // Assets are loaded at startup
|
int doneTasks = g_loadedTasks.load(std::memory_order_acquire);
|
||||||
|
if (totalTasks > 0) {
|
||||||
// Phase 2: Music loading - 70%
|
loadingProgress = std::min(1.0, double(doneTasks) / double(totalTasks));
|
||||||
double musicProgress = 0.0;
|
if (loadingProgress >= 1.0) {
|
||||||
if (totalTracks > 0) {
|
state = AppState::Menu;
|
||||||
musicProgress = musicLoaded ? 0.7 : std::min(0.7, (double)currentTrackLoading / totalTracks * 0.7);
|
stateMgr.setState(state);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
// Phase 3: Final initialization - 10%
|
// Fallback: time + audio heuristics (legacy behavior)
|
||||||
double timeProgress = std::min(0.1, (now - loadStart) / 500.0); // Faster final phase
|
double assetProgress = 0.2;
|
||||||
|
double musicProgress = 0.0;
|
||||||
loadingProgress = assetProgress + musicProgress + timeProgress;
|
if (totalTracks > 0) {
|
||||||
|
musicProgress = musicLoaded ? 0.7 : std::min(0.7, (double)currentTrackLoading / totalTracks * 0.7);
|
||||||
// Ensure we never exceed 100% and reach exactly 100% when everything is loaded
|
} else {
|
||||||
loadingProgress = std::min(1.0, loadingProgress);
|
if (Audio::instance().isLoadingComplete()) {
|
||||||
|
musicProgress = 0.7;
|
||||||
// Fix floating point precision issues (0.2 + 0.7 + 0.1 can be 0.9999...)
|
} else if (Audio::instance().getLoadedTrackCount() > 0) {
|
||||||
if (loadingProgress > 0.99) loadingProgress = 1.0;
|
musicProgress = 0.35;
|
||||||
|
} else {
|
||||||
if (musicLoaded && timeProgress >= 0.1) {
|
Uint32 elapsedMs = SDL_GetTicks() - static_cast<Uint32>(loadStart);
|
||||||
loadingProgress = 1.0;
|
if (elapsedMs > 1500) {
|
||||||
}
|
musicProgress = 0.7;
|
||||||
|
musicLoaded = true;
|
||||||
if (loadingProgress >= 1.0 && musicLoaded) {
|
} else {
|
||||||
state = AppState::Menu;
|
musicProgress = 0.0;
|
||||||
stateMgr.setState(state);
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
if (state == AppState::Menu || state == AppState::Playing)
|
||||||
@ -1570,6 +1574,20 @@ int main(int, char **)
|
|||||||
break;
|
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) {
|
if (menuFadePhase == MenuFadePhase::FadeOut) {
|
||||||
menuFadeClockMs += frameMs;
|
menuFadeClockMs += frameMs;
|
||||||
menuFadeAlpha = std::min(1.0f, float(menuFadeClockMs / MENU_PLAY_FADE_DURATION_MS));
|
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
|
// `mainScreenTex` is rendered as a top layer just before presenting
|
||||||
// so we don't draw it here. Keep the space warp background only.
|
// so we don't draw it here. Keep the space warp background only.
|
||||||
} else if (state == AppState::LevelSelector || state == AppState::Options) {
|
} else if (state == AppState::LevelSelector || state == AppState::Options) {
|
||||||
if (backgroundTex) {
|
// No static background texture to draw (background image removed).
|
||||||
SDL_FRect fullRect = { 0, 0, (float)winW, (float)winH };
|
|
||||||
SDL_RenderTexture(renderer, backgroundTex, nullptr, &fullRect);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Use regular starfield for other states (not gameplay)
|
// Use regular starfield for other states (not gameplay)
|
||||||
starfield.draw(renderer);
|
starfield.draw(renderer);
|
||||||
@ -1757,6 +1772,66 @@ int main(int, char **)
|
|||||||
float percentWidth = strlen(percentText) * 12.0f; // Approximate width for pixel font
|
float percentWidth = strlen(percentText) * 12.0f; // Approximate width for pixel font
|
||||||
float percentX = (LOGICAL_W - percentWidth) / 2.0f;
|
float percentX = (LOGICAL_W - percentWidth) / 2.0f;
|
||||||
pixelFont.draw(renderer, percentX + contentOffsetX, currentY + contentOffsetY, percentText, 1.5f, {255, 204, 0, 255});
|
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;
|
break;
|
||||||
case AppState::Menu:
|
case AppState::Menu:
|
||||||
@ -2030,8 +2105,6 @@ int main(int, char **)
|
|||||||
}
|
}
|
||||||
if (logoTex)
|
if (logoTex)
|
||||||
SDL_DestroyTexture(logoTex);
|
SDL_DestroyTexture(logoTex);
|
||||||
if (backgroundTex)
|
|
||||||
SDL_DestroyTexture(backgroundTex);
|
|
||||||
if (mainScreenTex)
|
if (mainScreenTex)
|
||||||
SDL_DestroyTexture(mainScreenTex);
|
SDL_DestroyTexture(mainScreenTex);
|
||||||
resetLevelBackgrounds(levelBackgrounds);
|
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