Merge branch 'feature/BetterFireworksEffect' into develop
This commit is contained in:
BIN
FreeSans.ttf
BIN
FreeSans.ttf
Binary file not shown.
@ -8,6 +8,9 @@ Fullscreen=1
|
||||
Music=1
|
||||
Sound=1
|
||||
|
||||
[Gameplay]
|
||||
SmoothScroll=1
|
||||
|
||||
[Player]
|
||||
Name=GREGOR
|
||||
|
||||
|
||||
@ -3,6 +3,55 @@
|
||||
#include <SDL3/SDL.h>
|
||||
#include <algorithm>
|
||||
#include <random>
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
constexpr float PI_F = 3.14159265358979323846f;
|
||||
|
||||
float randRange(float minVal, float maxVal) {
|
||||
return minVal + (static_cast<float>(rand()) / static_cast<float>(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<size_t>(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<Uint8>(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<Uint8>(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<float>(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<Uint64>(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<Uint64>(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<float>(frameMs / 1000.0);
|
||||
const float deltaMs = static_cast<float>(frameMs);
|
||||
|
||||
for (auto& firework : fireworks) {
|
||||
if (!firework.active) continue;
|
||||
|
||||
bool hasActiveParticles = false;
|
||||
std::vector<BlockParticle> 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<int>(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<int>(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<int>(std::ceil(radius));
|
||||
for (int dy = -ir; dy <= ir; ++dy) {
|
||||
float row = std::sqrt(std::max(0.0f, radius * radius - static_cast<float>(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<Uint8>((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<int>(spark.x),
|
||||
static_cast<int>(spark.y),
|
||||
static_cast<int>(tailX),
|
||||
static_cast<int>(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<Uint8>(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<int>(particle.x - dx),
|
||||
static_cast<int>(particle.y - dy),
|
||||
static_cast<int>(particle.x + dx),
|
||||
static_cast<int>(particle.y + dy));
|
||||
SDL_RenderLine(renderer,
|
||||
static_cast<int>(particle.x - dy * 0.45f),
|
||||
static_cast<int>(particle.y + dx * 0.45f),
|
||||
static_cast<int>(particle.x + dy * 0.45f),
|
||||
static_cast<int>(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<Uint8>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
@ -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<BlockParticle> particles;
|
||||
std::vector<SparkParticle> sparks;
|
||||
bool active = false;
|
||||
float originX = 0.0f;
|
||||
float originY = 0.0f;
|
||||
float elapsedMs = 0.0f;
|
||||
int nextBurst = 0;
|
||||
std::array<float, 3> burstSchedule{0.0f, 250.0f, 520.0f};
|
||||
std::array<SDL_Color, 3> burstColors{};
|
||||
};
|
||||
|
||||
std::vector<TetrisFirework> 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);
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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";
|
||||
};
|
||||
|
||||
@ -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<PIECE_COUNT;++i) bag.push_back(static_cast<PieceType>(i));
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -194,9 +194,28 @@ void LineEffect::startLineClear(const std::vector<int>& 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<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) {
|
||||
@ -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;
|
||||
|
||||
@ -3,6 +3,9 @@
|
||||
#include <SDL3/SDL.h>
|
||||
#include <vector>
|
||||
#include <random>
|
||||
#include <array>
|
||||
|
||||
#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<int16_t>& sample);
|
||||
void initAudio();
|
||||
SDL_Color pickFireColor() const;
|
||||
|
||||
std::array<float, Game::ROWS> rowDropTargets{};
|
||||
float dropProgress = 0.0f;
|
||||
int dropBlockSize = 0;
|
||||
};
|
||||
|
||||
@ -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<float>(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
|
||||
|
||||
@ -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
|
||||
|
||||
66
src/main.cpp
66
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<LoadingState>(ctx);
|
||||
auto menuState = std::make_unique<MenuState>(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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<float>(valueW) - 16.0f)
|
||||
: (row.x + (row.w - static_cast<float>(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();
|
||||
}
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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<bool()> queryFullscreen; // Optional callback if fullscreenFlag is not reliable
|
||||
std::function<void()> requestQuit; // Allows menu/option states to close the app gracefully
|
||||
std::function<void()> startPlayTransition; // Optional fade hook when transitioning from menu to gameplay
|
||||
std::function<void(AppState)> requestFadeTransition; // Generic state fade requests (menu/options/level)
|
||||
// Pointer to the application's StateManager so states can request transitions
|
||||
StateManager* stateManager = nullptr;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user