diff --git a/src/gameplay/core/Game.cpp b/src/gameplay/core/Game.cpp index fe9e205..aab6c90 100644 --- a/src/gameplay/core/Game.cpp +++ b/src/gameplay/core/Game.cpp @@ -55,6 +55,7 @@ void Game::reset(int startLevel_) { // immediately sets up its own level state. std::fill(board.begin(), board.end(), 0); clearAsteroidGrid(); + recentAsteroidExplosions.clear(); std::fill(blockCounts.begin(), blockCounts.end(), 0); bag.clear(); _score = 0; _lines = 0; _level = startLevel_; startLevel = startLevel_; @@ -164,6 +165,7 @@ void Game::setupChallengeLevel(int level, bool preserveStats) { completedLines.clear(); hardDropCells.clear(); hardDropFxId = 0; + recentAsteroidExplosions.clear(); fallAcc = 0.0; gameOver = false; paused = false; @@ -548,6 +550,7 @@ void Game::clearCompletedLines() { void Game::actualClearLines() { if (completedLines.empty()) return; + recentAsteroidExplosions.clear(); std::array newBoard{}; std::array, COLS*ROWS> newAst{}; diff --git a/src/gameplay/core/Game.h b/src/gameplay/core/Game.h index c285f99..920ff97 100644 --- a/src/gameplay/core/Game.h +++ b/src/gameplay/core/Game.h @@ -76,6 +76,8 @@ public: void updateElapsedTime(); // Update elapsed time from system clock bool isSoftDropping() const { return softDropping; } const std::array, COLS*ROWS>& asteroidCells() const { return asteroidGrid; } + const std::vector& getRecentAsteroidExplosions() const { return recentAsteroidExplosions; } + void clearRecentAsteroidExplosions() { recentAsteroidExplosions.clear(); } // Block statistics const std::array& getBlockCounts() const { return blockCounts; } @@ -181,6 +183,9 @@ private: std::optional pendingAsteroidDestroyType; bool asteroidDestroySoundPreplayed{false}; + // Recent asteroid explosion positions (grid coords) for renderer FX + std::vector recentAsteroidExplosions; + // Internal helpers ---------------------------------------------------- void refillBag(); void spawn(); diff --git a/src/graphics/renderers/GameRenderer.cpp b/src/graphics/renderers/GameRenderer.cpp index 36b49bf..154cf65 100644 --- a/src/graphics/renderers/GameRenderer.cpp +++ b/src/graphics/renderers/GameRenderer.cpp @@ -49,6 +49,31 @@ Starfield3D s_inGridStarfield; bool s_starfieldInitialized = false; std::vector s_sparkles; float s_sparkleSpawnAcc = 0.0f; + +struct AsteroidBurst { + float x; + float y; + float lifeMs; + float maxLifeMs; + float baseRadius; + SDL_Color color; + float spin; +}; + +std::vector s_asteroidBursts; + +struct AsteroidShard { + float x; + float y; + float vx; + float vy; + float lifeMs; + float maxLifeMs; + float size; + SDL_Color color; +}; + +std::vector s_asteroidShards; } struct TransportEffectState { @@ -927,6 +952,63 @@ void GameRenderer::renderPlayingState( rowDropOffsets[y] = (lineEffect ? lineEffect->getRowDropOffset(y) : 0.0f); } + // Spawn glamour bursts for freshly destroyed asteroids + if (game) { + const auto& bursts = game->getRecentAsteroidExplosions(); + if (!bursts.empty()) { + std::uniform_real_distribution lifeDist(280.0f, 420.0f); + std::uniform_real_distribution radiusDist(finalBlockSize * 0.35f, finalBlockSize * 0.7f); + std::uniform_real_distribution spinDist(-4.0f, 4.0f); + std::uniform_real_distribution shardLife(240.0f, 520.0f); + std::uniform_real_distribution shardVX(-0.16f, 0.16f); + std::uniform_real_distribution shardVY(-0.22f, -0.06f); + std::uniform_real_distribution shardSize(finalBlockSize * 0.06f, finalBlockSize * 0.12f); + for (const auto& p : bursts) { + if (p.x < 0 || p.x >= Game::COLS || p.y < 0 || p.y >= Game::ROWS) { + continue; + } + float fx = gridX + (static_cast(p.x) + 0.5f) * finalBlockSize; + float fy = gridY + (static_cast(p.y) + 0.5f) * finalBlockSize + rowDropOffsets[p.y]; + + SDL_Color palette[3] = { + SDL_Color{255, 230, 120, 255}, + SDL_Color{140, 220, 255, 255}, + SDL_Color{255, 160, 235, 255} + }; + SDL_Color c = palette[s_impactRng() % 3]; + AsteroidBurst burst{ + fx, + fy, + lifeDist(s_impactRng), + 0.0f, + radiusDist(s_impactRng), + c, + spinDist(s_impactRng) + }; + burst.maxLifeMs = burst.lifeMs; + s_asteroidBursts.push_back(burst); + + // Spawn shards for extra sparkle + int shardCount = 10 + (s_impactRng() % 8); + for (int i = 0; i < shardCount; ++i) { + AsteroidShard shard{ + fx, + fy, + shardVX(s_impactRng), + shardVY(s_impactRng), + shardLife(s_impactRng), + 0.0f, + shardSize(s_impactRng), + c + }; + shard.maxLifeMs = shard.lifeMs; + s_asteroidShards.push_back(shard); + } + } + game->clearRecentAsteroidExplosions(); + } + } + // Draw the game board const auto &board = game->boardRef(); const auto &asteroidCells = game->asteroidCells(); @@ -1096,6 +1178,75 @@ void GameRenderer::renderPlayingState( } } + // Update & draw asteroid glamour shards and bursts + if (!s_asteroidShards.empty() || !s_asteroidBursts.empty()) { + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_ADD); + + // Shards + auto shardIt = s_asteroidShards.begin(); + while (shardIt != s_asteroidShards.end()) { + AsteroidShard& s = *shardIt; + s.lifeMs -= sparkDeltaMs; + if (s.lifeMs <= 0.0f) { + shardIt = s_asteroidShards.erase(shardIt); + continue; + } + s.vy += 0.0007f * sparkDeltaMs; + s.x += s.vx * sparkDeltaMs; + s.y += s.vy * sparkDeltaMs; + float lifeRatio = std::clamp(static_cast(s.lifeMs / s.maxLifeMs), 0.0f, 1.0f); + Uint8 alpha = static_cast(lifeRatio * 200.0f); + SDL_SetRenderDrawColor(renderer, s.color.r, s.color.g, s.color.b, alpha); + float size = s.size * (0.7f + (1.0f - lifeRatio) * 0.8f); + SDL_FRect shardRect{ + s.x - size * 0.5f, + s.y - size * 0.5f, + size, + size * 1.4f + }; + SDL_RenderFillRect(renderer, &shardRect); + ++shardIt; + } + + // Bursts + auto it = s_asteroidBursts.begin(); + while (it != s_asteroidBursts.end()) { + AsteroidBurst& b = *it; + b.lifeMs -= sparkDeltaMs; + if (b.lifeMs <= 0.0f) { + it = s_asteroidBursts.erase(it); + continue; + } + float t = 1.0f - static_cast(b.lifeMs / b.maxLifeMs); + float alpha = std::clamp(1.0f - t, 0.0f, 1.0f); + float radius = b.baseRadius * (1.0f + t * 1.6f); + float thickness = std::max(2.0f, radius * 0.25f); + float jitter = std::sin(t * 12.0f + b.spin) * 2.0f; + + SDL_Color c = b.color; + Uint8 a = static_cast(alpha * 220.0f); + SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, a); + SDL_FRect outer{ + b.x - radius + jitter, + b.y - radius + jitter, + radius * 2.0f, + radius * 2.0f + }; + SDL_RenderRect(renderer, &outer); + + SDL_FRect inner{ + b.x - (radius - thickness), + b.y - (radius - thickness), + (radius - thickness) * 2.0f, + (radius - thickness) * 2.0f + }; + SDL_SetRenderDrawColor(renderer, 255, 255, 255, static_cast(a * 0.9f)); + SDL_RenderRect(renderer, &inner); + ++it; + } + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); + } + if (!s_impactSparks.empty()) { SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); auto it = s_impactSparks.begin();