update visually

This commit is contained in:
2025-12-22 18:49:06 +01:00
parent d3ca238a51
commit 3c9dc0ff65
3 changed files with 232 additions and 41 deletions

View File

@ -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;

View File

@ -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<float>(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<float>(std::rand() % 17);
p.x = centerX + jitter;
p.y = m_rect.y + m_rect.h + static_cast<float>(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<float>(std::rand() % 37));
p.vy = 180.0f + static_cast<float>(std::rand() % 180);
p.w = 1.0f + static_cast<float>(std::rand() % 2);
p.h = 1.0f + static_cast<float>(std::rand() % 2);
p.alpha = 240.0f;
} else {
p.vx = (-14.0f + static_cast<float>(std::rand() % 29));
p.vy = 160.0f + static_cast<float>(std::rand() % 200);
p.w = 1.0f + static_cast<float>(std::rand() % 3);
p.h = 3.0f + static_cast<float>(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;
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<Uint8>(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<Uint8>(std::min(255, static_cast<int>(aura.r) + 10));
aura.g = static_cast<Uint8>(std::min(255, static_cast<int>(aura.g) + 10));
aura.b = static_cast<Uint8>(std::min(255, static_cast<int>(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<Uint8>(std::min(255, static_cast<int>(auraOuter.r) + 10));
auraOuter.g = static_cast<Uint8>(std::min(255, static_cast<int>(auraOuter.g) + 5));
auraOuter.b = static_cast<Uint8>(std::min(255, static_cast<int>(auraOuter.b) + 55));
SDL_Color auraInner = aura;
auraInner.r = static_cast<Uint8>(std::min(255, static_cast<int>(auraInner.r) + 40));
auraInner.g = static_cast<Uint8>(std::min(255, static_cast<int>(auraInner.g) + 40));
auraInner.b = static_cast<Uint8>(std::min(255, static_cast<int>(auraInner.b) + 70));
// Wider + softer outer halo, then tighter inner glow.
drawGlow(62.0f, static_cast<Uint8>(std::clamp(12.0f * auraBoost * flashBoost, 0.0f, 255.0f)), auraOuter);
drawGlow(44.0f, static_cast<Uint8>(std::clamp(20.0f * auraBoost * flashBoost, 0.0f, 255.0f)), auraOuter);
drawGlow(30.0f, static_cast<Uint8>(std::clamp(34.0f * auraBoost * flashBoost, 0.0f, 255.0f)), auraOuter);
drawGlow(18.0f, static_cast<Uint8>(std::clamp(54.0f * auraBoost * flashBoost, 0.0f, 255.0f)), auraInner);
drawGlow(10.0f, static_cast<Uint8>(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<Uint8>(std::min(255, static_cast<int>(hot.r) + 35));
hot.g = static_cast<Uint8>(std::min(255, static_cast<int>(hot.g) + 35));
hot.b = static_cast<Uint8>(std::min(255, static_cast<int>(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<Uint8>(std::clamp((m_state == SyncState::Synced ? 85.0f : 55.0f) * flashBoost, 0.0f, 255.0f));
Uint8 ha2 = static_cast<Uint8>(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<Uint8>(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<float>(i) / static_cast<float>(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<Uint8>(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<Uint8>(180.0f + pulse * 75.0f);
bodyA = static_cast<Uint8>(std::clamp(175.0f + pulse01 * 75.0f, 0.0f, 255.0f));
}
// Keep the center more translucent; let glow carry intensity.
bodyA = static_cast<Uint8>(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<Uint8>(std::clamp(bodyA * 0.60f, 0.0f, 255.0f)));
SDL_RenderFillRect(renderer, &left);
SDL_RenderFillRect(renderer, &right);
SDL_SetRenderDrawColor(renderer,
static_cast<Uint8>(std::min(255, static_cast<int>(color.r) + 35)),
static_cast<Uint8>(std::min(255, static_cast<int>(color.g) + 35)),
static_cast<Uint8>(std::min(255, static_cast<int>(color.b) + 55)),
static_cast<Uint8>(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<Uint8>(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<Uint8>(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<Uint8>(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<Uint8>(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);
}

View File

@ -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<SyncParticle> 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);