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

@ -181,3 +181,30 @@ void GlobalState::resetAnimationState() {
fireworks.clear();
lastFireworkTime = 0;
}
void GlobalState::updateLogicalDimensions(int windowWidth, int windowHeight) {
// For now, keep logical dimensions proportional to window size
// You can adjust this logic based on your specific needs
// Option 1: Keep fixed aspect ratio and scale uniformly
const float targetAspect = static_cast<float>(Config::Logical::WIDTH) / static_cast<float>(Config::Logical::HEIGHT);
const float windowAspect = static_cast<float>(windowWidth) / static_cast<float>(windowHeight);
if (windowAspect > targetAspect) {
// Window is wider than target aspect - fit to height
currentLogicalHeight = Config::Logical::HEIGHT;
currentLogicalWidth = static_cast<int>(currentLogicalHeight * windowAspect);
} else {
// Window is taller than target aspect - fit to width
currentLogicalWidth = Config::Logical::WIDTH;
currentLogicalHeight = static_cast<int>(currentLogicalWidth / windowAspect);
}
// Ensure minimum sizes
currentLogicalWidth = std::max(currentLogicalWidth, 800);
currentLogicalHeight = std::max(currentLogicalHeight, 600);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"[GlobalState] Updated logical dimensions: %dx%d (window: %dx%d)",
currentLogicalWidth, currentLogicalHeight, windowWidth, windowHeight);
}

View File

@ -66,6 +66,10 @@ public:
// Viewport and scaling
SDL_Rect logicalVP{0, 0, 1200, 1000}; // Will use Config::Logical constants
float logicalScale = 1.0f;
// Dynamic logical dimensions (computed from window size)
int currentLogicalWidth = 1200;
int currentLogicalHeight = 1000;
// Fireworks system (for menu animation)
struct BlockParticle {
@ -88,6 +92,11 @@ public:
void createFirework(float x, float y);
void drawFireworks(SDL_Renderer* renderer, SDL_Texture* blocksTex);
// Logical dimensions management
void updateLogicalDimensions(int windowWidth, int windowHeight);
int getLogicalWidth() const { return currentLogicalWidth; }
int getLogicalHeight() const { return currentLogicalHeight; }
// Reset methods for different states
void resetGameState();
void resetUIState();

View File

@ -0,0 +1,92 @@
#pragma once
#include <memory>
#include <unordered_map>
#include <typeindex>
#include <stdexcept>
/**
* @brief Dependency injection container for managing services
*
* Provides a centralized way to register and retrieve services,
* enabling loose coupling and better testability.
*/
class ServiceContainer {
private:
std::unordered_map<std::type_index, std::shared_ptr<void>> services_;
public:
/**
* @brief Register a service instance
* @tparam T Service type
* @param service Shared pointer to service instance
*/
template<typename T>
void registerService(std::shared_ptr<T> service) {
services_[std::type_index(typeid(T))] = service;
}
/**
* @brief Get a service instance
* @tparam T Service type
* @return Shared pointer to service instance
* @throws std::runtime_error if service is not registered
*/
template<typename T>
std::shared_ptr<T> getService() {
auto it = services_.find(std::type_index(typeid(T)));
if (it != services_.end()) {
return std::static_pointer_cast<T>(it->second);
}
throw std::runtime_error("Service not registered: " + std::string(typeid(T).name()));
}
/**
* @brief Get a service instance (const version)
* @tparam T Service type
* @return Shared pointer to service instance
* @throws std::runtime_error if service is not registered
*/
template<typename T>
std::shared_ptr<const T> getService() const {
auto it = services_.find(std::type_index(typeid(T)));
if (it != services_.end()) {
return std::static_pointer_cast<const T>(it->second);
}
throw std::runtime_error("Service not registered: " + std::string(typeid(T).name()));
}
/**
* @brief Check if a service is registered
* @tparam T Service type
* @return true if service is registered, false otherwise
*/
template<typename T>
bool hasService() const {
return services_.find(std::type_index(typeid(T))) != services_.end();
}
/**
* @brief Unregister a service
* @tparam T Service type
*/
template<typename T>
void unregisterService() {
services_.erase(std::type_index(typeid(T)));
}
/**
* @brief Clear all registered services
*/
void clear() {
services_.clear();
}
/**
* @brief Get the number of registered services
* @return Number of registered services
*/
size_t getServiceCount() const {
return services_.size();
}
};

View File

@ -1,30 +1,35 @@
#include "ApplicationManager.h"
#include "StateManager.h"
#include "InputManager.h"
#include "../state/StateManager.h"
#include "../input/InputManager.h"
#include "../interfaces/IAudioSystem.h"
#include "../interfaces/IRenderer.h"
#include "../interfaces/IAssetLoader.h"
#include "../interfaces/IInputHandler.h"
#include <filesystem>
#include "../audio/Audio.h"
#include "../audio/SoundEffect.h"
#include "../persistence/Scores.h"
#include "../states/State.h"
#include "../states/LoadingState.h"
#include "../states/MenuState.h"
#include "../states/LevelSelectorState.h"
#include "../states/PlayingState.h"
#include "AssetManager.h"
#include "Config.h"
#include "GlobalState.h"
#include "../graphics/RenderManager.h"
#include "../graphics/Font.h"
#include "../graphics/Starfield3D.h"
#include "../graphics/Starfield.h"
#include "../graphics/GameRenderer.h"
#include "../gameplay/Game.h"
#include "../gameplay/LineEffect.h"
#include "../../audio/Audio.h"
#include "../../audio/SoundEffect.h"
#include "../../persistence/Scores.h"
#include "../../states/State.h"
#include "../../states/LoadingState.h"
#include "../../states/MenuState.h"
#include "../../states/LevelSelectorState.h"
#include "../../states/PlayingState.h"
#include "../assets/AssetManager.h"
#include "../Config.h"
#include "../GlobalState.h"
#include "../../graphics/renderers/RenderManager.h"
#include "../../graphics/ui/Font.h"
#include "../../graphics/effects/Starfield3D.h"
#include "../../graphics/effects/Starfield.h"
#include "../../graphics/renderers/GameRenderer.h"
#include "../../gameplay/core/Game.h"
#include "../../gameplay/effects/LineEffect.h"
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <iostream>
#include <cmath>
#include <fstream>
#include <algorithm>
ApplicationManager::ApplicationManager() = default;
@ -33,6 +38,115 @@ static void traceFile(const char* msg) {
if (f) f << msg << "\n";
}
// Helper: extracted from inline lambda to avoid MSVC parsing issues with complex lambdas
void ApplicationManager::renderLoading(ApplicationManager* app, RenderManager& renderer) {
// Clear background first
renderer.clear(0, 0, 0, 255);
// Use 3D starfield for loading screen (full screen)
if (app->m_starfield3D) {
int winW_actual = 0, winH_actual = 0;
if (app->m_renderManager) app->m_renderManager->getWindowSize(winW_actual, winH_actual);
if (winW_actual > 0 && winH_actual > 0) app->m_starfield3D->resize(winW_actual, winH_actual);
app->m_starfield3D->draw(renderer.getSDLRenderer());
}
SDL_Rect logicalVP = {0,0,0,0};
float logicalScale = 1.0f;
if (app->m_renderManager) {
logicalVP = app->m_renderManager->getLogicalViewport();
logicalScale = app->m_renderManager->getLogicalScale();
}
SDL_SetRenderViewport(renderer.getSDLRenderer(), &logicalVP);
SDL_SetRenderScale(renderer.getSDLRenderer(), logicalScale, logicalScale);
float contentOffsetX = 0.0f;
float contentOffsetY = 0.0f;
auto drawRectOriginal = [&](float x, float y, float w, float h, SDL_Color c) {
SDL_SetRenderDrawColor(renderer.getSDLRenderer(), c.r, c.g, c.b, c.a);
SDL_FRect fr;
fr.x = x + contentOffsetX;
fr.y = y + contentOffsetY;
fr.w = w;
fr.h = h;
SDL_RenderFillRect(renderer.getSDLRenderer(), &fr);
};
// Compute dynamic logical width/height based on the RenderManager's
// computed viewport and scale so the loading UI sizes itself to the
// actual content area rather than a hardcoded design size.
float LOGICAL_W = static_cast<float>(Config::Logical::WIDTH);
float LOGICAL_H = static_cast<float>(Config::Logical::HEIGHT);
if (logicalScale > 0.0f && logicalVP.w > 0 && logicalVP.h > 0) {
// logicalVP is in window pixels; divide by scale to get logical units
LOGICAL_W = static_cast<float>(logicalVP.w) / logicalScale;
LOGICAL_H = static_cast<float>(logicalVP.h) / logicalScale;
}
const bool isLimitedHeight = LOGICAL_H < 450.0f;
SDL_Texture* logoTex = app->m_assetManager->getTexture("logo");
const float logoHeight = logoTex ? (isLimitedHeight ? LOGICAL_H * 0.25f : LOGICAL_H * 0.4f) : 0;
const float loadingTextHeight = 20;
const float barHeight = 20;
const float barPaddingVertical = isLimitedHeight ? 15 : 35;
const float percentTextHeight = 24;
const float spacingBetweenElements = isLimitedHeight ? 5 : 15;
const float totalContentHeight = logoHeight + (logoHeight > 0 ? spacingBetweenElements : 0) + loadingTextHeight + barPaddingVertical + barHeight + spacingBetweenElements + percentTextHeight;
float currentY = (LOGICAL_H - totalContentHeight) / 2.0f;
if (logoTex) {
const int lw = 872, lh = 273;
const float maxLogoWidth = std::min(LOGICAL_W * 0.9f, 600.0f);
const float availableHeight = isLimitedHeight ? LOGICAL_H * 0.25f : LOGICAL_H * 0.4f;
const float availableWidth = maxLogoWidth;
const float scaleFactorWidth = availableWidth / static_cast<float>(lw);
const float scaleFactorHeight = availableHeight / static_cast<float>(lh);
const float scaleFactor = std::min(scaleFactorWidth, scaleFactorHeight);
const float displayWidth = lw * scaleFactor;
const float displayHeight = lh * scaleFactor;
const float logoX = (LOGICAL_W - displayWidth) / 2.0f;
SDL_FRect dst{logoX + contentOffsetX, currentY + contentOffsetY, displayWidth, displayHeight};
SDL_RenderTexture(renderer.getSDLRenderer(), logoTex, nullptr, &dst);
currentY += displayHeight + spacingBetweenElements;
}
FontAtlas* pixelFont = (FontAtlas*)app->m_assetManager->getFont("pixel_font");
FontAtlas* fallbackFont = (FontAtlas*)app->m_assetManager->getFont("main_font");
FontAtlas* loadingFont = pixelFont ? pixelFont : fallbackFont;
if (loadingFont) {
const std::string loadingText = "LOADING";
int tW=0, tH=0; loadingFont->measure(loadingText, 1.0f, tW, tH);
float textX = (LOGICAL_W - (float)tW) * 0.5f;
loadingFont->draw(renderer.getSDLRenderer(), textX + contentOffsetX, currentY + contentOffsetY, loadingText, 1.0f, {255,204,0,255});
}
currentY += loadingTextHeight + barPaddingVertical;
const int barW = 400, barH = 20;
const int bx = (LOGICAL_W - barW) / 2;
float loadingProgress = app->m_assetManager->getLoadingProgress();
drawRectOriginal(bx - 3, currentY - 3, barW + 6, barH + 6, {68,68,80,255});
drawRectOriginal(bx, currentY, barW, barH, {34,34,34,255});
drawRectOriginal(bx, currentY, int(barW * loadingProgress), barH, {255,204,0,255});
currentY += barH + spacingBetweenElements;
if (loadingFont) {
int percentage = int(loadingProgress * 100);
char percentText[16];
std::snprintf(percentText, sizeof(percentText), "%d%%", percentage);
std::string pStr(percentText);
int pW=0, pH=0; loadingFont->measure(pStr, 1.5f, pW, pH);
float percentX = (LOGICAL_W - (float)pW) * 0.5f;
loadingFont->draw(renderer.getSDLRenderer(), percentX + contentOffsetX, currentY + contentOffsetY, pStr, 1.5f, {255,204,0,255});
}
SDL_SetRenderViewport(renderer.getSDLRenderer(), nullptr);
SDL_SetRenderScale(renderer.getSDLRenderer(), 1.0f, 1.0f);
}
ApplicationManager::~ApplicationManager() {
if (m_initialized) {
shutdown();
@ -49,6 +163,9 @@ bool ApplicationManager::initialize(int argc, char* argv[]) {
// Initialize GlobalState
GlobalState::instance().initialize();
// Set initial logical dimensions
GlobalState::instance().updateLogicalDimensions(m_windowWidth, m_windowHeight);
// Initialize SDL first
if (!initializeSDL()) {
@ -63,6 +180,9 @@ bool ApplicationManager::initialize(int argc, char* argv[]) {
return false;
}
// Register services for dependency injection
registerServices();
// Initialize game systems
if (!initializeGame()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize game systems");
@ -212,6 +332,7 @@ bool ApplicationManager::initializeManagers() {
m_renderManager->setFullscreen(!fs);
}
// Dont also forward Alt+Enter as an Enter keypress to states (prevents accidental "Start")
// Don't also forward Alt+Enter as an Enter keypress to states (prevents accidental "Start")
consume = true;
}
@ -269,6 +390,9 @@ bool ApplicationManager::initializeManagers() {
// Handle window resize events for RenderManager
if (we.type == SDL_EVENT_WINDOW_RESIZED && m_renderManager) {
m_renderManager->handleWindowResize(we.data1, we.data2);
// Update GlobalState logical dimensions when window resizes
GlobalState::instance().updateLogicalDimensions(we.data1, we.data2);
}
// Forward all window events to StateManager
@ -289,6 +413,45 @@ bool ApplicationManager::initializeManagers() {
return true;
}
void ApplicationManager::registerServices() {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Registering services for dependency injection...");
// Register concrete implementations as interface singletons
if (m_renderManager) {
std::shared_ptr<RenderManager> renderPtr(m_renderManager.get(), [](RenderManager*) {
// Custom deleter that does nothing since the unique_ptr manages lifetime
});
m_serviceContainer.registerSingleton<IRenderer>(renderPtr);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Registered IRenderer service");
}
if (m_assetManager) {
std::shared_ptr<AssetManager> assetPtr(m_assetManager.get(), [](AssetManager*) {
// Custom deleter that does nothing since the unique_ptr manages lifetime
});
m_serviceContainer.registerSingleton<IAssetLoader>(assetPtr);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Registered IAssetLoader service");
}
if (m_inputManager) {
std::shared_ptr<InputManager> inputPtr(m_inputManager.get(), [](InputManager*) {
// Custom deleter that does nothing since the unique_ptr manages lifetime
});
m_serviceContainer.registerSingleton<IInputHandler>(inputPtr);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Registered IInputHandler service");
}
// Register Audio system singleton
auto& audioInstance = Audio::instance();
auto audioPtr = std::shared_ptr<Audio>(&audioInstance, [](Audio*) {
// Custom deleter that does nothing since Audio is a singleton
});
m_serviceContainer.registerSingleton<IAudioSystem>(audioPtr);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Registered IAudioSystem service");
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Service registration completed successfully");
}
bool ApplicationManager::initializeGame() {
// Load essential assets using AssetManager
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loading essential assets...");
@ -496,142 +659,12 @@ void ApplicationManager::setupStateHandlers() {
};
// Loading State Handlers (matching original main.cpp implementation)
m_stateManager->registerRenderHandler(AppState::Loading,
[this, drawRect](RenderManager& renderer) {
// Clear background first
renderer.clear(0, 0, 0, 255);
// Use 3D starfield for loading screen (full screen)
// Ensure starfield uses actual window size so center and projection are correct
if (m_starfield3D) {
int winW_actual = 0, winH_actual = 0;
if (m_renderManager) {
m_renderManager->getWindowSize(winW_actual, winH_actual);
}
if (winW_actual > 0 && winH_actual > 0) {
m_starfield3D->resize(winW_actual, winH_actual);
}
m_starfield3D->draw(renderer.getSDLRenderer());
}
// Set viewport and scaling for content using ACTUAL window size
// Use RenderManager's computed logical viewport and scale so all states share the exact math
SDL_Rect logicalVP = {0,0,0,0};
float logicalScale = 1.0f;
if (m_renderManager) {
logicalVP = m_renderManager->getLogicalViewport();
logicalScale = m_renderManager->getLogicalScale();
}
SDL_SetRenderViewport(renderer.getSDLRenderer(), &logicalVP);
SDL_SetRenderScale(renderer.getSDLRenderer(), logicalScale, logicalScale);
// Calculate actual content area (centered within the viewport)
// Since we already have a centered viewport, content should be drawn at (0,0) in logical space
// The viewport itself handles the centering, so no additional offset is needed
float contentOffsetX = 0.0f;
float contentOffsetY = 0.0f;
auto drawRectOriginal = [&](float x, float y, float w, float h, SDL_Color c) {
SDL_SetRenderDrawColor(renderer.getSDLRenderer(), c.r, c.g, c.b, c.a);
SDL_FRect fr{x + contentOffsetX, y + contentOffsetY, w, h};
SDL_RenderFillRect(renderer.getSDLRenderer(), &fr);
};
// Calculate dimensions for perfect centering (like JavaScript version)
const bool isLimitedHeight = LOGICAL_H < 450;
SDL_Texture* logoTex = m_assetManager->getTexture("logo");
const float logoHeight = logoTex ? (isLimitedHeight ? LOGICAL_H * 0.25f : LOGICAL_H * 0.4f) : 0;
const float loadingTextHeight = 20; // Height of "LOADING" text (match JS)
const float barHeight = 20; // Loading bar height (match JS)
const float barPaddingVertical = isLimitedHeight ? 15 : 35;
const float percentTextHeight = 24; // Height of percentage text
const float spacingBetweenElements = isLimitedHeight ? 5 : 15;
// Total content height
const float totalContentHeight = logoHeight +
(logoHeight > 0 ? spacingBetweenElements : 0) +
loadingTextHeight +
barPaddingVertical +
barHeight +
spacingBetweenElements +
percentTextHeight;
// Start Y position for perfect vertical centering
float currentY = (LOGICAL_H - totalContentHeight) / 2.0f;
// Draw logo (centered, static like JavaScript version)
if (logoTex) {
// Use the same original large logo dimensions as JS (we used a half-size BMP previously)
const int lw = 872, lh = 273;
// Cap logo width similar to JS UI.MAX_LOGO_WIDTH (600) and available screen space
const float maxLogoWidth = std::min(LOGICAL_W * 0.9f, 600.0f);
const float availableHeight = isLimitedHeight ? LOGICAL_H * 0.25f : LOGICAL_H * 0.4f;
const float availableWidth = maxLogoWidth;
const float scaleFactorWidth = availableWidth / static_cast<float>(lw);
const float scaleFactorHeight = availableHeight / static_cast<float>(lh);
const float scaleFactor = std::min(scaleFactorWidth, scaleFactorHeight);
const float displayWidth = lw * scaleFactor;
const float displayHeight = lh * scaleFactor;
const float logoX = (LOGICAL_W - displayWidth) / 2.0f;
SDL_FRect dst{logoX + contentOffsetX, currentY + contentOffsetY, displayWidth, displayHeight};
SDL_RenderTexture(renderer.getSDLRenderer(), logoTex, nullptr, &dst);
currentY += displayHeight + spacingBetweenElements;
}
// Draw "LOADING" text (centered, using pixel font with fallback to main_font)
FontAtlas* pixelFont = (FontAtlas*)m_assetManager->getFont("pixel_font");
FontAtlas* fallbackFont = (FontAtlas*)m_assetManager->getFont("main_font");
FontAtlas* loadingFont = pixelFont ? pixelFont : fallbackFont;
if (loadingFont) {
const std::string loadingText = "LOADING";
int tW=0, tH=0; loadingFont->measure(loadingText, 1.0f, tW, tH);
float textX = (LOGICAL_W - (float)tW) * 0.5f;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Rendering LOADING text at (%f,%f)", textX + contentOffsetX, currentY + contentOffsetY);
loadingFont->draw(renderer.getSDLRenderer(), textX + contentOffsetX, currentY + contentOffsetY, loadingText, 1.0f, {255, 204, 0, 255});
} else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "No loading font available to render LOADING text");
}
currentY += loadingTextHeight + barPaddingVertical;
// Draw loading bar (like JavaScript version)
const int barW = 400, barH = 20;
const int bx = (LOGICAL_W - barW) / 2;
float loadingProgress = m_assetManager->getLoadingProgress();
// Bar border (dark gray) - using drawRect which adds content offset
drawRectOriginal(bx - 3, currentY - 3, barW + 6, barH + 6, {68, 68, 80, 255});
// Bar background (darker gray)
drawRectOriginal(bx, currentY, barW, barH, {34, 34, 34, 255});
// Progress bar (gold color)
drawRectOriginal(bx, currentY, int(barW * loadingProgress), barH, {255, 204, 0, 255});
currentY += barH + spacingBetweenElements;
// Draw percentage text (centered, using loadingFont)
if (loadingFont) {
int percentage = int(loadingProgress * 100);
char percentText[16];
std::snprintf(percentText, sizeof(percentText), "%d%%", percentage);
std::string pStr(percentText);
int pW=0, pH=0; loadingFont->measure(pStr, 1.5f, pW, pH);
float percentX = (LOGICAL_W - (float)pW) * 0.5f;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Rendering percent text '%s' at (%f,%f)", percentText, percentX + contentOffsetX, currentY + contentOffsetY);
loadingFont->draw(renderer.getSDLRenderer(), percentX + contentOffsetX, currentY + contentOffsetY, pStr, 1.5f, {255, 204, 0, 255});
}
// Reset viewport and scale
SDL_SetRenderViewport(renderer.getSDLRenderer(), nullptr);
SDL_SetRenderScale(renderer.getSDLRenderer(), 1.0f, 1.0f);
});
// Extracted to a helper to avoid complex inline lambda parsing issues on MSVC
auto loadingRenderForwarder = [this](RenderManager& renderer) {
// forward to helper defined below
renderLoading(this, renderer);
};
m_stateManager->registerRenderHandler(AppState::Loading, loadingRenderForwarder);
m_stateManager->registerUpdateHandler(AppState::Loading,
[this](float deltaTime) {
@ -729,6 +762,7 @@ void ApplicationManager::setupStateHandlers() {
globalState.updateFireworks(deltaTime);
// Start music as soon as at least one track has decoded (dont wait for all)
// Start music as soon as at least one track has decoded (don't wait for all)
if (m_musicEnabled && !m_musicStarted) {
if (Audio::instance().getLoadedTrackCount() > 0) {
Audio::instance().shuffle();
@ -785,16 +819,20 @@ void ApplicationManager::setupStateHandlers() {
float lx = (mx - logicalVP.x) / logicalScale;
float ly = (my - logicalVP.y) / logicalScale;
// Compute dynamic logical dimensions from viewport/scale
float dynW = (logicalScale > 0.f && logicalVP.w > 0) ? (float)logicalVP.w / logicalScale : (float)Config::Logical::WIDTH;
float dynH = (logicalScale > 0.f && logicalVP.h > 0) ? (float)logicalVP.h / logicalScale : (float)Config::Logical::HEIGHT;
// Respect settings popup
if (m_showSettingsPopup) {
m_showSettingsPopup = false;
} else {
bool isSmall = ((Config::Logical::WIDTH * logicalScale) < 700.0f);
float btnW = isSmall ? (Config::Logical::WIDTH * 0.4f) : 300.0f;
bool isSmall = ((dynW * logicalScale) < 700.0f);
float btnW = isSmall ? (dynW * 0.4f) : 300.0f;
float btnH = isSmall ? 60.0f : 70.0f;
float btnCX = Config::Logical::WIDTH * 0.5f;
float btnCX = dynW * 0.5f;
const float btnYOffset = 40.0f;
float btnCY = Config::Logical::HEIGHT * 0.86f + btnYOffset;
float btnCY = dynH * 0.86f + btnYOffset;
SDL_FRect playBtn{btnCX - btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH};
SDL_FRect levelBtn{btnCX + btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH};
@ -806,7 +844,7 @@ void ApplicationManager::setupStateHandlers() {
m_stateManager->setState(AppState::LevelSelector);
} else {
// Settings area detection (top-right small area)
SDL_FRect settingsBtn{Config::Logical::WIDTH - 60, 10, 50, 30};
SDL_FRect settingsBtn{dynW - 60, 10, 50, 30};
if (lx >= settingsBtn.x && lx <= settingsBtn.x + settingsBtn.w && ly >= settingsBtn.y && ly <= settingsBtn.y + settingsBtn.h) {
m_showSettingsPopup = true;
}
@ -826,12 +864,15 @@ void ApplicationManager::setupStateHandlers() {
float lx = (mx - logicalVP.x) / logicalScale;
float ly = (my - logicalVP.y) / logicalScale;
if (!m_showSettingsPopup) {
bool isSmall = ((Config::Logical::WIDTH * logicalScale) < 700.0f);
float btnW = isSmall ? (Config::Logical::WIDTH * 0.4f) : 300.0f;
// Compute dynamic logical dimensions
float dynW = (logicalScale > 0.f && logicalVP.w > 0) ? (float)logicalVP.w / logicalScale : (float)Config::Logical::WIDTH;
float dynH = (logicalScale > 0.f && logicalVP.h > 0) ? (float)logicalVP.h / logicalScale : (float)Config::Logical::HEIGHT;
bool isSmall = ((dynW * logicalScale) < 700.0f);
float btnW = isSmall ? (dynW * 0.4f) : 300.0f;
float btnH = isSmall ? 60.0f : 70.0f;
float btnCX = Config::Logical::WIDTH * 0.5f;
float btnCX = dynW * 0.5f;
const float btnYOffset = 40.0f;
float btnCY = Config::Logical::HEIGHT * 0.86f + btnYOffset;
float btnCY = dynH * 0.86f + btnYOffset;
SDL_FRect playBtn{btnCX - btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH};
SDL_FRect levelBtn{btnCX + btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH};
m_hoveredButton = -1;
@ -958,6 +999,7 @@ void ApplicationManager::setupStateHandlers() {
m_cachedBgLevel = bgLevel;
} else {
m_cachedBgLevel = -1; // dont change if missing
m_cachedBgLevel = -1; // don't change if missing
if (s) SDL_DestroySurface(s);
}
}
@ -1140,6 +1182,98 @@ void ApplicationManager::setupStateHandlers() {
m_stateManager->setState(AppState::GameOver);
}
});
// Debug overlay: show current window and logical sizes on the right side of the screen
auto debugOverlay = [this](RenderManager& renderer) {
// Window size
int winW = 0, winH = 0;
renderer.getWindowSize(winW, winH);
// Logical viewport and scale
SDL_Rect logicalVP{0,0,0,0};
float logicalScale = 1.0f;
if (m_renderManager) {
logicalVP = m_renderManager->getLogicalViewport();
logicalScale = m_renderManager->getLogicalScale();
}
// Use dynamic logical dimensions from GlobalState
float LOGICAL_W = static_cast<float>(GlobalState::instance().getLogicalWidth());
float LOGICAL_H = static_cast<float>(GlobalState::instance().getLogicalHeight());
// Use logical viewport so overlay is aligned with game content
SDL_SetRenderViewport(renderer.getSDLRenderer(), &logicalVP);
SDL_SetRenderScale(renderer.getSDLRenderer(), logicalScale, logicalScale);
// Choose font (pixel first, fallback to main)
FontAtlas* pixelFont = (FontAtlas*)(m_assetManager ? m_assetManager->getFont("pixel_font") : nullptr);
FontAtlas* mainFont = (FontAtlas*)(m_assetManager ? m_assetManager->getFont("main_font") : nullptr);
FontAtlas* font = pixelFont ? pixelFont : mainFont;
// Inline small helper for drawing a filled rect in logical coords
auto fillRect = [&](float x, float y, float w, float h, SDL_Color c) {
SDL_SetRenderDrawColor(renderer.getSDLRenderer(), c.r, c.g, c.b, c.a);
SDL_FRect r{ x, y, w, h };
SDL_RenderFillRect(renderer.getSDLRenderer(), &r);
};
// Prepare text lines
char buf[128];
std::snprintf(buf, sizeof(buf), "Win: %d x %d", winW, winH);
std::string sWin(buf);
std::snprintf(buf, sizeof(buf), "Logical: %.0f x %.0f", LOGICAL_W, LOGICAL_H);
std::string sLogical(buf);
std::snprintf(buf, sizeof(buf), "Scale: %.2f", logicalScale);
std::string sScale(buf);
// Determine size of longest line
int w1=0,h1=0, w2=0,h2=0, w3=0,h3=0;
if (font) {
font->measure(sWin, 1.0f, w1, h1);
font->measure(sLogical, 1.0f, w2, h2);
font->measure(sScale, 1.0f, w3, h3);
}
int maxW = std::max({w1,w2,w3});
int totalH = (h1 + h2 + h3) + 8; // small padding
// Position based on actual screen width (center horizontally)
const float margin = 8.0f;
// float x = (LOGICAL_W - (float)maxW) * 0.5f; // Center horizontally
// float y = margin;
// Desired position in window (pixel) coords
int winW_px = 0, winH_px = 0;
renderer.getWindowSize(winW_px, winH_px);
float desiredWinX = (float(winW_px) - (float)maxW) * 0.5f; // center on full window width
float desiredWinY = margin; // near top of the window
// Convert window coords to logical coords under current viewport/scale
float invScale = (logicalScale > 0.0f) ? (1.0f / logicalScale) : 1.0f;
float x = (desiredWinX - float(logicalVP.x)) * invScale;
float y = (desiredWinY - float(logicalVP.y)) * invScale;
// Draw background box for readability
fillRect(x - 6.0f, y - 6.0f, (float)maxW + 12.0f, (float)totalH + 8.0f, {0, 0, 0, 180});
// Draw text lines
SDL_Color textColor = {255, 204, 0, 255};
if (font) {
font->draw(renderer.getSDLRenderer(), x, y, sWin, 1.0f, textColor);
font->draw(renderer.getSDLRenderer(), x, y + (float)h1, sLogical, 1.0f, textColor);
font->draw(renderer.getSDLRenderer(), x, y + (float)(h1 + h2), sScale, 1.0f, textColor);
}
// Reset viewport/scale
SDL_SetRenderViewport(renderer.getSDLRenderer(), nullptr);
SDL_SetRenderScale(renderer.getSDLRenderer(), 1.0f, 1.0f);
};
// Register debug overlay for all primary states so it draws on top
if (m_stateManager) {
m_stateManager->registerRenderHandler(AppState::Loading, debugOverlay);
m_stateManager->registerRenderHandler(AppState::Menu, debugOverlay);
m_stateManager->registerRenderHandler(AppState::LevelSelector, debugOverlay);
m_stateManager->registerRenderHandler(AppState::Playing, debugOverlay);
m_stateManager->registerRenderHandler(AppState::GameOver, debugOverlay);
}
}
void ApplicationManager::processEvents() {

View File

@ -1,7 +1,8 @@
#pragma once
#include "Config.h"
#include "../states/State.h"
#include "../Config.h"
#include "../../states/State.h"
#include "../container/ServiceContainer.h"
#include <memory>
#include <string>
@ -52,12 +53,18 @@ public:
AssetManager* getAssetManager() const { return m_assetManager.get(); }
StateManager* getStateManager() const { return m_stateManager.get(); }
// Service container access
ServiceContainer& getServiceContainer() { return m_serviceContainer; }
private:
// Helper used by setupStateHandlers (defined in cpp)
static void renderLoading(ApplicationManager* app, RenderManager& renderer);
// Initialization methods
bool initializeSDL();
bool initializeManagers();
bool initializeGame();
void setupStateHandlers();
void registerServices();
// Main loop methods
void processEvents();
@ -73,6 +80,9 @@ private:
std::unique_ptr<InputManager> m_inputManager;
std::unique_ptr<AssetManager> m_assetManager;
std::unique_ptr<StateManager> m_stateManager;
// Dependency injection container
ServiceContainer m_serviceContainer;
// Visual effects
std::unique_ptr<Starfield3D> m_starfield3D;

View File

@ -1,7 +1,7 @@
#include "AssetManager.h"
#include "../graphics/Font.h"
#include "../audio/Audio.h"
#include "../audio/SoundEffect.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>
@ -443,3 +443,39 @@ float AssetManager::getLoadingProgress() const {
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();
}

View File

@ -6,6 +6,8 @@
#include <unordered_map>
#include <memory>
#include <functional>
#include "../interfaces/IAssetLoader.h"
#include "../interfaces/IAssetLoader.h"
// Forward declarations
class FontAtlas;
@ -28,7 +30,7 @@ class SoundEffectManager;
* - Dependency Inversion: Uses interfaces for audio systems
* - Interface Segregation: Separate methods for different asset types
*/
class AssetManager {
class AssetManager : public IAssetLoader {
public:
AssetManager();
~AssetManager();
@ -37,7 +39,16 @@ public:
bool initialize(SDL_Renderer* renderer);
void shutdown();
// Texture management
// IAssetLoader interface implementation
SDL_Texture* loadTextureFromPath(const std::string& path) override;
bool loadFontAsset(const std::string& name, const std::string& path, int size) override;
bool loadAudioAsset(const std::string& name, const std::string& path) override;
SDL_Texture* getTextureAsset(const std::string& name) override;
bool hasAsset(const std::string& name) const override;
void unloadAsset(const std::string& name) override;
void unloadAll() override;
// Existing AssetManager methods with specific implementations
SDL_Texture* loadTexture(const std::string& id, const std::string& filepath);
SDL_Texture* getTexture(const std::string& id) const;
bool unloadTexture(const std::string& id);

View File

@ -0,0 +1,93 @@
#pragma once
#include <memory>
#include <unordered_map>
#include <typeindex>
#include <functional>
/**
* @brief Simple dependency injection container
*
* Provides service registration and resolution for dependency injection.
* Supports both singleton and factory-based service creation.
*/
class ServiceContainer {
public:
ServiceContainer() = default;
~ServiceContainer() = default;
/**
* @brief Register a singleton service instance
* @tparam TInterface Interface type
* @tparam TImplementation Implementation type
* @param instance Shared pointer to the service instance
*/
template<typename TInterface, typename TImplementation>
void registerSingleton(std::shared_ptr<TImplementation> instance) {
static_assert(std::is_base_of_v<TInterface, TImplementation>,
"TImplementation must inherit from TInterface");
m_singletons[std::type_index(typeid(TInterface))] = instance;
}
/**
* @brief Register a factory function for service creation
* @tparam TInterface Interface type
* @param factory Factory function that creates the service
*/
template<typename TInterface>
void registerFactory(std::function<std::shared_ptr<TInterface>()> factory) {
m_factories[std::type_index(typeid(TInterface))] = [factory]() -> std::shared_ptr<void> {
return std::static_pointer_cast<void>(factory());
};
}
/**
* @brief Resolve a service by its interface type
* @tparam TInterface Interface type to resolve
* @return Shared pointer to the service instance, nullptr if not found
*/
template<typename TInterface>
std::shared_ptr<TInterface> resolve() {
std::type_index typeIndex(typeid(TInterface));
// Check singletons first
auto singletonIt = m_singletons.find(typeIndex);
if (singletonIt != m_singletons.end()) {
return std::static_pointer_cast<TInterface>(singletonIt->second);
}
// Check factories
auto factoryIt = m_factories.find(typeIndex);
if (factoryIt != m_factories.end()) {
auto instance = factoryIt->second();
return std::static_pointer_cast<TInterface>(instance);
}
return nullptr;
}
/**
* @brief Check if a service is registered
* @tparam TInterface Interface type to check
* @return true if service is registered, false otherwise
*/
template<typename TInterface>
bool isRegistered() const {
std::type_index typeIndex(typeid(TInterface));
return m_singletons.find(typeIndex) != m_singletons.end() ||
m_factories.find(typeIndex) != m_factories.end();
}
/**
* @brief Clear all registered services
*/
void clear() {
m_singletons.clear();
m_factories.clear();
}
private:
std::unordered_map<std::type_index, std::shared_ptr<void>> m_singletons;
std::unordered_map<std::type_index, std::function<std::shared_ptr<void>()>> m_factories;
};

View File

@ -294,3 +294,67 @@ void InputManager::resetDAS() {
m_dasState.repeatTimer = 0.0f;
m_dasState.activeKey = SDL_SCANCODE_UNKNOWN;
}
// IInputHandler interface implementation
bool InputManager::processEvent(const SDL_Event& event) {
// Process individual event and return if it was handled
switch (event.type) {
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP:
handleKeyEvent(event.key);
return true;
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:
handleMouseButtonEvent(event.button);
return true;
case SDL_EVENT_MOUSE_MOTION:
handleMouseMotionEvent(event.motion);
return true;
case SDL_EVENT_WINDOW_RESIZED:
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
case SDL_EVENT_WINDOW_MINIMIZED:
case SDL_EVENT_WINDOW_RESTORED:
handleWindowEvent(event.window);
return true;
case SDL_EVENT_QUIT:
handleQuitEvent();
return true;
default:
return false;
}
}
void InputManager::update(double deltaTime) {
update(static_cast<float>(deltaTime));
}
bool InputManager::isKeyCurrentlyPressed(SDL_Scancode scancode) const {
return isKeyHeld(scancode);
}
bool InputManager::isKeyJustPressed(SDL_Scancode scancode) const {
return isKeyPressed(scancode);
}
bool InputManager::isKeyJustReleased(SDL_Scancode scancode) const {
return isKeyReleased(scancode);
}
bool InputManager::isQuitRequested() const {
return shouldQuit();
}
void InputManager::reset() {
// Clear pressed/released states, keep held states
// In the current InputManager implementation, we use previous/current state
// so we just copy current to previous to reset the "just pressed/released" states
m_previousKeyState = m_currentKeyState;
m_previousMouseState = m_currentMouseState;
}
void InputManager::handleQuitEvent() {
m_shouldQuit = true;
for (auto& handler : m_quitHandlers) {
handler();
}
}

View File

@ -4,6 +4,7 @@
#include <unordered_map>
#include <functional>
#include <vector>
#include "../interfaces/IInputHandler.h"
/**
* InputManager - Centralized input handling system
@ -15,7 +16,7 @@
* - Support event handler registration
* - Implement game-specific input logic (DAS/ARR)
*/
class InputManager {
class InputManager : public IInputHandler {
public:
// Event handler types
using KeyHandler = std::function<void(SDL_Scancode key, bool pressed)>;
@ -27,7 +28,16 @@ public:
InputManager();
~InputManager() = default;
// Core input processing
// IInputHandler interface implementation
bool processEvent(const SDL_Event& event) override;
void update(double deltaTime) override;
bool isKeyCurrentlyPressed(SDL_Scancode scancode) const override;
bool isKeyJustPressed(SDL_Scancode scancode) const override;
bool isKeyJustReleased(SDL_Scancode scancode) const override;
bool isQuitRequested() const override;
void reset() override;
// Existing InputManager methods
void processEvents();
void update(float deltaTime);
@ -98,6 +108,7 @@ private:
void handleMouseButtonEvent(const SDL_MouseButtonEvent& event);
void handleMouseMotionEvent(const SDL_MouseMotionEvent& event);
void handleWindowEvent(const SDL_WindowEvent& event);
void handleQuitEvent();
void updateInputState();
void updateDAS(float deltaTime);
void resetDAS();

View File

@ -0,0 +1,64 @@
#pragma once
#include <SDL3/SDL.h>
#include <string>
/**
* @brief Abstract interface for asset loading operations
*
* Provides a common interface for loading different types of assets,
* enabling dependency injection and easier testing.
*/
class IAssetLoader {
public:
virtual ~IAssetLoader() = default;
/**
* @brief Load a texture from file (interface method)
* @param path Path to the texture file
* @return Pointer to loaded SDL_Texture, nullptr on failure
*/
virtual SDL_Texture* loadTextureFromPath(const std::string& path) = 0;
/**
* @brief Load a font from file (interface method)
* @param name Font identifier/name
* @param path Path to the font file
* @param size Font size in pixels
* @return true if font was loaded successfully, false otherwise
*/
virtual bool loadFontAsset(const std::string& name, const std::string& path, int size) = 0;
/**
* @brief Load an audio file (interface method)
* @param name Audio identifier/name
* @param path Path to the audio file
* @return true if audio was loaded successfully, false otherwise
*/
virtual bool loadAudioAsset(const std::string& name, const std::string& path) = 0;
/**
* @brief Get a previously loaded texture (interface method)
* @param name Texture identifier/name
* @return Pointer to SDL_Texture, nullptr if not found
*/
virtual SDL_Texture* getTextureAsset(const std::string& name) = 0;
/**
* @brief Check if an asset exists
* @param name Asset identifier/name
* @return true if asset exists, false otherwise
*/
virtual bool hasAsset(const std::string& name) const = 0;
/**
* @brief Unload a specific asset
* @param name Asset identifier/name
*/
virtual void unloadAsset(const std::string& name) = 0;
/**
* @brief Unload all assets
*/
virtual void unloadAll() = 0;
};

View File

@ -0,0 +1,55 @@
#pragma once
#include <string>
/**
* @brief Abstract interface for audio system operations
*
* Provides a common interface for audio playback, enabling
* dependency injection and easier testing.
*/
class IAudioSystem {
public:
virtual ~IAudioSystem() = default;
/**
* @brief Play a sound effect
* @param name Sound effect name/identifier
*/
virtual void playSound(const std::string& name) = 0;
/**
* @brief Play background music
* @param name Music track name/identifier
*/
virtual void playMusic(const std::string& name) = 0;
/**
* @brief Stop currently playing music
*/
virtual void stopMusic() = 0;
/**
* @brief Set master volume for all audio
* @param volume Volume level (0.0 to 1.0)
*/
virtual void setMasterVolume(float volume) = 0;
/**
* @brief Set music volume
* @param volume Volume level (0.0 to 1.0)
*/
virtual void setMusicVolume(float volume) = 0;
/**
* @brief Set sound effects volume
* @param volume Volume level (0.0 to 1.0)
*/
virtual void setSoundVolume(float volume) = 0;
/**
* @brief Check if music is currently playing
* @return true if music is playing, false otherwise
*/
virtual bool isMusicPlaying() const = 0;
};

View File

@ -0,0 +1,73 @@
#pragma once
/**
* @brief Abstract interface for game rules
*
* Provides a common interface for different Tetris rule implementations,
* enabling different game modes and rule variations.
*/
class IGameRules {
public:
virtual ~IGameRules() = default;
/**
* @brief Calculate score for cleared lines
* @param linesCleared Number of lines cleared simultaneously
* @param level Current game level
* @return Score points to award
*/
virtual int calculateScore(int linesCleared, int level) const = 0;
/**
* @brief Get gravity speed for a given level
* @param level Game level
* @return Time in milliseconds for one gravity drop
*/
virtual double getGravitySpeed(int level) const = 0;
/**
* @brief Check if level should increase
* @param totalLines Total lines cleared so far
* @param currentLevel Current game level
* @return true if level should increase, false otherwise
*/
virtual bool shouldLevelUp(int totalLines, int currentLevel) const = 0;
/**
* @brief Calculate next level based on lines cleared
* @param totalLines Total lines cleared so far
* @param startLevel Starting level
* @return New level
*/
virtual int calculateLevel(int totalLines, int startLevel) const = 0;
/**
* @brief Get soft drop speed multiplier
* @return Multiplier for gravity when soft dropping
*/
virtual double getSoftDropMultiplier() const = 0;
/**
* @brief Get hard drop score per cell
* @return Points awarded per cell for hard drop
*/
virtual int getHardDropScore() const = 0;
/**
* @brief Get soft drop score per cell
* @return Points awarded per cell for soft drop
*/
virtual int getSoftDropScore() const = 0;
/**
* @brief Check if T-spins are enabled in this rule set
* @return true if T-spins are supported, false otherwise
*/
virtual bool supportsTSpins() const = 0;
/**
* @brief Check if hold feature is enabled in this rule set
* @return true if hold is supported, false otherwise
*/
virtual bool supportsHold() const = 0;
};

View File

@ -0,0 +1,59 @@
#pragma once
#include <SDL3/SDL.h>
/**
* @brief Abstract interface for input handling operations
*
* Provides a common interface for input processing,
* enabling dependency injection and easier testing.
*/
class IInputHandler {
public:
virtual ~IInputHandler() = default;
/**
* @brief Process SDL events
* @param event SDL event to process
* @return true if event was handled, false otherwise
*/
virtual bool processEvent(const SDL_Event& event) = 0;
/**
* @brief Update input state (called per frame)
* @param deltaTime Time elapsed since last frame in milliseconds
*/
virtual void update(double deltaTime) = 0;
/**
* @brief Check if a key is currently pressed (interface method)
* @param scancode SDL scancode of the key
* @return true if key is pressed, false otherwise
*/
virtual bool isKeyCurrentlyPressed(SDL_Scancode scancode) const = 0;
/**
* @brief Check if a key was just pressed this frame
* @param scancode SDL scancode of the key
* @return true if key was just pressed, false otherwise
*/
virtual bool isKeyJustPressed(SDL_Scancode scancode) const = 0;
/**
* @brief Check if a key was just released this frame
* @param scancode SDL scancode of the key
* @return true if key was just released, false otherwise
*/
virtual bool isKeyJustReleased(SDL_Scancode scancode) const = 0;
/**
* @brief Check if quit was requested (e.g., Alt+F4, window close)
* @return true if quit was requested, false otherwise
*/
virtual bool isQuitRequested() const = 0;
/**
* @brief Reset input state (typically called at frame start)
*/
virtual void reset() = 0;
};

View File

@ -0,0 +1,56 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
/**
* @brief Abstract interface for rendering operations
*
* Provides a common interface for different rendering implementations,
* enabling dependency injection and easier testing.
*/
class IRenderer {
public:
virtual ~IRenderer() = default;
/**
* @brief Clear the render target with specified color (interface method)
* @param r Red component (0-255)
* @param g Green component (0-255)
* @param b Blue component (0-255)
* @param a Alpha component (0-255)
*/
virtual void clearScreen(uint8_t r, uint8_t g, uint8_t b, uint8_t a) = 0;
/**
* @brief Present the rendered frame to the display
*/
virtual void present() = 0;
/**
* @brief Get the underlying SDL renderer for direct operations
* @return Pointer to SDL_Renderer
* @note This is provided for compatibility with existing code
*/
virtual SDL_Renderer* getSDLRenderer() = 0;
/**
* @brief Get the current window size (interface method)
* @param width Output parameter for window width
* @param height Output parameter for window height
*/
virtual void getWindowDimensions(int& width, int& height) const = 0;
/**
* @brief Set the render viewport
* @param viewport Viewport rectangle
*/
virtual void setViewport(const SDL_Rect* viewport) = 0;
/**
* @brief Set the render scale
* @param scaleX Horizontal scale factor
* @param scaleY Vertical scale factor
*/
virtual void setScale(float scaleX, float scaleY) = 0;
};

View File

@ -1,5 +1,5 @@
#include "StateManager.h"
#include "../graphics/RenderManager.h"
#include "../../graphics/renderers/RenderManager.h"
#include <SDL3/SDL.h>
StateManager::StateManager(AppState initial)