feat: implement textured line clear effects and refine UI alignment

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

View File

@ -0,0 +1,224 @@
#include "StateManager.h"
#include "../../graphics/renderers/RenderManager.h"
#include <SDL3/SDL.h>
StateManager::StateManager(AppState initial)
: m_currentState(initial)
, m_previousState(initial)
{
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "StateManager initialized with state: %s", getStateName(initial));
}
void StateManager::registerEventHandler(AppState state, EventHandler handler) {
if (!isValidState(state)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Invalid state for event handler registration");
return;
}
m_eventHandlers[stateToInt(state)].push_back(std::move(handler));
SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Event handler registered for state: %s", getStateName(state));
}
void StateManager::registerUpdateHandler(AppState state, UpdateHandler handler) {
if (!isValidState(state)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Invalid state for update handler registration");
return;
}
m_updateHandlers[stateToInt(state)].push_back(std::move(handler));
SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Update handler registered for state: %s", getStateName(state));
}
void StateManager::registerRenderHandler(AppState state, RenderHandler handler) {
if (!isValidState(state)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Invalid state for render handler registration");
return;
}
m_renderHandlers[stateToInt(state)].push_back(std::move(handler));
SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Render handler registered for state: %s", getStateName(state));
}
void StateManager::registerOnEnter(AppState state, Hook hook) {
if (!isValidState(state)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Invalid state for enter hook registration");
return;
}
m_onEnterHooks[stateToInt(state)].push_back(std::move(hook));
SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Enter hook registered for state: %s", getStateName(state));
}
void StateManager::registerOnExit(AppState state, Hook hook) {
if (!isValidState(state)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Invalid state for exit hook registration");
return;
}
m_onExitHooks[stateToInt(state)].push_back(std::move(hook));
SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Exit hook registered for state: %s", getStateName(state));
}
// Legacy compatibility: overload accepting a no-arg function as handler
void StateManager::registerHandler(AppState s, std::function<void()> h) {
EventHandler wrapper = [h = std::move(h)](const SDL_Event&) { h(); };
registerEventHandler(s, std::move(wrapper));
}
bool StateManager::setState(AppState newState) {
if (!isValidState(newState)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Attempted to set invalid state");
return false;
}
if (newState == m_currentState) {
SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Attempted to set same state: %s", getStateName(newState));
return true;
}
if (!canTransitionTo(newState)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Invalid state transition from %s to %s",
getStateName(m_currentState), getStateName(newState));
return false;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "State transition: %s -> %s",
getStateName(m_currentState), getStateName(newState));
// Persistent trace for debugging abrupt exits
{
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "setState start %s -> %s\n", getStateName(m_currentState), getStateName(newState)); fclose(f); }
}
// Execute exit hooks for current state
executeExitHooks(m_currentState);
// Update state
m_previousState = m_currentState;
m_currentState = newState;
// Execute enter hooks for new state
executeEnterHooks(m_currentState);
// Trace completion
{
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "setState end %s\n", getStateName(m_currentState)); fclose(f); }
}
return true;
}
void StateManager::handleEvent(const SDL_Event& event) {
auto it = m_eventHandlers.find(stateToInt(m_currentState));
if (it == m_eventHandlers.end()) {
return;
}
for (auto& handler : it->second) {
try {
handler(event);
} catch (const std::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Exception in event handler for state %s: %s",
getStateName(m_currentState), e.what());
}
}
}
void StateManager::update(float deltaTime) {
auto it = m_updateHandlers.find(stateToInt(m_currentState));
if (it == m_updateHandlers.end()) {
return;
}
for (auto& handler : it->second) {
try {
handler(deltaTime);
} catch (const std::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Exception in update handler for state %s: %s",
getStateName(m_currentState), e.what());
}
}
}
void StateManager::render(RenderManager& renderer) {
auto it = m_renderHandlers.find(stateToInt(m_currentState));
if (it == m_renderHandlers.end()) {
return;
}
for (auto& handler : it->second) {
try {
handler(renderer);
} catch (const std::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Exception in render handler for state %s: %s",
getStateName(m_currentState), e.what());
}
}
}
bool StateManager::isValidState(AppState state) const {
// All enum values are currently valid
return static_cast<int>(state) >= static_cast<int>(AppState::Loading) &&
static_cast<int>(state) <= static_cast<int>(AppState::GameOver);
}
bool StateManager::canTransitionTo(AppState newState) const {
// For now, allow all transitions. Can be enhanced later with state machine validation
return isValidState(newState);
}
const char* StateManager::getStateName(AppState state) const {
switch (state) {
case AppState::Loading: return "Loading";
case AppState::Menu: return "Menu";
case AppState::LevelSelector: return "LevelSelector";
case AppState::Playing: return "Playing";
case AppState::LevelSelect: return "LevelSelect";
case AppState::GameOver: return "GameOver";
default: return "Unknown";
}
}
void StateManager::executeEnterHooks(AppState state) {
auto it = m_onEnterHooks.find(stateToInt(state));
if (it == m_onEnterHooks.end()) {
return;
}
int idx = 0;
for (auto& hook : it->second) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Executing enter hook %d for state %s", idx, getStateName(state));
// Also write to trace file for persistent record
{
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "executeEnterHook %d %s\n", idx, getStateName(state)); fclose(f); }
}
try {
hook();
} catch (const std::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Exception in enter hook for state %s: %s",
getStateName(state), e.what());
}
++idx;
}
}
void StateManager::executeExitHooks(AppState state) {
auto it = m_onExitHooks.find(stateToInt(state));
if (it == m_onExitHooks.end()) {
return;
}
int idx = 0;
for (auto& hook : it->second) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Executing exit hook %d for state %s", idx, getStateName(state));
{
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "executeExitHook %d %s\n", idx, getStateName(state)); fclose(f); }
}
try {
hook();
} catch (const std::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Exception in exit hook for state %s: %s",
getStateName(state), e.what());
}
++idx;
}
}