feat: implement textured line clear effects and refine UI alignment

- **Visual Effects**: Upgraded line clear particles to use the game's block texture instead of simple circles, matching the reference web game's aesthetic.
- **Particle Physics**: Tuned particle velocity, gravity, and fade rates for a more dynamic explosion effect.
- **Rendering Integration**: Updated [main.cpp](cci:7://file:///d:/Sites/Work/tetris/src/main.cpp:0:0-0:0) and `GameRenderer` to pass the block texture to the effect system and correctly trigger animations upon line completion.
- **Menu UI**: Fixed [MenuState](cci:1://file:///d:/Sites/Work/tetris/src/states/MenuState.cpp:19:0-19:55) layout calculations to use fixed logical dimensions (1200x1000), ensuring consistent centering and alignment of the logo, buttons, and settings icon across different window sizes.
- **Code Cleanup**: Refactored `PlayingState` to delegate effect triggering to the rendering layer where correct screen coordinates are available.
This commit is contained in:
2025-11-21 21:19:14 +01:00
parent b5ef9172b3
commit 66099809e0
47 changed files with 5547 additions and 267 deletions

View File

@ -0,0 +1,481 @@
#include "AssetManager.h"
#include "../../graphics/ui/Font.h"
#include "../../audio/Audio.h"
#include "../../audio/SoundEffect.h"
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <filesystem>
AssetManager::AssetManager()
: m_renderer(nullptr)
, m_audioSystem(nullptr)
, m_soundSystem(nullptr)
, m_totalLoadingTasks(0)
, m_completedLoadingTasks(0)
, m_loadingComplete(false)
, m_defaultTexturePath("assets/images/")
, m_defaultFontPath("assets/fonts/")
, m_initialized(false) {
}
AssetManager::~AssetManager() {
shutdown();
}
bool AssetManager::initialize(SDL_Renderer* renderer) {
if (m_initialized) {
logError("AssetManager already initialized");
return false;
}
if (!renderer) {
setError("Invalid renderer provided to AssetManager");
return false;
}
m_renderer = renderer;
// Get references to singleton systems
m_audioSystem = &Audio::instance();
m_soundSystem = &SoundEffectManager::instance();
m_initialized = true;
logInfo("AssetManager initialized successfully");
return true;
}
void AssetManager::shutdown() {
if (!m_initialized) {
return;
}
logInfo("Shutting down AssetManager...");
// Clear loading tasks
clearLoadingTasks();
// Cleanup textures
for (auto& [id, texture] : m_textures) {
if (texture) {
SDL_DestroyTexture(texture);
logInfo("Destroyed texture: " + id);
}
}
m_textures.clear();
// Cleanup fonts (unique_ptr handles destruction)
m_fonts.clear();
// Reset state
m_renderer = nullptr;
m_audioSystem = nullptr;
m_soundSystem = nullptr;
m_initialized = false;
logInfo("AssetManager shutdown complete");
}
SDL_Texture* AssetManager::loadTexture(const std::string& id, const std::string& filepath) {
if (!validateRenderer()) {
return nullptr;
}
// Check if already loaded
auto it = m_textures.find(id);
if (it != m_textures.end()) {
logInfo("Texture already loaded: " + id);
return it->second;
}
// Load new texture
SDL_Texture* texture = loadTextureFromFile(filepath);
if (!texture) {
setError("Failed to load texture: " + filepath);
return nullptr;
}
m_textures[id] = texture;
logInfo("Loaded texture: " + id + " from " + filepath);
return texture;
}
SDL_Texture* AssetManager::getTexture(const std::string& id) const {
auto it = m_textures.find(id);
return (it != m_textures.end()) ? it->second : nullptr;
}
bool AssetManager::unloadTexture(const std::string& id) {
auto it = m_textures.find(id);
if (it == m_textures.end()) {
setError("Texture not found: " + id);
return false;
}
if (it->second) {
SDL_DestroyTexture(it->second);
}
m_textures.erase(it);
logInfo("Unloaded texture: " + id);
return true;
}
bool AssetManager::loadFont(const std::string& id, const std::string& filepath, int baseSize) {
// Check if already loaded
auto it = m_fonts.find(id);
if (it != m_fonts.end()) {
logInfo("Font already loaded: " + id);
return true;
}
// Create new font
auto font = std::make_unique<FontAtlas>();
if (!font->init(filepath, baseSize)) {
setError("Failed to initialize font: " + filepath);
return false;
}
m_fonts[id] = std::move(font);
logInfo("Loaded font: " + id + " from " + filepath + " (size: " + std::to_string(baseSize) + ")");
return true;
}
FontAtlas* AssetManager::getFont(const std::string& id) const {
auto it = m_fonts.find(id);
return (it != m_fonts.end()) ? it->second.get() : nullptr;
}
bool AssetManager::unloadFont(const std::string& id) {
auto it = m_fonts.find(id);
if (it == m_fonts.end()) {
setError("Font not found: " + id);
return false;
}
// Shutdown the font before removing
it->second->shutdown();
m_fonts.erase(it);
logInfo("Unloaded font: " + id);
return true;
}
bool AssetManager::loadMusicTrack(const std::string& filepath) {
if (!m_audioSystem) {
setError("Audio system not available");
return false;
}
if (!fileExists(filepath)) {
setError("Music file not found: " + filepath);
return false;
}
try {
m_audioSystem->addTrackAsync(filepath);
logInfo("Added music track for loading: " + filepath);
return true;
} catch (const std::exception& e) {
setError("Failed to add music track: " + std::string(e.what()));
return false;
}
}
bool AssetManager::loadSoundEffect(const std::string& id, const std::string& filepath) {
if (!m_soundSystem) {
setError("Sound effect system not available");
return false;
}
if (!fileExists(filepath)) {
setError("Sound effect file not found: " + filepath);
return false;
}
if (m_soundSystem->loadSound(id, filepath)) {
logInfo("Loaded sound effect: " + id + " from " + filepath);
return true;
} else {
setError("Failed to load sound effect: " + id + " from " + filepath);
return false;
}
}
bool AssetManager::loadSoundEffectWithFallback(const std::string& id, const std::string& baseName) {
if (!m_soundSystem) {
setError("Sound effect system not available");
return false;
}
// Try WAV first, then MP3 fallback (matching main.cpp pattern)
std::string wavPath = "assets/music/" + baseName + ".wav";
std::string mp3Path = "assets/music/" + baseName + ".mp3";
// Check WAV first
if (fileExists(wavPath)) {
if (m_soundSystem->loadSound(id, wavPath)) {
logInfo("Loaded sound effect: " + id + " from " + wavPath + " (WAV)");
return true;
}
}
// Fallback to MP3
if (fileExists(mp3Path)) {
if (m_soundSystem->loadSound(id, mp3Path)) {
logInfo("Loaded sound effect: " + id + " from " + mp3Path + " (MP3 fallback)");
return true;
}
}
setError("Failed to load sound effect: " + id + " (tried both WAV and MP3)");
return false;
}
void AssetManager::startBackgroundMusicLoading() {
if (!m_audioSystem) {
setError("Audio system not available");
return;
}
m_audioSystem->startBackgroundLoading();
logInfo("Started background music loading");
}
bool AssetManager::isMusicLoadingComplete() const {
if (!m_audioSystem) {
return false;
}
return m_audioSystem->isLoadingComplete();
}
int AssetManager::getLoadedMusicTrackCount() const {
if (!m_audioSystem) {
return 0;
}
return m_audioSystem->getLoadedTrackCount();
}
void AssetManager::addLoadingTask(const LoadingTask& task) {
m_loadingTasks.push_back(task);
logInfo("Added loading task: " + task.id + " (" + task.filepath + ")");
}
void AssetManager::executeLoadingTasks(std::function<void(float)> progressCallback) {
if (m_loadingTasks.empty()) {
m_loadingComplete = true;
if (progressCallback) progressCallback(1.0f);
return;
}
logInfo("Starting progressive loading of " + std::to_string(m_loadingTasks.size()) + " loading tasks...");
m_totalLoadingTasks = m_loadingTasks.size();
m_completedLoadingTasks = 0;
m_currentTaskIndex = 0;
m_loadingComplete = false;
m_isProgressiveLoading = true;
m_lastLoadTime = SDL_GetTicks();
m_musicLoadingStarted = false;
m_musicLoadingProgress = 0.0f;
// Don't execute tasks immediately - let update() handle them progressively
}
void AssetManager::update(float deltaTime) {
if (!m_isProgressiveLoading || m_loadingTasks.empty()) {
// Handle music loading progress simulation if assets are done
if (m_musicLoadingStarted && !m_loadingComplete) {
m_musicLoadingProgress += deltaTime * 0.4f; // Simulate music loading progress
if (m_musicLoadingProgress >= 1.0f) {
m_musicLoadingProgress = 1.0f;
m_loadingComplete = true;
logInfo("Background music loading simulation complete");
}
}
return;
}
Uint64 currentTime = SDL_GetTicks();
// Add minimum delay between loading items (600ms per item for visual effect)
if (currentTime - m_lastLoadTime < 600) {
return;
}
// Load one item at a time
if (m_currentTaskIndex < m_loadingTasks.size()) {
const auto& task = m_loadingTasks[m_currentTaskIndex];
bool success = false;
switch (task.type) {
case LoadingTask::TEXTURE:
success = (loadTexture(task.id, task.filepath) != nullptr);
break;
case LoadingTask::FONT:
success = loadFont(task.id, task.filepath, task.fontSize);
break;
case LoadingTask::MUSIC:
success = loadMusicTrack(task.filepath);
break;
case LoadingTask::SOUND_EFFECT:
success = loadSoundEffect(task.id, task.filepath);
break;
}
if (!success) {
logError("Failed to load asset: " + task.id + " (" + task.filepath + ")");
}
m_currentTaskIndex++;
m_completedLoadingTasks = m_currentTaskIndex;
m_lastLoadTime = currentTime;
logInfo("Asset loading progress: " + std::to_string((float)m_completedLoadingTasks / m_totalLoadingTasks * 100.0f) + "%");
// Check if all asset tasks are complete
if (m_currentTaskIndex >= m_loadingTasks.size()) {
m_isProgressiveLoading = false;
logInfo("Completed " + std::to_string(m_completedLoadingTasks) + "/" + std::to_string(m_totalLoadingTasks) + " loading tasks");
// Start background music loading simulation
m_musicLoadingStarted = true;
m_musicLoadingProgress = 0.0f;
startBackgroundMusicLoading();
}
}
}
void AssetManager::clearLoadingTasks() {
m_loadingTasks.clear();
logInfo("Cleared loading tasks");
}
bool AssetManager::isResourceLoaded(const std::string& id) const {
return (m_textures.find(id) != m_textures.end()) ||
(m_fonts.find(id) != m_fonts.end());
}
std::string AssetManager::getAssetPath(const std::string& relativePath) {
// Simple path construction - could be enhanced with proper path handling
if (relativePath.find("assets/") == 0) {
return relativePath; // Already has assets/ prefix
}
return "assets/" + relativePath;
}
bool AssetManager::fileExists(const std::string& filepath) {
// Use SDL file I/O for consistency with main.cpp pattern
SDL_IOStream* file = SDL_IOFromFile(filepath.c_str(), "rb");
if (file) {
SDL_CloseIO(file);
return true;
}
return false;
}
SDL_Texture* AssetManager::loadTextureFromFile(const std::string& filepath) {
if (!validateRenderer()) {
return nullptr;
}
// Load using SDL_LoadBMP (matching main.cpp pattern)
SDL_Surface* surface = SDL_LoadBMP(filepath.c_str());
if (!surface) {
setError("Failed to load surface from: " + filepath + " - " + SDL_GetError());
return nullptr;
}
SDL_Texture* texture = SDL_CreateTextureFromSurface(m_renderer, surface);
SDL_DestroySurface(surface);
if (!texture) {
setError("Failed to create texture from surface: " + filepath + " - " + SDL_GetError());
return nullptr;
}
return texture;
}
bool AssetManager::validateRenderer() const {
if (!m_initialized) {
const_cast<AssetManager*>(this)->setError("AssetManager not initialized");
return false;
}
if (!m_renderer) {
const_cast<AssetManager*>(this)->setError("Invalid renderer");
return false;
}
return true;
}
void AssetManager::setError(const std::string& error) {
m_lastError = error;
logError(error);
}
void AssetManager::logInfo(const std::string& message) const {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[AssetManager] %s", message.c_str());
}
void AssetManager::logError(const std::string& message) const {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[AssetManager] %s", message.c_str());
}
// Loading progress tracking methods
bool AssetManager::isLoadingComplete() const {
// Loading is complete when both asset tasks and music loading are done
return m_loadingComplete && (!m_musicLoadingStarted || m_musicLoadingProgress >= 1.0f);
}
float AssetManager::getLoadingProgress() const {
if (m_totalLoadingTasks == 0) {
return 1.0f; // No tasks = complete
}
// Asset loading progress (80% of total progress)
float assetProgress = static_cast<float>(m_completedLoadingTasks) / static_cast<float>(m_totalLoadingTasks) * 0.8f;
// Music loading progress (20% of total progress)
float musicProgress = m_musicLoadingStarted ? m_musicLoadingProgress * 0.2f : 0.0f;
return assetProgress + musicProgress;
}
// IAssetLoader interface implementation
SDL_Texture* AssetManager::loadTextureFromPath(const std::string& path) {
// Use the path as both ID and filepath for the interface implementation
return loadTexture(path, path);
}
bool AssetManager::loadFontAsset(const std::string& name, const std::string& path, int size) {
// Delegate to the existing loadFont method
return loadFont(name, path, size);
}
bool AssetManager::loadAudioAsset(const std::string& name, const std::string& path) {
return loadSoundEffect(name, path);
}
SDL_Texture* AssetManager::getTextureAsset(const std::string& name) {
// Delegate to the existing getTexture method
return getTexture(name);
}
bool AssetManager::hasAsset(const std::string& name) const {
return m_textures.find(name) != m_textures.end() ||
m_fonts.find(name) != m_fonts.end();
}
void AssetManager::unloadAsset(const std::string& name) {
// Try to unload as texture first, then as font
if (!unloadTexture(name)) {
unloadFont(name);
}
}
void AssetManager::unloadAll() {
shutdown();
}