diff --git a/FreeSans.ttf b/FreeSans.ttf deleted file mode 100644 index 9db9585..0000000 Binary files a/FreeSans.ttf and /dev/null differ diff --git a/settings.ini b/settings.ini index ea793f9..200523e 100644 --- a/settings.ini +++ b/settings.ini @@ -8,6 +8,9 @@ Fullscreen=1 Music=1 Sound=1 +[Gameplay] +SmoothScroll=1 + [Player] Name=GREGOR diff --git a/src/core/GlobalState.cpp b/src/core/GlobalState.cpp index 752630e..b5ad912 100644 --- a/src/core/GlobalState.cpp +++ b/src/core/GlobalState.cpp @@ -3,6 +3,55 @@ #include #include #include +#include + +namespace { +constexpr float PI_F = 3.14159265358979323846f; + +float randRange(float minVal, float maxVal) { + return minVal + (static_cast(rand()) / static_cast(RAND_MAX)) * (maxVal - minVal); +} + +SDL_Color randomFireworkColor() { + static const SDL_Color palette[] = { + {255, 120, 80, 255}, + {255, 190, 60, 255}, + {120, 210, 255, 255}, + {170, 120, 255, 255}, + {255, 90, 180, 255}, + {120, 255, 170, 255}, + {255, 255, 180, 255} + }; + size_t idx = static_cast(rand() % (sizeof(palette) / sizeof(palette[0]))); + return palette[idx]; +} + +SDL_Color scaleColor(SDL_Color color, float factor, Uint8 alphaOverride = 0) { + auto clampChannel = [](float value) -> Uint8 { + return static_cast(std::max(0.0f, std::min(255.0f, std::round(value)))); + }; + SDL_Color result; + result.r = clampChannel(color.r * factor); + result.g = clampChannel(color.g * factor); + result.b = clampChannel(color.b * factor); + result.a = alphaOverride ? alphaOverride : color.a; + return result; +} + +SDL_Color mixColors(SDL_Color a, SDL_Color b, float t) { + t = std::clamp(t, 0.0f, 1.0f); + auto lerpChannel = [t](Uint8 ca, Uint8 cb) -> Uint8 { + float blended = ca + (cb - ca) * t; + return static_cast(std::max(0.0f, std::min(255.0f, blended))); + }; + SDL_Color result; + result.r = lerpChannel(a.r, b.r); + result.g = lerpChannel(a.g, b.g); + result.b = lerpChannel(a.b, b.b); + result.a = lerpChannel(a.a, b.a); + return result; +} +} GlobalState& GlobalState::instance() { static GlobalState instance; @@ -41,88 +90,163 @@ void GlobalState::shutdown() { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[GlobalState] Shutdown complete"); } +namespace { +using Firework = GlobalState::TetrisFirework; +using BlockParticle = GlobalState::BlockParticle; +using SparkParticle = GlobalState::SparkParticle; +void spawnSparks(Firework& firework, float cx, float cy, SDL_Color baseColor, float speedBase) { + int sparkCount = 10 + (rand() % 10); + for (int i = 0; i < sparkCount; ++i) { + SparkParticle spark; + spark.x = cx; + spark.y = cy; + float angle = randRange(0.0f, PI_F * 2.0f); + float speed = speedBase * randRange(1.05f, 1.6f); + spark.vx = std::cos(angle) * speed; + spark.vy = std::sin(angle) * speed - randRange(30.0f, 90.0f); + spark.life = 0.0f; + spark.maxLife = 260.0f + randRange(0.0f, 200.0f); + spark.thickness = randRange(0.8f, 2.2f); + spark.color = scaleColor(baseColor, randRange(0.85f, 1.2f), 255); + firework.sparks.push_back(spark); + } +} + +void triggerFireworkBurst(Firework& firework, int burstIndex) { + SDL_Color burstColor = firework.burstColors[burstIndex % firework.burstColors.size()]; + float centerX = firework.originX + randRange(-30.0f, 30.0f); + float centerY = firework.originY - burstIndex * randRange(14.0f, 24.0f) + randRange(-10.0f, 10.0f); + int particleCount = 22 + (rand() % 16); + float speedBase = 90.0f + burstIndex * 40.0f; + + for (int i = 0; i < particleCount; ++i) { + BlockParticle particle; + particle.x = centerX; + particle.y = centerY; + float angle = randRange(0.0f, PI_F * 2.0f); + float speed = speedBase + randRange(-20.0f, 70.0f); + particle.vx = std::cos(angle) * speed; + particle.vy = std::sin(angle) * speed - randRange(35.0f, 95.0f); + particle.maxLife = 950.0f + randRange(0.0f, 420.0f) + burstIndex * 220.0f; + particle.life = particle.maxLife; + particle.crackle = (rand() % 100) < 75; + particle.flickerSeed = randRange(0.0f, PI_F * 2.0f); + if (particle.crackle) { + particle.size = randRange(1.4f, 3.2f); + } else { + particle.size = 3.2f + randRange(0.0f, 2.6f) + burstIndex * 0.6f; + } + particle.color = scaleColor(burstColor, randRange(0.85f, 1.2f)); + particle.dualColor = (rand() % 100) < 55; + if (particle.dualColor) { + SDL_Color alt = randomFireworkColor(); + float luminanceDiff = std::abs(static_cast(alt.r + alt.g + alt.b) - (particle.color.r + particle.color.g + particle.color.b)); + if (luminanceDiff < 40.0f) { + alt = scaleColor(particle.color, randRange(0.6f, 1.4f)); + } + particle.accentColor = alt; + particle.colorBlendSpeed = randRange(0.6f, 1.4f); + } else { + particle.accentColor = particle.color; + particle.colorBlendSpeed = 1.0f; + } + firework.particles.push_back(particle); + } + + spawnSparks(firework, centerX, centerY, burstColor, speedBase); +} +} + void GlobalState::updateFireworks(double frameMs) { + if (frameMs <= 0.0) { + frameMs = 16.0; + } + const Uint64 currentTime = SDL_GetTicks(); - - // Check if we have any active fireworks - bool hasActiveFirework = false; + size_t activeCount = 0; for (const auto& fw : fireworks) { if (fw.active) { - hasActiveFirework = true; - break; + ++activeCount; } } - - // Only create new firework if no active ones exist - if (!hasActiveFirework && currentTime - lastFireworkTime > 1500 + (rand() % 2000)) { - float x = Config::Logical::WIDTH * (0.15f + (rand() % 70) / 100.0f); - float y = Config::Logical::HEIGHT * (0.20f + (rand() % 60) / 100.0f); + + constexpr size_t MAX_SIMULTANEOUS_FIREWORKS = 2; + bool canSpawnNew = activeCount < MAX_SIMULTANEOUS_FIREWORKS; + bool spawnedFirework = false; + + if (canSpawnNew) { + Uint64 interval = 1300 + static_cast(rand() % 1400); + if (currentTime - lastFireworkTime > interval) { + float x = Config::Logical::WIDTH * (0.15f + randRange(0.0f, 0.70f)); + float y = Config::Logical::HEIGHT * (0.18f + randRange(0.0f, 0.40f)); + createFirework(x, y); + lastFireworkTime = currentTime; + lastFireworkX = x; + lastFireworkY = y; + pendingStaggerFirework = (rand() % 100) < 65; + if (pendingStaggerFirework) { + nextStaggerFireworkTime = currentTime + 250 + static_cast(rand() % 420); + } + spawnedFirework = true; + } + } + + if (!spawnedFirework && pendingStaggerFirework && canSpawnNew && currentTime >= nextStaggerFireworkTime) { + float x = lastFireworkX + randRange(-140.0f, 140.0f); + float y = lastFireworkY + randRange(-80.0f, 50.0f); + x = std::clamp(x, Config::Logical::WIDTH * 0.10f, Config::Logical::WIDTH * 0.90f); + y = std::clamp(y, Config::Logical::HEIGHT * 0.15f, Config::Logical::HEIGHT * 0.70f); createFirework(x, y); lastFireworkTime = currentTime; + lastFireworkX = x; + lastFireworkY = y; + pendingStaggerFirework = false; + spawnedFirework = true; } - // Update existing fireworks + const float dtSeconds = static_cast(frameMs / 1000.0); + const float deltaMs = static_cast(frameMs); + for (auto& firework : fireworks) { - if (!firework.active) continue; - - bool hasActiveParticles = false; - std::vector newParticles; - newParticles.reserve(20); // Pre-allocate to avoid reallocation - - for (auto& particle : firework.particles) { - if (particle.life <= 0) continue; - - // Update physics - float dt = float(frameMs / 1000.0f); - particle.x += particle.vx * dt; - particle.y += particle.vy * dt; - particle.vx *= (1.0f - 0.5f * dt); - particle.vy = particle.vy * (1.0f - 0.2f * dt) + 80.0f * dt; - particle.life -= frameMs; - - // Update size - float lifeRatio = particle.life / particle.maxLife; - particle.size = (6.0f - particle.generation * 2.0f) + (4.0f - particle.generation) * lifeRatio; - - // Only primary particles create secondary explosions (single cascade level) - if (!particle.hasExploded && particle.generation == 0 && lifeRatio < 0.5f) { - particle.hasExploded = true; - - // Spawn only 3-4 secondary particles - int secondaryCount = 3 + (rand() % 2); - for (int i = 0; i < secondaryCount; ++i) { - BlockParticle secondary; - secondary.x = particle.x; - secondary.y = particle.y; - secondary.generation = 1; // Only one level of cascade - secondary.hasExploded = true; // Don't cascade further - - float angle = (float)(rand() % 360) * 3.14159f / 180.0f; - float speed = 40.0f + (rand() % 40); - secondary.vx = cos(angle) * speed; - secondary.vy = sin(angle) * speed - 20.0f; - - secondary.type = 1 + (rand() % 7); - secondary.maxLife = 700.0f + (rand() % 400); - secondary.life = secondary.maxLife; - secondary.size = 3.0f + (rand() % 2); - - newParticles.push_back(secondary); - } - } - - if (particle.life > 0) { - hasActiveParticles = true; - } - } - - // Add secondary particles - for (auto& newParticle : newParticles) { - firework.particles.push_back(newParticle); - hasActiveParticles = true; + if (!firework.active) { + continue; } - firework.active = hasActiveParticles; + firework.elapsedMs += deltaMs; + while (firework.nextBurst < static_cast(firework.burstSchedule.size()) && + firework.elapsedMs >= firework.burstSchedule[firework.nextBurst]) { + triggerFireworkBurst(firework, firework.nextBurst); + firework.nextBurst++; + } + + for (auto it = firework.particles.begin(); it != firework.particles.end();) { + it->life -= deltaMs; + if (it->life <= 0.0f) { + it = firework.particles.erase(it); + continue; + } + it->x += it->vx * dtSeconds; + it->y += it->vy * dtSeconds; + it->vx *= 0.986f; + it->vy = it->vy * 0.972f + 70.0f * dtSeconds; + ++it; + } + + for (auto it = firework.sparks.begin(); it != firework.sparks.end();) { + it->life += deltaMs; + if (it->life >= it->maxLife) { + it = firework.sparks.erase(it); + continue; + } + it->x += it->vx * dtSeconds; + it->y += it->vy * dtSeconds; + it->vx *= 0.992f; + it->vy = it->vy * 0.965f + 120.0f * dtSeconds; + ++it; + } + + bool pendingBursts = firework.nextBurst < static_cast(firework.burstSchedule.size()); + firework.active = pendingBursts || !firework.particles.empty() || !firework.sparks.empty(); } } @@ -142,78 +266,103 @@ void GlobalState::createFirework(float x, float y) { firework = &fireworks.back(); } - // Initialize firework firework->active = true; firework->particles.clear(); + firework->sparks.clear(); + firework->originX = x; + firework->originY = y; + firework->elapsedMs = 0.0f; + firework->nextBurst = 0; + firework->burstSchedule = { + 0.0f, + 220.0f + randRange(0.0f, 160.0f), + 420.0f + randRange(0.0f, 260.0f) + }; - // Create fewer particles for subtle background effect - const int particleCount = 12 + (rand() % 8); // 12-20 particles per explosion - for (int i = 0; i < particleCount; ++i) { - BlockParticle particle; - particle.x = x; - particle.y = y; - particle.generation = 0; // Primary explosion - particle.hasExploded = false; - - // Random velocity in all directions - float angle = (float)(rand() % 360) * 3.14159f / 180.0f; - float speed = 80.0f + (rand() % 100); // Moderate speed - particle.vx = cos(angle) * speed; - particle.vy = sin(angle) * speed - 50.0f; // Slight upward bias - - particle.type = 1 + (rand() % 7); // Random tetris piece color - particle.maxLife = 1500.0f + (rand() % 1000); // Medium life: ~1.5-2.5 seconds - particle.life = particle.maxLife; - particle.size = 6.0f + (rand() % 5); // Smaller particles - - firework->particles.push_back(particle); + SDL_Color baseColor = randomFireworkColor(); + for (int i = 0; i < 3; ++i) { + float wobble = randRange(-0.08f, 0.08f); + firework->burstColors[i] = scaleColor(baseColor, 1.0f - i * 0.12f + wobble, 255); } } void GlobalState::drawFireworks(SDL_Renderer* renderer, SDL_Texture* blocksTex) { - (void)blocksTex; // Not using texture anymore - - SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); + (void)blocksTex; + auto renderCircle = [renderer](float cx, float cy, float radius) { + int ir = static_cast(std::ceil(radius)); + for (int dy = -ir; dy <= ir; ++dy) { + float row = std::sqrt(std::max(0.0f, radius * radius - static_cast(dy * dy))); + SDL_FRect line{cx - row, cy + dy, row * 2.0f, 1.0f}; + SDL_RenderFillRect(renderer, &line); + } + }; + + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_ADD); + for (const auto& firework : fireworks) { + if (!firework.active) continue; + + for (const auto& spark : firework.sparks) { + if (spark.life >= spark.maxLife) continue; + float progress = spark.life / spark.maxLife; + Uint8 alpha = static_cast((1.0f - progress) * 255.0f); + if (alpha == 0) continue; + SDL_SetRenderDrawColor(renderer, spark.color.r, spark.color.g, spark.color.b, alpha); + float trailScale = 0.015f * spark.thickness; + float tailX = spark.x - spark.vx * trailScale; + float tailY = spark.y - spark.vy * trailScale; + SDL_RenderLine(renderer, + static_cast(spark.x), + static_cast(spark.y), + static_cast(tailX), + static_cast(tailY)); + } + } + + auto sampleParticleColor = [](const BlockParticle& particle) -> SDL_Color { + if (!particle.dualColor) { + return particle.color; + } + float elapsed = particle.maxLife - particle.life; + float phase = particle.flickerSeed * 1.8f + elapsed * 0.0025f * particle.colorBlendSpeed; + float mixFactor = 0.5f + 0.5f * std::sin(phase); + return mixColors(particle.color, particle.accentColor, mixFactor); + }; + + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); for (const auto& firework : fireworks) { if (!firework.active) continue; for (const auto& particle : firework.particles) { - if (particle.life <= 0) continue; - - // Calculate alpha based on remaining life + if (particle.life <= 0.0f) continue; float lifeRatio = particle.life / particle.maxLife; - Uint8 alpha = (Uint8)(128 * std::min(1.0f, lifeRatio * 1.6f)); - - // Color based on particle type - SDL_Color colors[] = { - {0, 240, 240, alpha}, // Cyan - {240, 160, 0, alpha}, // Orange - {0, 0, 240, alpha}, // Blue - {240, 240, 0, alpha}, // Yellow - {0, 240, 0, alpha}, // Green - {160, 0, 240, alpha}, // Purple - {240, 0, 0, alpha} // Red - }; - - SDL_Color color = colors[(particle.type - 1) % 7]; - SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a); - - // For small particles, just draw a filled rect (much faster) - if (particle.size <= 4.0f) { - SDL_FRect rect{particle.x - particle.size/2, particle.y - particle.size/2, particle.size, particle.size}; - SDL_RenderFillRect(renderer, &rect); + float alphaF = std::pow(std::max(0.0f, lifeRatio), 0.75f); + if (particle.crackle) { + SDL_Color dynamicColor = sampleParticleColor(particle); + float flicker = 0.55f + 0.45f * std::sin(particle.flickerSeed + particle.life * 0.018f); + Uint8 alpha = static_cast(alphaF * flicker * 255.0f); + if (alpha == 0) continue; + SDL_SetRenderDrawColor(renderer, dynamicColor.r, dynamicColor.g, dynamicColor.b, alpha); + float stretch = particle.size * (2.5f + (1.0f - lifeRatio) * 1.3f); + float angle = particle.flickerSeed * 3.0f + particle.life * 0.004f; + float dx = std::cos(angle) * stretch; + float dy = std::sin(angle) * stretch * 0.7f; + SDL_RenderLine(renderer, + static_cast(particle.x - dx), + static_cast(particle.y - dy), + static_cast(particle.x + dx), + static_cast(particle.y + dy)); + SDL_RenderLine(renderer, + static_cast(particle.x - dy * 0.45f), + static_cast(particle.y + dx * 0.45f), + static_cast(particle.x + dy * 0.45f), + static_cast(particle.y - dx * 0.45f)); } else { - // For larger particles, draw a simple circle approximation - float radius = particle.size / 2.0f; - int r = (int)radius; - for (int dy = -r; dy <= r; ++dy) { - int width = (int)sqrt(radius*radius - dy*dy) * 2; - if (width > 0) { - SDL_FRect line{particle.x - width/2.0f, particle.y + dy, (float)width, 1.0f}; - SDL_RenderFillRect(renderer, &line); - } - } + SDL_Color dynamicColor = sampleParticleColor(particle); + Uint8 alpha = static_cast(alphaF * 255.0f); + SDL_SetRenderDrawColor(renderer, dynamicColor.r, dynamicColor.g, dynamicColor.b, alpha); + float radius = particle.size * (0.5f + 0.3f * lifeRatio); + renderCircle(particle.x, particle.y, radius); } } } diff --git a/src/core/GlobalState.h b/src/core/GlobalState.h index ce93ff7..d3df436 100644 --- a/src/core/GlobalState.h +++ b/src/core/GlobalState.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -73,21 +74,52 @@ public: // Fireworks system (for menu animation) struct BlockParticle { - float x, y, vx, vy; - int type; - float life, maxLife; - float size; - int generation = 0; // 0 = primary, 1 = secondary, 2 = tertiary - bool hasExploded = false; // Track if this particle has spawned children + float x = 0.0f; + float y = 0.0f; + float vx = 0.0f; + float vy = 0.0f; + float size = 0.0f; + float life = 0.0f; + float maxLife = 0.0f; + SDL_Color color{255, 255, 255, 255}; + SDL_Color accentColor{255, 255, 255, 255}; + int generation = 0; + bool hasExploded = false; + bool crackle = false; + float flickerSeed = 0.0f; + bool dualColor = false; + float colorBlendSpeed = 0.0f; + }; + + struct SparkParticle { + float x = 0.0f; + float y = 0.0f; + float vx = 0.0f; + float vy = 0.0f; + float life = 0.0f; + float maxLife = 0.0f; + float thickness = 1.0f; + SDL_Color color{255, 255, 255, 255}; }; struct TetrisFirework { std::vector particles; + std::vector sparks; bool active = false; + float originX = 0.0f; + float originY = 0.0f; + float elapsedMs = 0.0f; + int nextBurst = 0; + std::array burstSchedule{0.0f, 250.0f, 520.0f}; + std::array burstColors{}; }; std::vector fireworks; Uint64 lastFireworkTime = 0; + bool pendingStaggerFirework = false; + Uint64 nextStaggerFireworkTime = 0; + float lastFireworkX = 0.0f; + float lastFireworkY = 0.0f; // Fireworks management methods void updateFireworks(double frameMs); diff --git a/src/core/Settings.cpp b/src/core/Settings.cpp index 2ff17b3..2a24320 100644 --- a/src/core/Settings.cpp +++ b/src/core/Settings.cpp @@ -66,6 +66,10 @@ bool Settings::load() { } else if (key == "Sound") { m_soundEnabled = (value == "1" || value == "true" || value == "True"); } + } else if (currentSection == "Gameplay") { + if (key == "SmoothScroll") { + m_smoothScrollEnabled = (value == "1" || value == "true" || value == "True"); + } } else if (currentSection == "Player") { if (key == "Name") { m_playerName = value; @@ -100,6 +104,9 @@ bool Settings::save() { file << "Music=" << (m_musicEnabled ? "1" : "0") << "\n"; file << "Sound=" << (m_soundEnabled ? "1" : "0") << "\n\n"; + file << "[Gameplay]\n"; + file << "SmoothScroll=" << (m_smoothScrollEnabled ? "1" : "0") << "\n\n"; + file << "[Player]\n"; file << "Name=" << m_playerName << "\n\n"; diff --git a/src/core/Settings.h b/src/core/Settings.h index ce78740..048bd81 100644 --- a/src/core/Settings.h +++ b/src/core/Settings.h @@ -28,6 +28,9 @@ public: bool isDebugEnabled() const { return m_debugEnabled; } void setDebugEnabled(bool value) { m_debugEnabled = value; } + + bool isSmoothScrollEnabled() const { return m_smoothScrollEnabled; } + void setSmoothScrollEnabled(bool value) { m_smoothScrollEnabled = value; } const std::string& getPlayerName() const { return m_playerName; } void setPlayerName(const std::string& name) { m_playerName = name; } @@ -45,5 +48,6 @@ private: bool m_musicEnabled = true; bool m_soundEnabled = true; bool m_debugEnabled = false; + bool m_smoothScrollEnabled = true; std::string m_playerName = "Player"; }; diff --git a/src/gameplay/core/Game.cpp b/src/gameplay/core/Game.cpp index 9360ba9..2196648 100644 --- a/src/gameplay/core/Game.cpp +++ b/src/gameplay/core/Game.cpp @@ -103,6 +103,24 @@ void Game::setPaused(bool p) { paused = p; } +void Game::setSoftDropping(bool on) { + if (softDropping == on) { + return; + } + + double oldStep = softDropping ? (gravityMs / 5.0) : gravityMs; + softDropping = on; + double newStep = softDropping ? (gravityMs / 5.0) : gravityMs; + + if (oldStep <= 0.0 || newStep <= 0.0) { + return; + } + + double progress = fallAcc / oldStep; + progress = std::clamp(progress, 0.0, 1.0); + fallAcc = progress * newStep; +} + void Game::refillBag() { bag.clear(); for (int i=0;i(i)); diff --git a/src/gameplay/core/Game.h b/src/gameplay/core/Game.h index 07a8aeb..48de705 100644 --- a/src/gameplay/core/Game.h +++ b/src/gameplay/core/Game.h @@ -28,7 +28,7 @@ public: void tickGravity(double frameMs); // advance gravity accumulator & drop void softDropBoost(double frameMs); // accelerate fall while held void hardDrop(); // instant drop & lock - void setSoftDropping(bool on) { softDropping = on; } // mark if player holds Down + void setSoftDropping(bool on); // mark if player holds Down void move(int dx); // horizontal move void rotate(int dir); // +1 cw, -1 ccw (simple wall-kick) void holdCurrent(); // swap with hold (once per spawn) diff --git a/src/gameplay/effects/LineEffect.cpp b/src/gameplay/effects/LineEffect.cpp index 21a9e08..e918210 100644 --- a/src/gameplay/effects/LineEffect.cpp +++ b/src/gameplay/effects/LineEffect.cpp @@ -194,9 +194,28 @@ void LineEffect::startLineClear(const std::vector& rows, int gridX, int gri 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) { @@ -267,12 +286,16 @@ bool LineEffect::update(float deltaTime) { 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; @@ -335,6 +358,21 @@ void LineEffect::render(SDL_Renderer* renderer, SDL_Texture* blocksTex, int grid } } +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; diff --git a/src/gameplay/effects/LineEffect.h b/src/gameplay/effects/LineEffect.h index 63c1ba1..99834ab 100644 --- a/src/gameplay/effects/LineEffect.h +++ b/src/gameplay/effects/LineEffect.h @@ -3,6 +3,9 @@ #include #include #include +#include + +#include "../core/Game.h" class LineEffect { public: @@ -71,6 +74,7 @@ public: // Update and render the effect bool update(float deltaTime); // Returns true if effect is complete void render(SDL_Renderer* renderer, SDL_Texture* blocksTex, int gridX, int gridY, int blockSize); + float getRowDropOffset(int row) const; // Audio void playLineClearSound(int lineCount); @@ -95,7 +99,7 @@ private: // Animation timing - Flash then immediate explosion effect 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 + static constexpr float DROP_DURATION = 0.35f; // Allow lingering sparks before collapse void createParticles(int row, int gridX, int gridY, int blockSize); void spawnShardBurst(float x, float y, SDL_Color tint); @@ -112,4 +116,8 @@ private: bool loadAudioSample(const std::string& path, std::vector& sample); void initAudio(); SDL_Color pickFireColor() const; + + std::array rowDropTargets{}; + float dropProgress = 0.0f; + int dropBlockSize = 0; }; diff --git a/src/graphics/renderers/GameRenderer.cpp b/src/graphics/renderers/GameRenderer.cpp index 50b0c54..074d586 100644 --- a/src/graphics/renderers/GameRenderer.cpp +++ b/src/graphics/renderers/GameRenderer.cpp @@ -47,14 +47,14 @@ void GameRenderer::drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksT SDL_RenderTexture(renderer, blocksTex, &srcRect, &dstRect); } -void GameRenderer::drawPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, const Game::Piece& piece, float ox, float oy, float tileSize, bool isGhost) { +void GameRenderer::drawPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, const Game::Piece& piece, float ox, float oy, float tileSize, bool isGhost, float pixelOffsetY) { if (piece.type >= PIECE_COUNT) return; for (int cy = 0; cy < 4; ++cy) { for (int cx = 0; cx < 4; ++cx) { if (Game::cellFilled(piece, cx, cy)) { float px = ox + (piece.x + cx) * tileSize; - float py = oy + (piece.y + cy) * tileSize; + float py = oy + (piece.y + cy) * tileSize + pixelOffsetY; if (isGhost) { SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); @@ -226,17 +226,72 @@ void GameRenderer::renderPlayingState( // Draw the game board const auto &board = game->boardRef(); for (int y = 0; y < Game::ROWS; ++y) { + float dropOffset = (lineEffect ? lineEffect->getRowDropOffset(y) : 0.0f); for (int x = 0; x < Game::COLS; ++x) { int v = board[y * Game::COLS + x]; if (v > 0) { float bx = gridX + x * finalBlockSize; - float by = gridY + y * finalBlockSize; + float by = gridY + y * finalBlockSize + dropOffset; drawBlockTexture(renderer, blocksTex, bx, by, finalBlockSize, v - 1); } } } bool allowActivePieceRender = true; + const bool smoothScrollEnabled = Settings::instance().isSmoothScrollEnabled(); + + auto computeFallOffset = [&]() -> float { + if (game->isPaused()) { + return 0.0f; + } + double gravityMs = game->getGravityMs(); + if (gravityMs <= 0.0) { + return 0.0f; + } + double effectiveMs = game->isSoftDropping() ? std::max(5.0, gravityMs / 5.0) : gravityMs; + double accumulator = std::clamp(game->getFallAccumulator(), 0.0, effectiveMs); + if (effectiveMs <= 0.0) { + return 0.0f; + } + float progress = static_cast(accumulator / effectiveMs); + progress = std::clamp(progress, 0.0f, 1.0f); + return progress * finalBlockSize; + }; + + float activePieceOffset = (!game->isPaused() && smoothScrollEnabled) ? computeFallOffset() : 0.0f; + if (activePieceOffset > 0.0f) { + const auto& boardRef = game->boardRef(); + const Game::Piece& piece = game->current(); + float maxAllowed = finalBlockSize; + for (int cy = 0; cy < 4; ++cy) { + for (int cx = 0; cx < 4; ++cx) { + if (!Game::cellFilled(piece, cx, cy)) { + continue; + } + int gx = piece.x + cx; + int gy = piece.y + cy; + if (gx < 0 || gx >= Game::COLS) { + continue; + } + int testY = gy + 1; + int emptyRows = 0; + if (testY < 0) { + emptyRows -= testY; // number of rows until we reach row 0 + testY = 0; + } + while (testY >= 0 && testY < Game::ROWS) { + if (boardRef[testY * Game::COLS + gx] != 0) { + break; + } + ++emptyRows; + ++testY; + } + float cellLimit = (emptyRows > 0) ? finalBlockSize : 0.0f; + maxAllowed = std::min(maxAllowed, cellLimit); + } + } + activePieceOffset = std::min(activePieceOffset, maxAllowed); + } // Draw ghost piece (where current piece will land) if (allowActivePieceRender) { @@ -273,7 +328,7 @@ void GameRenderer::renderPlayingState( // Draw the falling piece if (allowActivePieceRender) { - drawPiece(renderer, blocksTex, game->current(), gridX, gridY, finalBlockSize, false); + drawPiece(renderer, blocksTex, game->current(), gridX, gridY, finalBlockSize, false, activePieceOffset); } // Draw line clearing effects diff --git a/src/graphics/renderers/GameRenderer.h b/src/graphics/renderers/GameRenderer.h index cd24d19..77e55a2 100644 --- a/src/graphics/renderers/GameRenderer.h +++ b/src/graphics/renderers/GameRenderer.h @@ -50,7 +50,7 @@ public: private: // Helper functions for drawing game elements static void drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType); - static void drawPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, const Game::Piece& piece, float ox, float oy, float tileSize, bool isGhost = false); + static void drawPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, const Game::Piece& piece, float ox, float oy, float tileSize, bool isGhost = false, float pixelOffsetY = 0.0f); static void drawSmallPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, PieceType pieceType, float x, float y, float tileSize); // Helper function for drawing rectangles diff --git a/src/main.cpp b/src/main.cpp index 020a158..b77bf4e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -913,6 +913,31 @@ int main(int, char **) running = false; }; + auto beginStateFade = [&](AppState targetState, bool armGameplayCountdown) { + if (!ctx.stateManager) { + return; + } + if (state == targetState) { + return; + } + if (menuFadePhase != MenuFadePhase::None) { + return; + } + + menuFadePhase = MenuFadePhase::FadeOut; + menuFadeClockMs = 0.0; + menuFadeAlpha = 0.0f; + menuFadeTarget = targetState; + menuPlayCountdownArmed = armGameplayCountdown; + gameplayCountdownActive = false; + gameplayCountdownIndex = 0; + gameplayCountdownElapsed = 0.0; + + if (!armGameplayCountdown) { + game.setPaused(false); + } + }; + auto startMenuPlayTransition = [&]() { if (!ctx.stateManager) { return; @@ -922,20 +947,22 @@ int main(int, char **) ctx.stateManager->setState(state); return; } - if (menuFadePhase != MenuFadePhase::None) { - return; - } - menuFadePhase = MenuFadePhase::FadeOut; - menuFadeClockMs = 0.0; - menuFadeAlpha = 0.0f; - menuFadeTarget = AppState::Playing; - menuPlayCountdownArmed = true; - gameplayCountdownActive = false; - gameplayCountdownIndex = 0; - gameplayCountdownElapsed = 0.0; + beginStateFade(AppState::Playing, true); }; ctx.startPlayTransition = startMenuPlayTransition; + auto requestStateFade = [&](AppState targetState) { + if (!ctx.stateManager) { + return; + } + if (targetState == AppState::Playing) { + startMenuPlayTransition(); + return; + } + beginStateFade(targetState, false); + }; + ctx.requestFadeTransition = requestStateFade; + // Instantiate state objects auto loadingState = std::make_unique(ctx); auto menuState = std::make_unique(ctx); @@ -1100,11 +1127,9 @@ int main(int, char **) if (pointInRect(buttonRects[0])) { startMenuPlayTransition(); } else if (pointInRect(buttonRects[1])) { - state = AppState::LevelSelector; - stateMgr.setState(state); + requestStateFade(AppState::LevelSelector); } else if (pointInRect(buttonRects[2])) { - state = AppState::Options; - stateMgr.setState(state); + requestStateFade(AppState::Options); } else if (pointInRect(buttonRects[3])) { showExitConfirmPopup = true; exitPopupSelectedButton = 1; @@ -1461,14 +1486,23 @@ int main(int, char **) menuFadeClockMs += frameMs; menuFadeAlpha = std::min(1.0f, float(menuFadeClockMs / MENU_PLAY_FADE_DURATION_MS)); if (menuFadeClockMs >= MENU_PLAY_FADE_DURATION_MS) { - if (menuFadeTarget == AppState::Playing) { + if (state != menuFadeTarget) { state = menuFadeTarget; stateMgr.setState(state); + } + + if (menuFadeTarget == AppState::Playing) { menuPlayCountdownArmed = true; gameplayCountdownActive = false; gameplayCountdownIndex = 0; gameplayCountdownElapsed = 0.0; game.setPaused(true); + } else { + menuPlayCountdownArmed = false; + gameplayCountdownActive = false; + gameplayCountdownIndex = 0; + gameplayCountdownElapsed = 0.0; + game.setPaused(false); } menuFadePhase = MenuFadePhase::FadeIn; menuFadeClockMs = MENU_PLAY_FADE_DURATION_MS; diff --git a/src/states/LevelSelectorState.cpp b/src/states/LevelSelectorState.cpp index b293204..5996c26 100644 --- a/src/states/LevelSelectorState.cpp +++ b/src/states/LevelSelectorState.cpp @@ -326,14 +326,18 @@ void LevelSelectorState::selectLevel(int level) { *ctx.startLevelSelection = level; } // Transition back to menu - if (ctx.stateManager) { + if (ctx.requestFadeTransition) { + ctx.requestFadeTransition(AppState::Menu); + } else if (ctx.stateManager) { ctx.stateManager->setState(AppState::Menu); } } void LevelSelectorState::closePopup() { // Transition back to menu without changing level - if (ctx.stateManager) { + if (ctx.requestFadeTransition) { + ctx.requestFadeTransition(AppState::Menu); + } else if (ctx.stateManager) { ctx.stateManager->setState(AppState::Menu); } } diff --git a/src/states/MenuState.cpp b/src/states/MenuState.cpp index 5b42fd1..7344229 100644 --- a/src/states/MenuState.cpp +++ b/src/states/MenuState.cpp @@ -126,10 +126,18 @@ void MenuState::handleEvent(const SDL_Event& e) { triggerPlay(); break; case 1: - ctx.stateManager->setState(AppState::LevelSelector); + if (ctx.requestFadeTransition) { + ctx.requestFadeTransition(AppState::LevelSelector); + } else if (ctx.stateManager) { + ctx.stateManager->setState(AppState::LevelSelector); + } break; case 2: - ctx.stateManager->setState(AppState::Options); + if (ctx.requestFadeTransition) { + ctx.requestFadeTransition(AppState::Options); + } else if (ctx.stateManager) { + ctx.stateManager->setState(AppState::Options); + } break; case 3: setExitPrompt(true); diff --git a/src/states/OptionsState.cpp b/src/states/OptionsState.cpp index bcd0bf4..9fc3e2a 100644 --- a/src/states/OptionsState.cpp +++ b/src/states/OptionsState.cpp @@ -61,6 +61,10 @@ void OptionsState::handleEvent(const SDL_Event& e) { toggleSoundFx(); return; } + if (m_selectedField == Field::SmoothScroll) { + toggleSmoothScroll(); + return; + } break; default: break; @@ -151,7 +155,7 @@ void OptionsState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l SDL_SetRenderDrawColor(renderer, 40, 80, 140, 240); SDL_RenderRect(renderer, &inner); - constexpr int rowCount = 4; + constexpr int rowCount = 5; const float rowHeight = 60.0f; const float spacing = std::max(0.0f, (inner.h - rowHeight * rowCount) / (rowCount + 1)); @@ -180,7 +184,7 @@ void OptionsState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l int valueW = 0, valueH = 0; float valueScale = (field == Field::Back) ? 1.3f : 1.5f; retroFont->measure(value, valueScale, valueW, valueH); - bool rightAlign = (field == Field::Fullscreen || field == Field::Music || field == Field::SoundFx); + bool rightAlign = (field == Field::Fullscreen || field == Field::Music || field == Field::SoundFx || field == Field::SmoothScroll); float valX = rightAlign ? (row.x + row.w - static_cast(valueW) - 16.0f) : (row.x + (row.w - static_cast(valueW)) * 0.5f); @@ -197,6 +201,8 @@ void OptionsState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l rowY += rowHeight + spacing; drawField(Field::SoundFx, rowY, "SOUND FX", isSoundFxEnabled() ? "ON" : "OFF"); rowY += rowHeight + spacing; + drawField(Field::SmoothScroll, rowY, "SMOOTH SCROLL", isSmoothScrollEnabled() ? "ON" : "OFF"); + rowY += rowHeight + spacing; drawField(Field::Back, rowY, "", "RETURN TO MENU"); (void)retroFont; // footer removed for cleaner layout @@ -220,6 +226,9 @@ void OptionsState::activateSelection() { case Field::SoundFx: toggleSoundFx(); break; + case Field::SmoothScroll: + toggleSmoothScroll(); + break; case Field::Back: exitToMenu(); break; @@ -266,8 +275,16 @@ void OptionsState::toggleSoundFx() { Settings::instance().save(); } +void OptionsState::toggleSmoothScroll() { + bool next = !Settings::instance().isSmoothScrollEnabled(); + Settings::instance().setSmoothScrollEnabled(next); + Settings::instance().save(); +} + void OptionsState::exitToMenu() { - if (ctx.stateManager) { + if (ctx.requestFadeTransition) { + ctx.requestFadeTransition(AppState::Menu); + } else if (ctx.stateManager) { ctx.stateManager->setState(AppState::Menu); } } @@ -289,3 +306,7 @@ bool OptionsState::isMusicEnabled() const { bool OptionsState::isSoundFxEnabled() const { return SoundEffectManager::instance().isEnabled(); } + +bool OptionsState::isSmoothScrollEnabled() const { + return Settings::instance().isSmoothScrollEnabled(); +} diff --git a/src/states/OptionsState.h b/src/states/OptionsState.h index 58885e3..2ae8638 100644 --- a/src/states/OptionsState.h +++ b/src/states/OptionsState.h @@ -16,7 +16,8 @@ private: Fullscreen = 0, Music = 1, SoundFx = 2, - Back = 3 + SmoothScroll = 3, + Back = 4 }; static constexpr int MAX_NAME_LENGTH = 12; @@ -29,8 +30,10 @@ private: void toggleFullscreen(); void toggleMusic(); void toggleSoundFx(); + void toggleSmoothScroll(); void exitToMenu(); bool isFullscreen() const; bool isMusicEnabled() const; bool isSoundFxEnabled() const; + bool isSmoothScrollEnabled() const; }; diff --git a/src/states/State.h b/src/states/State.h index 1834d48..87601a8 100644 --- a/src/states/State.h +++ b/src/states/State.h @@ -13,6 +13,7 @@ class Starfield; class Starfield3D; class FontAtlas; class LineEffect; +enum class AppState; // Forward declare StateManager so StateContext can hold a pointer without // including the StateManager header here. @@ -58,6 +59,7 @@ struct StateContext { std::function queryFullscreen; // Optional callback if fullscreenFlag is not reliable std::function requestQuit; // Allows menu/option states to close the app gracefully std::function startPlayTransition; // Optional fade hook when transitioning from menu to gameplay + std::function requestFadeTransition; // Generic state fade requests (menu/options/level) // Pointer to the application's StateManager so states can request transitions StateManager* stateManager = nullptr; };