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

@ -17,12 +17,12 @@
#include "audio/Audio.h"
#include "audio/SoundEffect.h"
#include "gameplay/Game.h"
#include "gameplay/core/Game.h"
#include "persistence/Scores.h"
#include "graphics/Starfield.h"
#include "Starfield3D.h"
#include "graphics/Font.h"
#include "gameplay/LineEffect.h"
#include "graphics/effects/Starfield.h"
#include "graphics/effects/Starfield3D.h"
#include "graphics/ui/Font.h"
#include "gameplay/effects/LineEffect.h"
#include "states/State.h"
#include "states/LoadingState.h"
#include "states/MenuState.h"
@ -268,7 +268,7 @@ static void drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musi
// Starfield now managed by Starfield class
// State manager integration (scaffolded in StateManager.h)
#include "core/StateManager.h"
#include "core/state/StateManager.h"
// -----------------------------------------------------------------------------
// Intro/Menu state variables
@ -664,28 +664,10 @@ int main(int, char **)
stateMgr.registerOnEnter(AppState::LevelSelector, [&](){ levelSelectorState->onEnter(); });
stateMgr.registerOnExit(AppState::LevelSelector, [&](){ levelSelectorState->onExit(); });
// Combined Playing state handler: run playingState handler and inline gameplay mapping
// Combined Playing state handler: run playingState handler
stateMgr.registerHandler(AppState::Playing, [&](const SDL_Event& e){
// First give the PlayingState a chance to handle the event
playingState->handleEvent(e);
// Then perform inline gameplay mappings (gravity/rotation/hard-drop/hold)
if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
if (!game.isPaused()) {
if (e.key.scancode == SDL_SCANCODE_SPACE) {
game.hardDrop();
}
else if (e.key.scancode == SDL_SCANCODE_UP) {
game.rotate(+1);
}
else if (e.key.scancode == SDL_SCANCODE_Z || (e.key.mod & SDL_KMOD_SHIFT)) {
game.rotate(-1);
}
else if (e.key.scancode == SDL_SCANCODE_C || (e.key.mod & SDL_KMOD_CTRL)) {
game.holdCurrent();
}
}
}
});
stateMgr.registerOnEnter(AppState::Playing, [&](){ playingState->onEnter(); });
stateMgr.registerOnExit(AppState::Playing, [&](){ playingState->onExit(); });
@ -983,7 +965,8 @@ int main(int, char **)
// Update progress based on background loading
if (currentTrackLoading > 0 && !musicLoaded) {
currentTrackLoading = Audio::instance().getLoadedTrackCount();
if (Audio::instance().isLoadingComplete()) {
// If loading is complete OR we've loaded all expected tracks (handles potential thread cleanup hang)
if (Audio::instance().isLoadingComplete() || (totalTracks > 0 && currentTrackLoading >= totalTracks)) {
Audio::instance().shuffle(); // Shuffle once all tracks are loaded
musicLoaded = true;
}
@ -1006,6 +989,10 @@ int main(int, char **)
// Ensure we never exceed 100% and reach exactly 100% when everything is loaded
loadingProgress = std::min(1.0, loadingProgress);
// Fix floating point precision issues (0.2 + 0.7 + 0.1 can be 0.9999...)
if (loadingProgress > 0.99) loadingProgress = 1.0;
if (musicLoaded && timeProgress >= 0.1) {
loadingProgress = 1.0;
}
@ -1082,6 +1069,7 @@ int main(int, char **)
snprintf(bgPath, sizeof(bgPath), "assets/images/tetris_main_back_level%d.bmp", bgLevel);
SDL_Surface* levelBgSurface = SDL_LoadBMP(bgPath);
if (levelBgSurface) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loaded background for level %d: %s", bgLevel, bgPath);
nextLevelBackgroundTex = SDL_CreateTextureFromSurface(renderer, levelBgSurface);
SDL_DestroySurface(levelBgSurface);
// start fade transition
@ -1089,6 +1077,7 @@ int main(int, char **)
levelFadeElapsed = 0.0f;
cachedLevel = bgLevel;
} else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load background for level %d: %s (Error: %s)", bgLevel, bgPath, SDL_GetError());
// don't change textures if file missing
cachedLevel = -1;
}
@ -1427,9 +1416,14 @@ int main(int, char **)
drawPiece(renderer, blocksTex, game.current(), gridX, gridY, finalBlockSize, false);
}
// Handle line clearing effects
if (game.hasCompletedLines() && !lineEffect.isActive()) {
lineEffect.startLineClear(game.getCompletedLines(), static_cast<int>(gridX), static_cast<int>(gridY), static_cast<int>(finalBlockSize));
}
// Draw line clearing effects
if (lineEffect.isActive()) {
lineEffect.render(renderer, static_cast<int>(gridX), static_cast<int>(gridY), static_cast<int>(finalBlockSize));
lineEffect.render(renderer, blocksTex, static_cast<int>(gridX), static_cast<int>(gridY), static_cast<int>(finalBlockSize));
}
// Draw next piece preview