From 3c9dc0ff65d03fe388d54a3c032c590d9156b6f7 Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Mon, 22 Dec 2025 18:49:06 +0100 Subject: [PATCH] update visually --- src/graphics/renderers/GameRenderer.cpp | 2 +- src/graphics/renderers/SyncLineRenderer.cpp | 262 +++++++++++++++++--- src/graphics/renderers/SyncLineRenderer.h | 9 +- 3 files changed, 232 insertions(+), 41 deletions(-) diff --git a/src/graphics/renderers/GameRenderer.cpp b/src/graphics/renderers/GameRenderer.cpp index da3b07a..ddd4c94 100644 --- a/src/graphics/renderers/GameRenderer.cpp +++ b/src/graphics/renderers/GameRenderer.cpp @@ -1897,7 +1897,7 @@ void GameRenderer::renderCoopPlayingState( SDL_RenderFillRect(renderer, &fr); }; - static constexpr float COOP_GAP_PX = 10.0f; + static constexpr float COOP_GAP_PX = 20.0f; const float availableWidth = logicalW - (MIN_MARGIN * 2) - (PANEL_WIDTH * 2) - (PANEL_SPACING * 2); const float availableHeight = logicalH - TOP_MARGIN - BOTTOM_MARGIN - NEXT_PANEL_HEIGHT; diff --git a/src/graphics/renderers/SyncLineRenderer.cpp b/src/graphics/renderers/SyncLineRenderer.cpp index ddf3153..bf15bd6 100644 --- a/src/graphics/renderers/SyncLineRenderer.cpp +++ b/src/graphics/renderers/SyncLineRenderer.cpp @@ -11,15 +11,51 @@ SyncLineRenderer::SyncLineRenderer() m_particles.reserve(MAX_PARTICLES); } +static float syncWobbleX(float t) { + // Small, smooth horizontal motion to make the conduit feel fluid. + // Kept subtle so it doesn't distract from gameplay. + return std::sinf(t * 2.1f) * 1.25f + std::sinf(t * 5.2f + 1.3f) * 0.55f; +} + void SyncLineRenderer::SpawnParticle() { if (m_particles.size() >= MAX_PARTICLES) { return; } SyncParticle p; - p.y = m_rect.y + m_rect.h; - p.speed = 120.0f + static_cast(std::rand() % 80); - p.alpha = 180.0f; + const float centerX = (m_rect.x + (m_rect.w * 0.5f)) + syncWobbleX(m_time); + // Spawn around the beam center so it reads like a conduit. + const float jitter = -8.0f + static_cast(std::rand() % 17); + + p.x = centerX + jitter; + p.y = m_rect.y + m_rect.h + static_cast(std::rand() % 10); + + // Two styles: tiny sparkle dots + short streaks. + const bool dot = (std::rand() % 100) < 35; + if (dot) { + p.vx = (-18.0f + static_cast(std::rand() % 37)); + p.vy = 180.0f + static_cast(std::rand() % 180); + p.w = 1.0f + static_cast(std::rand() % 2); + p.h = 1.0f + static_cast(std::rand() % 2); + p.alpha = 240.0f; + } else { + p.vx = (-14.0f + static_cast(std::rand() % 29)); + p.vy = 160.0f + static_cast(std::rand() % 200); + p.w = 1.0f + static_cast(std::rand() % 3); + p.h = 3.0f + static_cast(std::rand() % 10); + p.alpha = 220.0f; + } + + // Slight color variance (cyan/green/white) to keep it energetic. + const int roll = std::rand() % 100; + if (roll < 55) { + p.color = SDL_Color{110, 255, 210, 255}; + } else if (roll < 90) { + p.color = SDL_Color{120, 210, 255, 255}; + } else { + p.color = SDL_Color{255, 255, 255, 255}; + } + m_particles.push_back(p); } @@ -44,7 +80,7 @@ void SyncLineRenderer::TriggerClearFlash() { m_flashTimer = FLASH_DURATION; // Reward burst: strong visual feedback on cooperative clear. - SpawnBurst(20); + SpawnBurst(56); } void SyncLineRenderer::Update(float deltaTime) { @@ -53,16 +89,18 @@ void SyncLineRenderer::Update(float deltaTime) { // State-driven particle spawning float spawnRatePerSec = 0.0f; + int particlesPerSpawn = 1; switch (m_state) { case SyncState::LeftReady: case SyncState::RightReady: - spawnRatePerSec = 3.0f; // slow/occasional + spawnRatePerSec = 24.0f; // steady break; case SyncState::Synced: - spawnRatePerSec = 14.0f; // continuous + spawnRatePerSec = 78.0f; // very heavy stream + particlesPerSpawn = 2; break; default: - spawnRatePerSec = 0.0f; + spawnRatePerSec = 18.0f; // always-on sparkle stream break; } @@ -72,17 +110,25 @@ void SyncLineRenderer::Update(float deltaTime) { m_spawnAcc += deltaTime * spawnRatePerSec; while (m_spawnAcc >= 1.0f) { m_spawnAcc -= 1.0f; - SpawnParticle(); + for (int i = 0; i < particlesPerSpawn; ++i) { + SpawnParticle(); + } } } // Update particles for (auto& p : m_particles) { - p.y -= p.speed * deltaTime; - p.alpha -= 120.0f * deltaTime; + p.x += p.vx * deltaTime; + p.y -= p.vy * deltaTime; + // Slow drift & fade. + p.vx *= (1.0f - 0.35f * deltaTime); + p.alpha -= 115.0f * deltaTime; } std::erase_if(m_particles, [&](const SyncParticle& p) { - return p.y < m_rect.y || p.alpha <= 0.0f; + // Cull when out of view or too far from the beam. + const float centerX = (m_rect.x + (m_rect.w * 0.5f)) + syncWobbleX(m_time); + const float maxDx = 18.0f; + return (p.y < (m_rect.y - 16.0f)) || p.alpha <= 0.0f || std::fabs(p.x - centerX) > maxDx; }); if (m_state == SyncState::ClearFlash) { @@ -116,57 +162,197 @@ void SyncLineRenderer::Render(SDL_Renderer* renderer) { return; } + // We render the conduit with lots of translucent layers. Using additive blending + // for glow/pulse makes it read like a blurred beam without shaders. SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); - // 1) Pulse wave (Synced only) + const float wobbleX = syncWobbleX(m_time); + const float centerX = (m_rect.x + (m_rect.w * 0.5f)) + wobbleX; + const float h = m_rect.h; + const float hotspotH = std::clamp(h * 0.12f, 18.0f, 44.0f); + + // Flash factor (0..1) + const float flashT = (m_state == SyncState::ClearFlash && FLASH_DURATION > 0.0f) + ? std::clamp(m_flashTimer / FLASH_DURATION, 0.0f, 1.0f) + : 0.0f; + + SDL_Color color = GetBaseColor(); + + // Synced pulse drives aura + core intensity. + float pulse01 = 0.0f; if (m_state == SyncState::Synced) { - float wave = std::fmod(m_pulseTime * 2.0f, 1.0f); - float width = 4.0f + wave * 12.0f; - Uint8 alpha = static_cast(120.0f * (1.0f - wave)); + pulse01 = 0.5f + 0.5f * std::sinf(m_time * 6.0f); + } + + // 1) Outer aura layers (bloom-like using rectangles) + auto drawGlow = [&](float extraW, Uint8 a, SDL_Color c) { + SDL_FRect fr{ + centerX - (m_rect.w + extraW) * 0.5f, + m_rect.y, + m_rect.w + extraW, + m_rect.h + }; + SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, a); + SDL_RenderFillRect(renderer, &fr); + }; + + SDL_Color aura = color; + // Slightly bias aura towards cyan so it reads “energy conduit”. + aura.r = static_cast(std::min(255, static_cast(aura.r) + 10)); + aura.g = static_cast(std::min(255, static_cast(aura.g) + 10)); + aura.b = static_cast(std::min(255, static_cast(aura.b) + 35)); + + const float auraBoost = (m_state == SyncState::Synced) ? (0.70f + 0.80f * pulse01) : 0.70f; + const float flashBoost = 1.0f + flashT * 1.45f; + + SDL_BlendMode oldBlend = SDL_BLENDMODE_BLEND; + SDL_GetRenderDrawBlendMode(renderer, &oldBlend); + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_ADD); + + SDL_Color auraOuter = aura; + auraOuter.r = static_cast(std::min(255, static_cast(auraOuter.r) + 10)); + auraOuter.g = static_cast(std::min(255, static_cast(auraOuter.g) + 5)); + auraOuter.b = static_cast(std::min(255, static_cast(auraOuter.b) + 55)); + + SDL_Color auraInner = aura; + auraInner.r = static_cast(std::min(255, static_cast(auraInner.r) + 40)); + auraInner.g = static_cast(std::min(255, static_cast(auraInner.g) + 40)); + auraInner.b = static_cast(std::min(255, static_cast(auraInner.b) + 70)); + + // Wider + softer outer halo, then tighter inner glow. + drawGlow(62.0f, static_cast(std::clamp(12.0f * auraBoost * flashBoost, 0.0f, 255.0f)), auraOuter); + drawGlow(44.0f, static_cast(std::clamp(20.0f * auraBoost * flashBoost, 0.0f, 255.0f)), auraOuter); + drawGlow(30.0f, static_cast(std::clamp(34.0f * auraBoost * flashBoost, 0.0f, 255.0f)), auraOuter); + drawGlow(18.0f, static_cast(std::clamp(54.0f * auraBoost * flashBoost, 0.0f, 255.0f)), auraInner); + drawGlow(10.0f, static_cast(std::clamp(78.0f * auraBoost * flashBoost, 0.0f, 255.0f)), auraInner); + + // 2) Hotspots near top/bottom (adds that “powered endpoints” vibe) + SDL_Color hot = auraInner; + hot.r = static_cast(std::min(255, static_cast(hot.r) + 35)); + hot.g = static_cast(std::min(255, static_cast(hot.g) + 35)); + hot.b = static_cast(std::min(255, static_cast(hot.b) + 35)); + { + const float hotW1 = 34.0f; + const float hotW2 = 18.0f; + SDL_FRect topHot1{ centerX - (m_rect.w + hotW1) * 0.5f, m_rect.y, m_rect.w + hotW1, hotspotH }; + SDL_FRect botHot1{ centerX - (m_rect.w + hotW1) * 0.5f, m_rect.y + m_rect.h - hotspotH, m_rect.w + hotW1, hotspotH }; + SDL_FRect topHot2{ centerX - (m_rect.w + hotW2) * 0.5f, m_rect.y + hotspotH * 0.12f, m_rect.w + hotW2, hotspotH * 0.78f }; + SDL_FRect botHot2{ centerX - (m_rect.w + hotW2) * 0.5f, m_rect.y + m_rect.h - hotspotH * 0.90f, m_rect.w + hotW2, hotspotH * 0.78f }; + + Uint8 ha1 = static_cast(std::clamp((m_state == SyncState::Synced ? 85.0f : 55.0f) * flashBoost, 0.0f, 255.0f)); + Uint8 ha2 = static_cast(std::clamp((m_state == SyncState::Synced ? 130.0f : 90.0f) * flashBoost, 0.0f, 255.0f)); + SDL_SetRenderDrawColor(renderer, hot.r, hot.g, hot.b, ha1); + SDL_RenderFillRect(renderer, &topHot1); + SDL_RenderFillRect(renderer, &botHot1); + SDL_SetRenderDrawColor(renderer, 255, 255, 255, ha2); + SDL_RenderFillRect(renderer, &topHot2); + SDL_RenderFillRect(renderer, &botHot2); + } + + // 3) Synced pulse wave (a travelling “breath” around the beam) + if (m_state == SyncState::Synced) { + float wave = std::fmod(m_pulseTime * 2.4f, 1.0f); + float width = 10.0f + wave * 26.0f; + Uint8 alpha = static_cast(std::clamp(150.0f * (1.0f - wave) * flashBoost, 0.0f, 255.0f)); SDL_FRect waveRect{ - m_rect.x - width * 0.5f, + centerX - (m_rect.w + width) * 0.5f, m_rect.y, m_rect.w + width, m_rect.h }; - SDL_SetRenderDrawColor(renderer, 100, 255, 120, alpha); + SDL_SetRenderDrawColor(renderer, 140, 255, 220, alpha); SDL_RenderFillRect(renderer, &waveRect); } - SDL_Color color = GetBaseColor(); + // 4) Shimmer bands (stylish motion inside the conduit) + { + const int bands = 7; + const float speed = (m_state == SyncState::Synced) ? 160.0f : 95.0f; + const float bandW = m_rect.w + 12.0f; + for (int i = 0; i < bands; ++i) { + const float phase = (static_cast(i) / static_cast(bands)); + const float y = m_rect.y + std::fmod(m_time * speed + phase * h, h); + const float fade = 0.35f + 0.65f * std::sinf((m_time * 2.1f) + phase * 6.28318f); + const float bandH = 2.0f + (phase * 2.0f); + Uint8 a = static_cast(std::clamp((26.0f + 36.0f * pulse01) * std::fabs(fade) * flashBoost, 0.0f, 255.0f)); + SDL_FRect fr{ centerX - bandW * 0.5f, y, bandW, bandH }; + SDL_SetRenderDrawColor(renderer, 200, 255, 255, a); + SDL_RenderFillRect(renderer, &fr); + } + } + // 5) Core beam (thin bright core + thicker body with horizontal gradient) + Uint8 bodyA = color.a; if (m_state == SyncState::Synced) { - float pulse = 0.5f + 0.5f * std::sinf(m_time * 6.0f); - color.a = static_cast(180.0f + pulse * 75.0f); + bodyA = static_cast(std::clamp(175.0f + pulse01 * 75.0f, 0.0f, 255.0f)); + } + // Keep the center more translucent; let glow carry intensity. + bodyA = static_cast(std::clamp(bodyA * (0.72f + flashT * 0.35f), 0.0f, 255.0f)); + + // Render a smooth-looking body by stacking a few vertical strips. + // This approximates a gradient (bright center -> soft edges) without shaders. + { + // Allow thinner beam while keeping gradient readable. + const float bodyW = std::max(4.0f, m_rect.w); + const float x0 = centerX - bodyW * 0.5f; + + SDL_FRect left{ x0, m_rect.y, bodyW * 0.34f, m_rect.h }; + SDL_FRect mid{ x0 + bodyW * 0.34f, m_rect.y, bodyW * 0.32f, m_rect.h }; + SDL_FRect right{ x0 + bodyW * 0.66f, m_rect.y, bodyW * 0.34f, m_rect.h }; + + SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, static_cast(std::clamp(bodyA * 0.60f, 0.0f, 255.0f))); + SDL_RenderFillRect(renderer, &left); + SDL_RenderFillRect(renderer, &right); + + SDL_SetRenderDrawColor(renderer, + static_cast(std::min(255, static_cast(color.r) + 35)), + static_cast(std::min(255, static_cast(color.g) + 35)), + static_cast(std::min(255, static_cast(color.b) + 55)), + static_cast(std::clamp(bodyA * 0.88f, 0.0f, 255.0f))); + SDL_RenderFillRect(renderer, &mid); } - // 2) Base SYNC line - SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a); - SDL_RenderFillRect(renderer, &m_rect); + SDL_FRect coreRect{ centerX - 1.1f, m_rect.y, 2.2f, m_rect.h }; + Uint8 coreA = static_cast(std::clamp(210.0f + pulse01 * 70.0f + flashT * 95.0f, 0.0f, 255.0f)); + SDL_SetRenderDrawColor(renderer, 255, 255, 255, coreA); + SDL_RenderFillRect(renderer, &coreRect); - // 3) Energy particles + // Switch back to normal alpha blend for particles so they stay readable. + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); + + // 6) Energy particles (sparks/streaks traveling upward) for (const auto& p : m_particles) { - SDL_FRect dot{ - m_rect.x - 3.0f, - p.y, - m_rect.w + 6.0f, - 3.0f - }; - Uint8 a = static_cast(std::clamp(p.alpha, 0.0f, 255.0f)); - SDL_SetRenderDrawColor(renderer, 120, 255, 180, a); - SDL_RenderFillRect(renderer, &dot); + + // Add a tiny sinusoidal sway so the stream feels alive. + const float sway = std::sinf((p.y * 0.045f) + (m_time * 6.2f)) * 0.9f; + SDL_FRect spark{ (p.x + sway) - (p.w * 0.5f), p.y, p.w, p.h }; + SDL_SetRenderDrawColor(renderer, p.color.r, p.color.g, p.color.b, a); + SDL_RenderFillRect(renderer, &spark); + + // A little aura around each spark helps it read at speed. + if (a > 40) { + SDL_FRect sparkGlow{ spark.x - 1.0f, spark.y - 1.0f, spark.w + 2.0f, spark.h + 2.0f }; + SDL_SetRenderDrawColor(renderer, p.color.r, p.color.g, p.color.b, static_cast(a * 0.35f)); + SDL_RenderFillRect(renderer, &sparkGlow); + } } - // 4) Flash/glow overlay + // 7) Flash/glow overlay (adds “clear burst” punch) if (m_state == SyncState::ClearFlash) { - SDL_FRect glow = m_rect; - glow.x -= 3; - glow.w += 6; + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_ADD); - SDL_SetRenderDrawColor(renderer, 255, 255, 255, 180); + const float extra = 74.0f; + SDL_FRect glow{ centerX - (m_rect.w + extra) * 0.5f, m_rect.y, m_rect.w + extra, m_rect.h }; + Uint8 ga = static_cast(std::clamp(90.0f + 140.0f * flashT, 0.0f, 255.0f)); + SDL_SetRenderDrawColor(renderer, 255, 255, 255, ga); SDL_RenderFillRect(renderer, &glow); + + SDL_SetRenderDrawBlendMode(renderer, oldBlend); } + + // Restore whatever blend mode the caller had. + SDL_SetRenderDrawBlendMode(renderer, oldBlend); } diff --git a/src/graphics/renderers/SyncLineRenderer.h b/src/graphics/renderers/SyncLineRenderer.h index f8b1d25..2a256fb 100644 --- a/src/graphics/renderers/SyncLineRenderer.h +++ b/src/graphics/renderers/SyncLineRenderer.h @@ -24,9 +24,14 @@ public: private: struct SyncParticle { + float x; float y; - float speed; + float vx; + float vy; + float w; + float h; float alpha; + SDL_Color color; }; SDL_FRect m_rect{}; @@ -40,7 +45,7 @@ private: std::vector m_particles; static constexpr float FLASH_DURATION = 0.15f; - static constexpr size_t MAX_PARTICLES = 64; + static constexpr size_t MAX_PARTICLES = 240; void SpawnParticle(); void SpawnBurst(int count);