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:
@ -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);
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
92
src/core/ServiceContainer.h
Normal file
92
src/core/ServiceContainer.h
Normal 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();
|
||||
}
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
// Don’t 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 (don’t 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; // don’t 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() {
|
||||
@ -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;
|
||||
@ -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();
|
||||
}
|
||||
@ -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);
|
||||
93
src/core/container/ServiceContainer.h
Normal file
93
src/core/container/ServiceContainer.h
Normal 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;
|
||||
};
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
64
src/core/interfaces/IAssetLoader.h
Normal file
64
src/core/interfaces/IAssetLoader.h
Normal 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;
|
||||
};
|
||||
55
src/core/interfaces/IAudioSystem.h
Normal file
55
src/core/interfaces/IAudioSystem.h
Normal 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;
|
||||
};
|
||||
73
src/core/interfaces/IGameRules.h
Normal file
73
src/core/interfaces/IGameRules.h
Normal 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;
|
||||
};
|
||||
59
src/core/interfaces/IInputHandler.h
Normal file
59
src/core/interfaces/IInputHandler.h
Normal 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;
|
||||
};
|
||||
56
src/core/interfaces/IRenderer.h
Normal file
56
src/core/interfaces/IRenderer.h
Normal 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;
|
||||
};
|
||||
@ -1,5 +1,5 @@
|
||||
#include "StateManager.h"
|
||||
#include "../graphics/RenderManager.h"
|
||||
#include "../../graphics/renderers/RenderManager.h"
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
StateManager::StateManager(AppState initial)
|
||||
Reference in New Issue
Block a user