468 lines
16 KiB
C++
468 lines
16 KiB
C++
// LineEffect.cpp - Implementation of line clearing visual and audio effects
|
|
#include "LineEffect.h"
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#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<float>(rand()) / static_cast<float>(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<float>(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<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.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<Uint8>(alpha * 255.0f);
|
|
SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, adjustedAlpha);
|
|
float radius = size * 0.5f;
|
|
for (int iy = -static_cast<int>(radius); iy <= static_cast<int>(radius); ++iy) {
|
|
for (int ix = -static_cast<int>(radius); ix <= static_cast<int>(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<float>(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<Uint8>(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<int>(std::ceil(radius));
|
|
for (int iy = -intRadius; iy <= intRadius; ++iy) {
|
|
float dy = static_cast<float>(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<Uint8>(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<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;
|
|
dropProgress = 0.0f;
|
|
dropBlockSize = blockSize;
|
|
rowDropTargets.fill(0.0f);
|
|
particles.clear();
|
|
sparks.clear();
|
|
glowPulses.clear();
|
|
|
|
std::array<bool, Game::ROWS> 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<float>(clearedBelow * blockSize);
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
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<float>(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<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) {
|
|
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<Uint8>(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<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);
|
|
}
|
|
}
|
|
|
|
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];
|
|
}
|