Merge branch 'feature/BackgroundFadeInOut' into develop
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@ -70,4 +70,7 @@ dist_package/
|
||||
# Local environment files (if any)
|
||||
.env
|
||||
|
||||
# Ignore local settings file
|
||||
settings.ini
|
||||
|
||||
# End of .gitignore
|
||||
|
||||
@ -6,10 +6,10 @@ Fullscreen=1
|
||||
|
||||
[Audio]
|
||||
Music=1
|
||||
Sound=0
|
||||
Sound=1
|
||||
|
||||
[Player]
|
||||
Name=Player
|
||||
Name=GREGOR
|
||||
|
||||
[Debug]
|
||||
Enabled=0
|
||||
Enabled=1
|
||||
|
||||
@ -3,82 +3,144 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include "audio/Audio.h"
|
||||
#include "gameplay/core/Game.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;
|
||||
}
|
||||
namespace {
|
||||
float randRange(float min, float max) {
|
||||
return min + (static_cast<float>(rand()) / static_cast<float>(RAND_MAX)) * (max - min);
|
||||
}
|
||||
}
|
||||
|
||||
void LineEffect::Particle::update() {
|
||||
x += vx * 0.016f;
|
||||
y += vy * 0.016f;
|
||||
vy += 250.0f * 0.016f;
|
||||
vx *= 0.98f;
|
||||
alpha -= 0.025f; // Slower fade for blocks (longer visibility)
|
||||
if (alpha < 0.0f) alpha = 0.0f;
|
||||
|
||||
if (size > 2.0f) size -= 0.05f;
|
||||
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) {
|
||||
// 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_FRect dstRect = {x - size/2.0f, y - size/2.0f, 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);
|
||||
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) {
|
||||
}
|
||||
@ -133,6 +195,8 @@ void LineEffect::startLineClear(const std::vector<int>& rows, int gridX, int gri
|
||||
state = AnimationState::FLASH_WHITE;
|
||||
timer = 0.0f;
|
||||
particles.clear();
|
||||
sparks.clear();
|
||||
glowPulses.clear();
|
||||
|
||||
// Create particles for each clearing row
|
||||
for (int row : rows) {
|
||||
@ -144,22 +208,38 @@ void LineEffect::startLineClear(const std::vector<int>& rows, int gridX, int gri
|
||||
}
|
||||
|
||||
void LineEffect::createParticles(int row, int gridX, int gridY, int blockSize) {
|
||||
// Create particles spread across the row with explosive pattern
|
||||
int particlesPerRow = 60; // 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);
|
||||
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;
|
||||
|
||||
@ -174,7 +254,9 @@ bool LineEffect::update(float deltaTime) {
|
||||
break;
|
||||
|
||||
case AnimationState::EXPLODE_BLOCKS:
|
||||
updateParticles();
|
||||
updateParticles(deltaTime);
|
||||
updateSparks(deltaTime);
|
||||
updateGlowPulses(deltaTime);
|
||||
if (timer >= EXPLODE_DURATION) {
|
||||
state = AnimationState::BLOCKS_DROP;
|
||||
timer = 0.0f;
|
||||
@ -182,11 +264,15 @@ bool LineEffect::update(float deltaTime) {
|
||||
break;
|
||||
|
||||
case AnimationState::BLOCKS_DROP:
|
||||
updateParticles();
|
||||
updateParticles(deltaTime);
|
||||
updateSparks(deltaTime);
|
||||
updateGlowPulses(deltaTime);
|
||||
if (timer >= DROP_DURATION) {
|
||||
state = AnimationState::IDLE;
|
||||
clearingRows.clear();
|
||||
particles.clear();
|
||||
sparks.clear();
|
||||
glowPulses.clear();
|
||||
return true; // Effect complete
|
||||
}
|
||||
break;
|
||||
@ -198,18 +284,34 @@ bool LineEffect::update(float deltaTime) {
|
||||
return false; // Effect still running
|
||||
}
|
||||
|
||||
void LineEffect::updateParticles() {
|
||||
// Update all particles
|
||||
void LineEffect::updateParticles(float dt) {
|
||||
for (auto& particle : particles) {
|
||||
particle.update();
|
||||
particle.update(dt);
|
||||
}
|
||||
|
||||
// Remove dead particles
|
||||
particles.erase(
|
||||
std::remove_if(particles.begin(), particles.end(),
|
||||
[](const Particle& p) { return !p.isAlive(); }),
|
||||
particles.end()
|
||||
);
|
||||
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) {
|
||||
@ -265,11 +367,46 @@ void LineEffect::renderFlash(int gridX, int gridY, int blockSize) {
|
||||
}
|
||||
|
||||
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;
|
||||
@ -278,3 +415,15 @@ void LineEffect::playLineClearSound(int lineCount) {
|
||||
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];
|
||||
}
|
||||
|
||||
@ -7,17 +7,49 @@
|
||||
class LineEffect {
|
||||
public:
|
||||
struct Particle {
|
||||
enum class Style { Shard, Ember };
|
||||
float x, y;
|
||||
float vx, vy;
|
||||
float size;
|
||||
float alpha;
|
||||
int blockType; // Added for textured particles
|
||||
float life;
|
||||
float lifeSpan;
|
||||
int blockType;
|
||||
SDL_Color color;
|
||||
|
||||
Particle(float px, float py);
|
||||
void update();
|
||||
Style style;
|
||||
|
||||
Particle(float px, float py, Style particleStyle, SDL_Color tint);
|
||||
void update(float dt);
|
||||
void render(SDL_Renderer* renderer, SDL_Texture* blocksTex);
|
||||
bool isAlive() const { return alpha > 0.0f; }
|
||||
bool isAlive() const { return life < lifeSpan && alpha > 0.01f; }
|
||||
};
|
||||
|
||||
struct Spark {
|
||||
float x, y;
|
||||
float vx, vy;
|
||||
float life;
|
||||
float maxLife;
|
||||
float thickness;
|
||||
SDL_Color color;
|
||||
|
||||
Spark(float px, float py, SDL_Color tint);
|
||||
void update(float dt);
|
||||
void render(SDL_Renderer* renderer) const;
|
||||
bool isAlive() const { return life < maxLife; }
|
||||
};
|
||||
|
||||
struct GlowPulse {
|
||||
float x, y;
|
||||
float baseRadius;
|
||||
float maxRadius;
|
||||
float life;
|
||||
float maxLife;
|
||||
SDL_Color color;
|
||||
|
||||
GlowPulse(float px, float py, float baseR, float maxR, SDL_Color tint);
|
||||
void update(float dt);
|
||||
void render(SDL_Renderer* renderer) const;
|
||||
bool isAlive() const { return life < maxLife; }
|
||||
};
|
||||
|
||||
enum class AnimationState {
|
||||
@ -51,6 +83,8 @@ private:
|
||||
float timer{0.0f};
|
||||
std::vector<int> clearingRows;
|
||||
std::vector<Particle> particles;
|
||||
std::vector<Spark> sparks;
|
||||
std::vector<GlowPulse> glowPulses;
|
||||
std::mt19937 rng{std::random_device{}()};
|
||||
|
||||
// Audio resources
|
||||
@ -59,14 +93,23 @@ private:
|
||||
std::vector<int16_t> tetrisSample;
|
||||
|
||||
// Animation timing - Flash then immediate explosion effect
|
||||
static constexpr float FLASH_DURATION = 0.12f; // Very brief white flash
|
||||
static constexpr float EXPLODE_DURATION = 0.6f; // Longer explosive effect
|
||||
static constexpr float DROP_DURATION = 0.05f; // Almost instant block drop
|
||||
static constexpr float FLASH_DURATION = 0.18f; // Slightly longer flash for anticipation
|
||||
static constexpr float EXPLODE_DURATION = 0.9f; // Extended fireworks time
|
||||
static constexpr float DROP_DURATION = 0.20f; // Allow lingering sparks before collapse
|
||||
|
||||
void createParticles(int row, int gridX, int gridY, int blockSize);
|
||||
void updateParticles();
|
||||
void spawnShardBurst(float x, float y, SDL_Color tint);
|
||||
void spawnSparkBurst(float x, float y, SDL_Color tint);
|
||||
void spawnGlowPulse(float x, float y, float blockSize, SDL_Color tint);
|
||||
void updateParticles(float dt);
|
||||
void updateSparks(float dt);
|
||||
void updateGlowPulses(float dt);
|
||||
void renderFlash(int gridX, int gridY, int blockSize);
|
||||
void renderExplosion(SDL_Texture* blocksTex);
|
||||
void renderGlowPulses();
|
||||
void renderSparks();
|
||||
void renderParticleGlows();
|
||||
bool loadAudioSample(const std::string& path, std::vector<int16_t>& sample);
|
||||
void initAudio();
|
||||
SDL_Color pickFireColor() const;
|
||||
};
|
||||
|
||||
207
src/main.cpp
207
src/main.cpp
@ -108,15 +108,35 @@ static SDL_Texture* loadTextureFromImage(SDL_Renderer* renderer, const std::stri
|
||||
return texture;
|
||||
}
|
||||
|
||||
enum class LevelBackgroundPhase { Idle, ZoomOut, ZoomIn };
|
||||
|
||||
struct LevelBackgroundFader {
|
||||
SDL_Texture* currentTex = nullptr;
|
||||
SDL_Texture* nextTex = nullptr;
|
||||
int currentLevel = -1;
|
||||
int queuedLevel = -1;
|
||||
float fadeElapsedMs = 0.0f;
|
||||
float phaseElapsedMs = 0.0f;
|
||||
float phaseDurationMs = 0.0f;
|
||||
float fadeDurationMs = Config::Gameplay::LEVEL_FADE_DURATION;
|
||||
LevelBackgroundPhase phase = LevelBackgroundPhase::Idle;
|
||||
};
|
||||
|
||||
static float getPhaseDurationMs(const LevelBackgroundFader& fader, LevelBackgroundPhase phase) {
|
||||
const float total = std::max(1200.0f, fader.fadeDurationMs);
|
||||
switch (phase) {
|
||||
case LevelBackgroundPhase::ZoomOut: return total * 0.45f;
|
||||
case LevelBackgroundPhase::ZoomIn: return total * 0.45f;
|
||||
case LevelBackgroundPhase::Idle:
|
||||
default: return 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
static void setPhase(LevelBackgroundFader& fader, LevelBackgroundPhase nextPhase) {
|
||||
fader.phase = nextPhase;
|
||||
fader.phaseDurationMs = getPhaseDurationMs(fader, nextPhase);
|
||||
fader.phaseElapsedMs = 0.0f;
|
||||
}
|
||||
|
||||
static void destroyTexture(SDL_Texture*& tex) {
|
||||
if (tex) {
|
||||
SDL_DestroyTexture(tex);
|
||||
@ -146,32 +166,90 @@ static bool queueLevelBackground(LevelBackgroundFader& fader, SDL_Renderer* rend
|
||||
destroyTexture(fader.nextTex);
|
||||
fader.nextTex = newTexture;
|
||||
fader.queuedLevel = level;
|
||||
fader.fadeElapsedMs = 0.0f;
|
||||
|
||||
if (!fader.currentTex) {
|
||||
// First background load happens instantly.
|
||||
fader.currentTex = fader.nextTex;
|
||||
fader.currentLevel = fader.queuedLevel;
|
||||
fader.nextTex = nullptr;
|
||||
fader.queuedLevel = -1;
|
||||
fader.phase = LevelBackgroundPhase::Idle;
|
||||
fader.phaseElapsedMs = 0.0f;
|
||||
fader.phaseDurationMs = 0.0f;
|
||||
} else if (fader.phase == LevelBackgroundPhase::Idle) {
|
||||
// Kick off fancy transition.
|
||||
setPhase(fader, LevelBackgroundPhase::ZoomOut);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void updateLevelBackgroundFade(LevelBackgroundFader& fader, float frameMs) {
|
||||
if (!fader.currentTex || !fader.nextTex) {
|
||||
if (fader.phase == LevelBackgroundPhase::Idle) {
|
||||
return;
|
||||
}
|
||||
|
||||
fader.fadeElapsedMs += frameMs;
|
||||
if (fader.fadeElapsedMs >= fader.fadeDurationMs) {
|
||||
destroyTexture(fader.currentTex);
|
||||
fader.currentTex = fader.nextTex;
|
||||
fader.currentLevel = fader.queuedLevel;
|
||||
fader.nextTex = nullptr;
|
||||
fader.queuedLevel = -1;
|
||||
fader.fadeElapsedMs = 0.0f;
|
||||
// Guard against missing textures
|
||||
if (!fader.currentTex && !fader.nextTex) {
|
||||
fader.phase = LevelBackgroundPhase::Idle;
|
||||
return;
|
||||
}
|
||||
|
||||
fader.phaseElapsedMs += frameMs;
|
||||
if (fader.phaseElapsedMs < std::max(1.0f, fader.phaseDurationMs)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (fader.phase) {
|
||||
case LevelBackgroundPhase::ZoomOut:
|
||||
// After zoom-out, swap textures then start zoom-in.
|
||||
if (fader.nextTex) {
|
||||
destroyTexture(fader.currentTex);
|
||||
fader.currentTex = fader.nextTex;
|
||||
fader.currentLevel = fader.queuedLevel;
|
||||
fader.nextTex = nullptr;
|
||||
fader.queuedLevel = -1;
|
||||
}
|
||||
setPhase(fader, LevelBackgroundPhase::ZoomIn);
|
||||
break;
|
||||
case LevelBackgroundPhase::ZoomIn:
|
||||
fader.phase = LevelBackgroundPhase::Idle;
|
||||
fader.phaseElapsedMs = 0.0f;
|
||||
fader.phaseDurationMs = 0.0f;
|
||||
break;
|
||||
case LevelBackgroundPhase::Idle:
|
||||
default:
|
||||
fader.phase = LevelBackgroundPhase::Idle;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void renderScaledBackground(SDL_Renderer* renderer, SDL_Texture* tex, int winW, int winH, float scale, Uint8 alpha = 255) {
|
||||
if (!renderer || !tex) {
|
||||
return;
|
||||
}
|
||||
|
||||
scale = std::max(0.5f, scale);
|
||||
SDL_FRect dest{
|
||||
(winW - winW * scale) * 0.5f,
|
||||
(winH - winH * scale) * 0.5f,
|
||||
winW * scale,
|
||||
winH * scale
|
||||
};
|
||||
|
||||
SDL_SetTextureAlphaMod(tex, alpha);
|
||||
SDL_RenderTexture(renderer, tex, nullptr, &dest);
|
||||
SDL_SetTextureAlphaMod(tex, 255);
|
||||
}
|
||||
|
||||
static void drawOverlay(SDL_Renderer* renderer, const SDL_FRect& rect, SDL_Color color, Uint8 alpha) {
|
||||
if (!renderer || alpha == 0) {
|
||||
return;
|
||||
}
|
||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, alpha);
|
||||
SDL_RenderFillRect(renderer, &rect);
|
||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE);
|
||||
}
|
||||
|
||||
static void renderLevelBackgrounds(const LevelBackgroundFader& fader, SDL_Renderer* renderer, int winW, int winH) {
|
||||
@ -180,22 +258,36 @@ static void renderLevelBackgrounds(const LevelBackgroundFader& fader, SDL_Render
|
||||
}
|
||||
|
||||
SDL_FRect fullRect{0.f, 0.f, static_cast<float>(winW), static_cast<float>(winH)};
|
||||
const float duration = std::max(1.0f, fader.phaseDurationMs);
|
||||
const float progress = (fader.phase == LevelBackgroundPhase::Idle) ? 0.0f : std::clamp(fader.phaseElapsedMs / duration, 0.0f, 1.0f);
|
||||
|
||||
if (fader.currentTex && fader.nextTex) {
|
||||
const float duration = std::max(1.0f, fader.fadeDurationMs);
|
||||
const float alpha = std::clamp(fader.fadeElapsedMs / duration, 0.0f, 1.0f);
|
||||
|
||||
SDL_SetTextureAlphaMod(fader.currentTex, Uint8((1.0f - alpha) * 255.0f));
|
||||
SDL_RenderTexture(renderer, fader.currentTex, nullptr, &fullRect);
|
||||
SDL_SetTextureAlphaMod(fader.currentTex, 255);
|
||||
|
||||
SDL_SetTextureAlphaMod(fader.nextTex, Uint8(alpha * 255.0f));
|
||||
SDL_RenderTexture(renderer, fader.nextTex, nullptr, &fullRect);
|
||||
SDL_SetTextureAlphaMod(fader.nextTex, 255);
|
||||
} else if (fader.currentTex) {
|
||||
SDL_RenderTexture(renderer, fader.currentTex, nullptr, &fullRect);
|
||||
} else if (fader.nextTex) {
|
||||
SDL_RenderTexture(renderer, fader.nextTex, nullptr, &fullRect);
|
||||
switch (fader.phase) {
|
||||
case LevelBackgroundPhase::ZoomOut: {
|
||||
const float scale = 1.0f + progress * 0.15f;
|
||||
if (fader.currentTex) {
|
||||
renderScaledBackground(renderer, fader.currentTex, winW, winH, scale, Uint8((1.0f - progress * 0.4f) * 255.0f));
|
||||
drawOverlay(renderer, fullRect, SDL_Color{0, 0, 0, 255}, Uint8(progress * 200.0f));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LevelBackgroundPhase::ZoomIn: {
|
||||
const float scale = 1.10f - progress * 0.10f;
|
||||
const Uint8 alpha = Uint8((0.4f + progress * 0.6f) * 255.0f);
|
||||
if (fader.currentTex) {
|
||||
renderScaledBackground(renderer, fader.currentTex, winW, winH, scale, alpha);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LevelBackgroundPhase::Idle:
|
||||
default:
|
||||
if (fader.currentTex) {
|
||||
renderScaledBackground(renderer, fader.currentTex, winW, winH, 1.0f, 255);
|
||||
} else if (fader.nextTex) {
|
||||
renderScaledBackground(renderer, fader.nextTex, winW, winH, 1.0f, 255);
|
||||
} else {
|
||||
drawOverlay(renderer, fullRect, SDL_Color{0, 0, 0, 255}, 255);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -204,7 +296,9 @@ static void resetLevelBackgrounds(LevelBackgroundFader& fader) {
|
||||
destroyTexture(fader.nextTex);
|
||||
fader.currentLevel = -1;
|
||||
fader.queuedLevel = -1;
|
||||
fader.fadeElapsedMs = 0.0f;
|
||||
fader.phaseElapsedMs = 0.0f;
|
||||
fader.phaseDurationMs = 0.0f;
|
||||
fader.phase = LevelBackgroundPhase::Idle;
|
||||
}
|
||||
|
||||
// Hover state for level popup ( -1 = none, 0..19 = hovered level )
|
||||
@ -659,9 +753,18 @@ int main(int, char **)
|
||||
SoundEffectManager::instance().loadSound("clear_line", "assets/music/clear_line.wav");
|
||||
|
||||
// Load voice lines for line clears using WAV files (with MP3 fallback)
|
||||
std::vector<std::string> doubleSounds = {"nice_combo", "you_fire", "well_played", "keep_that_ryhtm"};
|
||||
std::vector<std::string> tripleSounds = {"great_move", "smooth_clear", "impressive", "triple_strike"};
|
||||
std::vector<std::string> singleSounds = {"well_played", "smooth_clear", "great_move"};
|
||||
std::vector<std::string> doubleSounds = {"nice_combo", "you_fire", "keep_that_ryhtm"};
|
||||
std::vector<std::string> tripleSounds = {"impressive", "triple_strike"};
|
||||
std::vector<std::string> tetrisSounds = {"amazing", "you_re_unstoppable", "boom_tetris", "wonderful"};
|
||||
std::vector<std::string> allVoiceSounds;
|
||||
auto appendVoices = [&allVoiceSounds](const std::vector<std::string>& src) {
|
||||
allVoiceSounds.insert(allVoiceSounds.end(), src.begin(), src.end());
|
||||
};
|
||||
appendVoices(singleSounds);
|
||||
appendVoices(doubleSounds);
|
||||
appendVoices(tripleSounds);
|
||||
appendVoices(tetrisSounds);
|
||||
|
||||
// Helper function to load sound with WAV/MP3 fallback and file existence check
|
||||
auto loadSoundWithFallback = [&](const std::string& id, const std::string& baseName) {
|
||||
@ -705,20 +808,34 @@ int main(int, char **)
|
||||
loadSoundWithFallback("wonderful", "wonderful");
|
||||
loadSoundWithFallback("lets_go", "lets_go"); // For level up
|
||||
|
||||
// Set up sound effect callbacks
|
||||
game.setSoundCallback([&](int linesCleared) {
|
||||
// Play basic line clear sound first
|
||||
SoundEffectManager::instance().playSound("clear_line", 1.0f); // Increased volume
|
||||
|
||||
// Then play voice line based on number of lines cleared
|
||||
if (linesCleared == 2) {
|
||||
SoundEffectManager::instance().playRandomSound(doubleSounds, 1.0f); // Increased volume
|
||||
} else if (linesCleared == 3) {
|
||||
SoundEffectManager::instance().playRandomSound(tripleSounds, 1.0f); // Increased volume
|
||||
} else if (linesCleared == 4) {
|
||||
SoundEffectManager::instance().playRandomSound(tetrisSounds, 1.0f); // Increased volume
|
||||
auto playVoiceCue = [&](int linesCleared) {
|
||||
const std::vector<std::string>* bank = nullptr;
|
||||
switch (linesCleared) {
|
||||
case 1: bank = &singleSounds; break;
|
||||
case 2: bank = &doubleSounds; break;
|
||||
case 3: bank = &tripleSounds; break;
|
||||
default:
|
||||
if (linesCleared >= 4) {
|
||||
bank = &tetrisSounds;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Single line clears just play the basic clear sound (no voice in JS version)
|
||||
if (bank && !bank->empty()) {
|
||||
SoundEffectManager::instance().playRandomSound(*bank, 1.0f);
|
||||
}
|
||||
};
|
||||
|
||||
// Set up sound effect callbacks
|
||||
game.setSoundCallback([&, playVoiceCue](int linesCleared) {
|
||||
if (linesCleared <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Always play the core line-clear sound for consistency
|
||||
SoundEffectManager::instance().playSound("clear_line", 1.0f);
|
||||
|
||||
// Layer a voiced callout based on the number of cleared lines
|
||||
playVoiceCue(linesCleared);
|
||||
});
|
||||
|
||||
game.setLevelUpCallback([&](int newLevel) {
|
||||
@ -898,8 +1015,10 @@ int main(int, char **)
|
||||
}
|
||||
if (e.key.scancode == SDL_SCANCODE_N)
|
||||
{
|
||||
// Test sound effects - play lets_go.wav specifically
|
||||
SoundEffectManager::instance().playSound("lets_go", 1.0f);
|
||||
// Manually trigger a random voice line for quick testing
|
||||
if (!allVoiceSounds.empty()) {
|
||||
SoundEffectManager::instance().playRandomSound(allVoiceSounds, 1.0f);
|
||||
}
|
||||
}
|
||||
if (e.key.key == SDLK_F11 || (e.key.key == SDLK_RETURN && (e.key.mod & SDL_KMOD_ALT)))
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user