// LineEffect.cpp - Implementation of line clearing visual and audio effects #include "LineEffect.h" #include #include #include "audio/Audio.h" #include "gameplay/core/Game.h" #ifndef M_PI #define M_PI 3.14159265358979323846 #endif namespace { float randRange(float min, float max) { return min + (static_cast(rand()) / static_cast(RAND_MAX)) * (max - min); } } LineEffect::Particle::Particle(float px, float py, Style particleStyle, SDL_Color tint) : x(px), y(py), size(0.0f), alpha(1.0f), life(0.0f), lifeSpan(0.0f), blockType(rand() % 7), color(tint), style(particleStyle) { float angle = randRange(0.0f, static_cast(M_PI) * 2.0f); float speed = (style == Style::Shard) ? randRange(140.0f, 260.0f) : randRange(70.0f, 140.0f); vx = std::cos(angle) * speed; vy = std::sin(angle) * speed - ((style == Style::Shard) ? randRange(40.0f, 110.0f) : randRange(10.0f, 40.0f)); size = (style == Style::Shard) ? randRange(8.0f, 16.0f) : randRange(5.0f, 10.0f); lifeSpan = (style == Style::Shard) ? randRange(0.70f, 1.20f) : randRange(1.00f, 1.50f); } void LineEffect::Particle::update(float dt) { life += dt; if (life >= lifeSpan) { alpha = 0.0f; return; } const float progress = life / lifeSpan; x += vx * dt; y += vy * dt; float gravity = (style == Style::Shard) ? 520.0f : 240.0f; vy += gravity * dt; vx *= (style == Style::Shard) ? 0.96f : 0.985f; float shrinkRate = (style == Style::Shard) ? 24.0f : 10.0f; size = std::max(2.0f, size - shrinkRate * dt); alpha = 1.0f - progress; } void LineEffect::Particle::render(SDL_Renderer* renderer, SDL_Texture* blocksTex) { if (alpha <= 0.0f) return; if (blocksTex) { Uint8 prevA = 255; SDL_GetTextureAlphaMod(blocksTex, &prevA); SDL_SetTextureAlphaMod(blocksTex, static_cast(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.0f, y - size/2.0f, size, size}; SDL_RenderTexture(renderer, blocksTex, &srcRect, &dstRect); SDL_SetTextureAlphaMod(blocksTex, prevA); } else { SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); Uint8 adjustedAlpha = static_cast(alpha * 255.0f); SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, adjustedAlpha); float radius = size * 0.5f; for (int iy = -static_cast(radius); iy <= static_cast(radius); ++iy) { for (int ix = -static_cast(radius); ix <= static_cast(radius); ++ix) { float dist2 = float(ix * ix + iy * iy); if (dist2 <= radius * radius) { SDL_RenderPoint(renderer, x + ix, y + iy); } } } } } LineEffect::Spark::Spark(float px, float py, SDL_Color tint) : x(px), y(py), vx(0.0f), vy(0.0f), life(0.0f), maxLife(randRange(0.40f, 0.80f)), thickness(randRange(1.0f, 2.0f)), color(tint) { float angle = randRange(0.0f, static_cast(M_PI) * 2.0f); float speed = randRange(240.0f, 520.0f); vx = std::cos(angle) * speed; vy = std::sin(angle) * speed - randRange(80.0f, 150.0f); } void LineEffect::Spark::update(float dt) { life += dt; x += vx * dt; y += vy * dt; vy += 420.0f * dt; vx *= 0.99f; } void LineEffect::Spark::render(SDL_Renderer* renderer) const { if (life >= maxLife) return; float progress = life / maxLife; float alpha = (1.0f - progress) * 255.0f; if (alpha <= 0.0f) return; SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, static_cast(alpha)); float trail = 0.018f * (1.2f - progress) * thickness; SDL_FPoint tail{ x - vx * trail, y - vy * trail }; SDL_RenderLine(renderer, x, y, tail.x, tail.y); } LineEffect::GlowPulse::GlowPulse(float px, float py, float baseR, float maxR, SDL_Color tint) : x(px), y(py), baseRadius(baseR), maxRadius(maxR), life(0.0f), maxLife(randRange(0.45f, 0.70f)), color(tint) { if (color.a == 0) color.a = 200; } void LineEffect::GlowPulse::update(float dt) { life += dt; if (life > maxLife) life = maxLife; } void LineEffect::GlowPulse::render(SDL_Renderer* renderer) const { if (life >= maxLife) return; float progress = life / maxLife; float radius = baseRadius + (maxRadius - baseRadius) * progress; float baseAlpha = (1.0f - progress) * (color.a / 255.0f); int intRadius = static_cast(std::ceil(radius)); for (int iy = -intRadius; iy <= intRadius; ++iy) { float dy = static_cast(iy); float rowWidth = std::sqrt(std::max(0.0f, radius * radius - dy * dy)); float falloff = std::max(0.0f, 1.0f - std::abs(dy) / radius); Uint8 alpha = static_cast(baseAlpha * falloff * 255.0f); if (alpha == 0) continue; SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, alpha); SDL_FRect segment{ x - rowWidth, y + dy, rowWidth * 2.0f, 1.4f }; SDL_RenderFillRect(renderer, &segment); } } 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(0.2f * sampleRate); lineClearSample.resize(duration * 2); // Stereo for (int i = 0; i < duration; ++i) { float t = static_cast(i) / sampleRate; float wave = std::sin(2.0f * M_PI * 440.0f * t) * 0.3f; // 440Hz sine wave int16_t sample = static_cast(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(0.4f * sampleRate); tetrisSample.resize(duration * 2); for (int i = 0; i < duration; ++i) { float t = static_cast(i) / sampleRate; float wave = std::sin(2.0f * M_PI * 880.0f * t) * 0.4f; // 880Hz sine wave int16_t sample = static_cast(wave * 32767.0f); tetrisSample[i * 2] = sample; // Left channel tetrisSample[i * 2 + 1] = sample; // Right channel } } void LineEffect::startLineClear(const std::vector& rows, int gridX, int gridY, int blockSize) { if (rows.empty()) return; clearingRows = rows; state = AnimationState::FLASH_WHITE; timer = 0.0f; dropProgress = 0.0f; dropBlockSize = blockSize; rowDropTargets.fill(0.0f); particles.clear(); sparks.clear(); glowPulses.clear(); std::array rowClearing{}; for (int row : rows) { if (row >= 0 && row < Game::ROWS) { rowClearing[row] = true; } } int clearedBelow = 0; for (int row = Game::ROWS - 1; row >= 0; --row) { if (rowClearing[row]) { ++clearedBelow; } else if (clearedBelow > 0) { rowDropTargets[row] = static_cast(clearedBelow * blockSize); } } // Create particles for each clearing row for (int row : rows) { createParticles(row, gridX, gridY, blockSize); } // Play appropriate sound playLineClearSound(static_cast(rows.size())); } void LineEffect::createParticles(int row, int gridX, int gridY, int blockSize) { const float centerY = gridY + row * blockSize + blockSize * 0.5f; for (int col = 0; col < Game::COLS; ++col) { float centerX = gridX + col * blockSize + blockSize * 0.5f; SDL_Color tint = pickFireColor(); spawnGlowPulse(centerX, centerY, static_cast(blockSize), tint); spawnShardBurst(centerX, centerY, tint); spawnSparkBurst(centerX, centerY, tint); } } void LineEffect::spawnShardBurst(float x, float y, SDL_Color tint) { int shardCount = 3 + rand() % 4; for (int i = 0; i < shardCount; ++i) { particles.emplace_back(x, y, Particle::Style::Shard, tint); } int emberCount = 2 + rand() % 3; for (int i = 0; i < emberCount; ++i) { particles.emplace_back(x, y, Particle::Style::Ember, tint); } } void LineEffect::spawnSparkBurst(float x, float y, SDL_Color tint) { int sparkCount = 4 + rand() % 5; for (int i = 0; i < sparkCount; ++i) { sparks.emplace_back(x, y, tint); } } void LineEffect::spawnGlowPulse(float x, float y, float blockSize, SDL_Color tint) { glowPulses.emplace_back(x, y, blockSize * 0.45f, blockSize * 2.3f, tint); } 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(deltaTime); updateSparks(deltaTime); updateGlowPulses(deltaTime); if (timer >= EXPLODE_DURATION) { state = AnimationState::BLOCKS_DROP; timer = 0.0f; } break; case AnimationState::BLOCKS_DROP: updateParticles(deltaTime); updateSparks(deltaTime); updateGlowPulses(deltaTime); dropProgress = std::min(1.0f, timer / DROP_DURATION); if (timer >= DROP_DURATION) { state = AnimationState::IDLE; clearingRows.clear(); particles.clear(); sparks.clear(); glowPulses.clear(); rowDropTargets.fill(0.0f); dropProgress = 0.0f; dropBlockSize = 0; return true; // Effect complete } break; case AnimationState::IDLE: return true; } return false; // Effect still running } void LineEffect::updateParticles(float dt) { for (auto& particle : particles) { particle.update(dt); } particles.erase( std::remove_if(particles.begin(), particles.end(), [](const Particle& p) { return !p.isAlive(); }), particles.end()); } void LineEffect::updateSparks(float dt) { for (auto& spark : sparks) { spark.update(dt); } sparks.erase( std::remove_if(sparks.begin(), sparks.end(), [](const Spark& s) { return !s.isAlive(); }), sparks.end()); } void LineEffect::updateGlowPulses(float dt) { for (auto& pulse : glowPulses) { pulse.update(dt); } glowPulses.erase( std::remove_if(glowPulses.begin(), glowPulses.end(), [](const GlowPulse& p) { return !p.isAlive(); }), glowPulses.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; } } float LineEffect::getRowDropOffset(int row) const { if (state != AnimationState::BLOCKS_DROP) { return 0.0f; } if (row < 0 || row >= Game::ROWS) { return 0.0f; } float target = rowDropTargets[row]; if (target <= 0.0f) { return 0.0f; } float eased = dropProgress * dropProgress * dropProgress; return target * eased; } 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(flashIntensity * 180.0f); for (int row : clearingRows) { SDL_SetRenderDrawColor(renderer, 255, 255, 255, alpha); SDL_FRect flashRect = { static_cast(gridX - 4), static_cast(gridY + row * blockSize - 4), static_cast(10 * blockSize + 8), static_cast(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) { renderGlowPulses(); renderSparks(); renderParticleGlows(); for (auto& particle : particles) { particle.render(renderer, blocksTex); } } void LineEffect::renderGlowPulses() { if (glowPulses.empty()) return; SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_ADD); for (const auto& pulse : glowPulses) { pulse.render(renderer); } SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); } void LineEffect::renderSparks() { if (sparks.empty()) return; SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_ADD); for (const auto& spark : sparks) { spark.render(renderer); } SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); } void LineEffect::renderParticleGlows() { if (particles.empty()) return; SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_ADD); for (const auto& particle : particles) { if (particle.alpha <= 0.0f) continue; float radius = particle.size * 0.6f; SDL_SetRenderDrawColor(renderer, particle.color.r, particle.color.g, particle.color.b, static_cast(particle.alpha * 150.0f)); SDL_FRect glowRect{ particle.x - radius, particle.y - radius, radius * 2.0f, radius * 2.0f }; SDL_RenderFillRect(renderer, &glowRect); } SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); } void LineEffect::playLineClearSound(int lineCount) { // Choose appropriate sound based on line count const std::vector* 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); } } SDL_Color LineEffect::pickFireColor() const { static const SDL_Color palette[] = { {255, 200, 120, 210}, {255, 150, 90, 220}, {255, 100, 160, 200}, {120, 200, 255, 200}, {255, 255, 200, 210} }; const size_t count = sizeof(palette) / sizeof(palette[0]); return palette[rand() % count]; }