#include "GlobalState.h" #include "Config.h" #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; return instance; } void GlobalState::initialize() { if (m_initialized) { return; } // Initialize timing lastMs = SDL_GetTicks(); loadStart = SDL_GetTicks(); // Initialize viewport to logical dimensions logicalVP = {0, 0, Config::Logical::WIDTH, Config::Logical::HEIGHT}; // Initialize fireworks system fireworks.clear(); lastFireworkTime = 0; m_initialized = true; SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[GlobalState] Initialized"); } void GlobalState::shutdown() { if (!m_initialized) { return; } // Clear fireworks fireworks.clear(); m_initialized = false; 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(); size_t activeCount = 0; for (const auto& fw : fireworks) { if (fw.active) { ++activeCount; } } 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; } const float dtSeconds = static_cast(frameMs / 1000.0); const float deltaMs = static_cast(frameMs); for (auto& firework : fireworks) { if (!firework.active) { continue; } 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(); } } void GlobalState::createFirework(float x, float y) { // Find an inactive firework to reuse TetrisFirework* firework = nullptr; for (auto& fw : fireworks) { if (!fw.active) { firework = &fw; break; } } // If no inactive firework found, create a new one if (!firework) { fireworks.emplace_back(); firework = &fireworks.back(); } 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) }; 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; 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.0f) continue; float lifeRatio = particle.life / particle.maxLife; 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 { 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); } } } SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE); } void GlobalState::resetGameState() { // Reset game-related state leftHeld = false; rightHeld = false; moveTimerMs = 0.0; startLevelSelection = 0; } void GlobalState::resetUIState() { // Reset UI state showSettingsPopup = false; showExitConfirmPopup = false; hoveredButton = -1; } void GlobalState::resetAnimationState() { // Reset animation state logoAnimCounter = 0.0; fireworks.clear(); lastFireworkTime = 0; } void GlobalState::updateLogicalDimensions(int windowWidth, int windowHeight) { // For now, keep logical dimensions proportional to window size // You can adjust this logic based on your specific needs // Option 1: Keep fixed aspect ratio and scale uniformly const float targetAspect = static_cast(Config::Logical::WIDTH) / static_cast(Config::Logical::HEIGHT); const float windowAspect = static_cast(windowWidth) / static_cast(windowHeight); if (windowAspect > targetAspect) { // Window is wider than target aspect - fit to height currentLogicalHeight = Config::Logical::HEIGHT; currentLogicalWidth = static_cast(currentLogicalHeight * windowAspect); } else { // Window is taller than target aspect - fit to width currentLogicalWidth = Config::Logical::WIDTH; currentLogicalHeight = static_cast(currentLogicalWidth / windowAspect); } // Ensure minimum sizes currentLogicalWidth = std::max(currentLogicalWidth, 800); currentLogicalHeight = std::max(currentLogicalHeight, 600); SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[GlobalState] Updated logical dimensions: %dx%d (window: %dx%d)", currentLogicalWidth, currentLogicalHeight, windowWidth, windowHeight); }