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:
280
src/gameplay/effects/LineEffect.cpp
Normal file
280
src/gameplay/effects/LineEffect.cpp
Normal file
@ -0,0 +1,280 @@
|
||||
// LineEffect.cpp - Implementation of line clearing visual and audio effects
|
||||
#include "LineEffect.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include "audio/Audio.h"
|
||||
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.14159265358979323846
|
||||
#endif
|
||||
|
||||
LineEffect::Particle::Particle(float px, float py)
|
||||
: x(px), y(py), size(6.0f + static_cast<float>(rand()) / RAND_MAX * 12.0f), alpha(1.0f) {
|
||||
|
||||
// Random velocity for explosive effect
|
||||
float angle = static_cast<float>(rand()) / RAND_MAX * 2.0f * M_PI;
|
||||
float speed = 80.0f + static_cast<float>(rand()) / RAND_MAX * 150.0f;
|
||||
vx = std::cos(angle) * speed;
|
||||
vy = std::sin(angle) * speed - 30.0f;
|
||||
|
||||
// Random block type for texture
|
||||
blockType = rand() % 7;
|
||||
|
||||
// Fallback colors if texture not available
|
||||
switch (blockType % 4) {
|
||||
case 0: color = {255, 140, 30, 255}; break;
|
||||
case 1: color = {255, 255, 100, 255}; break;
|
||||
case 2: color = {255, 255, 255, 255}; break;
|
||||
case 3: color = {255, 100, 100, 255}; break;
|
||||
}
|
||||
}
|
||||
|
||||
void LineEffect::Particle::update() {
|
||||
x += vx * 0.016f;
|
||||
y += vy * 0.016f;
|
||||
vy += 250.0f * 0.016f;
|
||||
vx *= 0.98f;
|
||||
alpha -= 0.08f; // Slower fade for blocks
|
||||
if (alpha < 0.0f) alpha = 0.0f;
|
||||
|
||||
if (size > 2.0f) size -= 0.05f;
|
||||
}
|
||||
|
||||
void LineEffect::Particle::render(SDL_Renderer* renderer, SDL_Texture* blocksTex) {
|
||||
if (alpha <= 0.0f) return;
|
||||
|
||||
if (blocksTex) {
|
||||
// Render textured block fragment
|
||||
Uint8 prevA = 255;
|
||||
SDL_GetTextureAlphaMod(blocksTex, &prevA);
|
||||
SDL_SetTextureAlphaMod(blocksTex, static_cast<Uint8>(alpha * 255.0f));
|
||||
|
||||
const int SPRITE_SIZE = 90;
|
||||
float srcX = blockType * SPRITE_SIZE + 2;
|
||||
float srcY = 2;
|
||||
float srcW = SPRITE_SIZE - 4;
|
||||
float srcH = SPRITE_SIZE - 4;
|
||||
|
||||
SDL_FRect srcRect = {srcX, srcY, srcW, srcH};
|
||||
SDL_FRect dstRect = {x - size/2, y - size/2, size, size};
|
||||
|
||||
SDL_RenderTexture(renderer, blocksTex, &srcRect, &dstRect);
|
||||
|
||||
SDL_SetTextureAlphaMod(blocksTex, prevA);
|
||||
} else {
|
||||
// Fallback to circle rendering
|
||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||
Uint8 adjustedAlpha = static_cast<Uint8>(alpha * 255.0f);
|
||||
SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, adjustedAlpha);
|
||||
|
||||
for (int i = 0; i < static_cast<int>(size); ++i) {
|
||||
for (int j = 0; j < static_cast<int>(size); ++j) {
|
||||
float dx = i - size/2.0f;
|
||||
float dy = j - size/2.0f;
|
||||
if (dx*dx + dy*dy <= (size/2.0f)*(size/2.0f)) {
|
||||
SDL_RenderPoint(renderer, x + dx, y + dy);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LineEffect::LineEffect() : renderer(nullptr), state(AnimationState::IDLE), timer(0.0f),
|
||||
rng(std::random_device{}()), audioStream(nullptr) {
|
||||
}
|
||||
|
||||
LineEffect::~LineEffect() {
|
||||
shutdown();
|
||||
}
|
||||
|
||||
bool LineEffect::init(SDL_Renderer* r) {
|
||||
renderer = r;
|
||||
initAudio();
|
||||
return true;
|
||||
}
|
||||
|
||||
void LineEffect::shutdown() {
|
||||
// No separate audio stream anymore; SFX go through shared Audio mixer
|
||||
}
|
||||
|
||||
void LineEffect::initAudio() {
|
||||
// Generate simple beep sounds procedurally (fallback when voice SFX not provided)
|
||||
|
||||
// Generate a simple line clear beep (440Hz for 0.2 seconds)
|
||||
int sampleRate = 44100;
|
||||
int duration = static_cast<int>(0.2f * sampleRate);
|
||||
lineClearSample.resize(duration * 2); // Stereo
|
||||
|
||||
for (int i = 0; i < duration; ++i) {
|
||||
float t = static_cast<float>(i) / sampleRate;
|
||||
float wave = std::sin(2.0f * M_PI * 440.0f * t) * 0.3f; // 440Hz sine wave
|
||||
int16_t sample = static_cast<int16_t>(wave * 32767.0f);
|
||||
lineClearSample[i * 2] = sample; // Left channel
|
||||
lineClearSample[i * 2 + 1] = sample; // Right channel
|
||||
}
|
||||
|
||||
// Generate a higher pitched tetris sound (880Hz for 0.4 seconds)
|
||||
duration = static_cast<int>(0.4f * sampleRate);
|
||||
tetrisSample.resize(duration * 2);
|
||||
|
||||
for (int i = 0; i < duration; ++i) {
|
||||
float t = static_cast<float>(i) / sampleRate;
|
||||
float wave = std::sin(2.0f * M_PI * 880.0f * t) * 0.4f; // 880Hz sine wave
|
||||
int16_t sample = static_cast<int16_t>(wave * 32767.0f);
|
||||
tetrisSample[i * 2] = sample; // Left channel
|
||||
tetrisSample[i * 2 + 1] = sample; // Right channel
|
||||
}
|
||||
}
|
||||
|
||||
void LineEffect::startLineClear(const std::vector<int>& rows, int gridX, int gridY, int blockSize) {
|
||||
if (rows.empty()) return;
|
||||
|
||||
clearingRows = rows;
|
||||
state = AnimationState::FLASH_WHITE;
|
||||
timer = 0.0f;
|
||||
particles.clear();
|
||||
|
||||
// Create particles for each clearing row
|
||||
for (int row : rows) {
|
||||
createParticles(row, gridX, gridY, blockSize);
|
||||
}
|
||||
|
||||
// Play appropriate sound
|
||||
playLineClearSound(static_cast<int>(rows.size()));
|
||||
}
|
||||
|
||||
void LineEffect::createParticles(int row, int gridX, int gridY, int blockSize) {
|
||||
// Create particles spread across the row with explosive pattern
|
||||
int particlesPerRow = 35; // More particles for dramatic explosion effect
|
||||
|
||||
for (int i = 0; i < particlesPerRow; ++i) {
|
||||
// Create particles along the entire row width
|
||||
float x = gridX + (static_cast<float>(i) / (particlesPerRow - 1)) * (10 * blockSize);
|
||||
float y = gridY + row * blockSize + blockSize / 2.0f;
|
||||
|
||||
// Add some randomness to position
|
||||
x += (static_cast<float>(rand()) / RAND_MAX - 0.5f) * blockSize * 0.8f;
|
||||
y += (static_cast<float>(rand()) / RAND_MAX - 0.5f) * blockSize * 0.6f;
|
||||
|
||||
particles.emplace_back(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
bool LineEffect::update(float deltaTime) {
|
||||
if (state == AnimationState::IDLE) return true;
|
||||
|
||||
timer += deltaTime;
|
||||
|
||||
switch (state) {
|
||||
case AnimationState::FLASH_WHITE:
|
||||
if (timer >= FLASH_DURATION) {
|
||||
state = AnimationState::EXPLODE_BLOCKS;
|
||||
timer = 0.0f;
|
||||
}
|
||||
break;
|
||||
|
||||
case AnimationState::EXPLODE_BLOCKS:
|
||||
updateParticles();
|
||||
if (timer >= EXPLODE_DURATION) {
|
||||
state = AnimationState::BLOCKS_DROP;
|
||||
timer = 0.0f;
|
||||
}
|
||||
break;
|
||||
|
||||
case AnimationState::BLOCKS_DROP:
|
||||
updateParticles();
|
||||
if (timer >= DROP_DURATION) {
|
||||
state = AnimationState::IDLE;
|
||||
clearingRows.clear();
|
||||
particles.clear();
|
||||
return true; // Effect complete
|
||||
}
|
||||
break;
|
||||
|
||||
case AnimationState::IDLE:
|
||||
return true;
|
||||
}
|
||||
|
||||
return false; // Effect still running
|
||||
}
|
||||
|
||||
void LineEffect::updateParticles() {
|
||||
// Update all particles
|
||||
for (auto& particle : particles) {
|
||||
particle.update();
|
||||
}
|
||||
|
||||
// Remove dead particles
|
||||
particles.erase(
|
||||
std::remove_if(particles.begin(), particles.end(),
|
||||
[](const Particle& p) { return !p.isAlive(); }),
|
||||
particles.end()
|
||||
);
|
||||
}
|
||||
|
||||
void LineEffect::render(SDL_Renderer* renderer, SDL_Texture* blocksTex, int gridX, int gridY, int blockSize) {
|
||||
if (state == AnimationState::IDLE) return;
|
||||
|
||||
switch (state) {
|
||||
case AnimationState::FLASH_WHITE:
|
||||
renderFlash(gridX, gridY, blockSize);
|
||||
break;
|
||||
|
||||
case AnimationState::EXPLODE_BLOCKS:
|
||||
renderExplosion(blocksTex);
|
||||
break;
|
||||
|
||||
case AnimationState::BLOCKS_DROP:
|
||||
renderExplosion(blocksTex);
|
||||
break;
|
||||
|
||||
case AnimationState::IDLE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void LineEffect::renderFlash(int gridX, int gridY, int blockSize) {
|
||||
// Create a flashing white effect with varying opacity
|
||||
float progress = timer / FLASH_DURATION;
|
||||
float flashIntensity = std::sin(progress * M_PI * 6.0f) * 0.5f + 0.5f;
|
||||
|
||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||
Uint8 alpha = static_cast<Uint8>(flashIntensity * 180.0f);
|
||||
|
||||
for (int row : clearingRows) {
|
||||
SDL_SetRenderDrawColor(renderer, 255, 255, 255, alpha);
|
||||
SDL_FRect flashRect = {
|
||||
static_cast<float>(gridX - 4),
|
||||
static_cast<float>(gridY + row * blockSize - 4),
|
||||
static_cast<float>(10 * blockSize + 8),
|
||||
static_cast<float>(blockSize + 8)
|
||||
};
|
||||
SDL_RenderFillRect(renderer, &flashRect);
|
||||
|
||||
SDL_SetRenderDrawColor(renderer, 100, 150, 255, alpha / 2);
|
||||
for (int i = 1; i <= 3; ++i) {
|
||||
SDL_FRect glowRect = {
|
||||
flashRect.x - i,
|
||||
flashRect.y - i,
|
||||
flashRect.w + 2*i,
|
||||
flashRect.h + 2*i
|
||||
};
|
||||
SDL_RenderRect(renderer, &glowRect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LineEffect::renderExplosion(SDL_Texture* blocksTex) {
|
||||
for (auto& particle : particles) {
|
||||
particle.render(renderer, blocksTex);
|
||||
}
|
||||
}
|
||||
|
||||
void LineEffect::playLineClearSound(int lineCount) {
|
||||
// Choose appropriate sound based on line count
|
||||
const std::vector<int16_t>* sample = (lineCount == 4) ? &tetrisSample : &lineClearSample;
|
||||
if (sample && !sample->empty()) {
|
||||
// Mix via shared Audio device so it layers with music
|
||||
Audio::instance().playSfx(*sample, 2, 44100, (lineCount == 4) ? 0.9f : 0.7f);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user