sync line added in cooperate mode
This commit is contained in:
@ -51,6 +51,7 @@ set(TETRIS_SOURCES
|
|||||||
src/graphics/ui/Font.cpp
|
src/graphics/ui/Font.cpp
|
||||||
src/graphics/ui/HelpOverlay.cpp
|
src/graphics/ui/HelpOverlay.cpp
|
||||||
src/graphics/renderers/GameRenderer.cpp
|
src/graphics/renderers/GameRenderer.cpp
|
||||||
|
src/graphics/renderers/SyncLineRenderer.cpp
|
||||||
src/graphics/renderers/UIRenderer.cpp
|
src/graphics/renderers/UIRenderer.cpp
|
||||||
src/audio/Audio.cpp
|
src/audio/Audio.cpp
|
||||||
src/gameplay/effects/LineEffect.cpp
|
src/gameplay/effects/LineEffect.cpp
|
||||||
|
|||||||
@ -188,11 +188,13 @@ void LineEffect::initAudio() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void LineEffect::startLineClear(const std::vector<int>& rows, int gridX, int gridY, int blockSize, int gridCols) {
|
void LineEffect::startLineClear(const std::vector<int>& rows, int gridX, int gridY, int blockSize, int gridCols, int gapPx, int gapAfterCol) {
|
||||||
if (rows.empty()) return;
|
if (rows.empty()) return;
|
||||||
|
|
||||||
clearingRows = rows;
|
clearingRows = rows;
|
||||||
effectGridCols = std::max(1, gridCols);
|
effectGridCols = std::max(1, gridCols);
|
||||||
|
effectGapPx = std::max(0, gapPx);
|
||||||
|
effectGapAfterCol = std::clamp(gapAfterCol, 0, effectGridCols);
|
||||||
state = AnimationState::FLASH_WHITE;
|
state = AnimationState::FLASH_WHITE;
|
||||||
timer = 0.0f;
|
timer = 0.0f;
|
||||||
dropProgress = 0.0f;
|
dropProgress = 0.0f;
|
||||||
@ -231,6 +233,9 @@ void LineEffect::createParticles(int row, int gridX, int gridY, int blockSize) {
|
|||||||
const float centerY = gridY + row * blockSize + blockSize * 0.5f;
|
const float centerY = gridY + row * blockSize + blockSize * 0.5f;
|
||||||
for (int col = 0; col < effectGridCols; ++col) {
|
for (int col = 0; col < effectGridCols; ++col) {
|
||||||
float centerX = gridX + col * blockSize + blockSize * 0.5f;
|
float centerX = gridX + col * blockSize + blockSize * 0.5f;
|
||||||
|
if (effectGapPx > 0 && effectGapAfterCol > 0 && col >= effectGapAfterCol) {
|
||||||
|
centerX += static_cast<float>(effectGapPx);
|
||||||
|
}
|
||||||
SDL_Color tint = pickFireColor();
|
SDL_Color tint = pickFireColor();
|
||||||
spawnGlowPulse(centerX, centerY, static_cast<float>(blockSize), tint);
|
spawnGlowPulse(centerX, centerY, static_cast<float>(blockSize), tint);
|
||||||
spawnShardBurst(centerX, centerY, tint);
|
spawnShardBurst(centerX, centerY, tint);
|
||||||
@ -338,8 +343,12 @@ void LineEffect::updateGlowPulses(float dt) {
|
|||||||
glowPulses.end());
|
glowPulses.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
void LineEffect::render(SDL_Renderer* renderer, SDL_Texture* blocksTex, int gridX, int gridY, int blockSize) {
|
void LineEffect::render(SDL_Renderer* renderer, SDL_Texture* blocksTex, int gridX, int gridY, int blockSize, int gapPx, int gapAfterCol) {
|
||||||
if (state == AnimationState::IDLE) return;
|
if (state == AnimationState::IDLE) return;
|
||||||
|
|
||||||
|
// Allow caller to override gap mapping (useful for Coop renderer that inserts a mid-gap).
|
||||||
|
effectGapPx = std::max(0, gapPx);
|
||||||
|
effectGapAfterCol = std::clamp(gapAfterCol, 0, effectGridCols);
|
||||||
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case AnimationState::FLASH_WHITE:
|
case AnimationState::FLASH_WHITE:
|
||||||
@ -384,10 +393,11 @@ void LineEffect::renderFlash(int gridX, int gridY, int blockSize) {
|
|||||||
|
|
||||||
for (int row : clearingRows) {
|
for (int row : clearingRows) {
|
||||||
SDL_SetRenderDrawColor(renderer, 255, 255, 255, alpha);
|
SDL_SetRenderDrawColor(renderer, 255, 255, 255, alpha);
|
||||||
|
const int gapW = (effectGapPx > 0 && effectGapAfterCol > 0 && effectGapAfterCol < effectGridCols) ? effectGapPx : 0;
|
||||||
SDL_FRect flashRect = {
|
SDL_FRect flashRect = {
|
||||||
static_cast<float>(gridX - 4),
|
static_cast<float>(gridX - 4),
|
||||||
static_cast<float>(gridY + row * blockSize - 4),
|
static_cast<float>(gridY + row * blockSize - 4),
|
||||||
static_cast<float>(effectGridCols * blockSize + 8),
|
static_cast<float>(effectGridCols * blockSize + gapW + 8),
|
||||||
static_cast<float>(blockSize + 8)
|
static_cast<float>(blockSize + 8)
|
||||||
};
|
};
|
||||||
SDL_RenderFillRect(renderer, &flashRect);
|
SDL_RenderFillRect(renderer, &flashRect);
|
||||||
|
|||||||
@ -69,11 +69,11 @@ public:
|
|||||||
void shutdown();
|
void shutdown();
|
||||||
|
|
||||||
// Start line clear effect for the specified rows
|
// Start line clear effect for the specified rows
|
||||||
void startLineClear(const std::vector<int>& rows, int gridX, int gridY, int blockSize, int gridCols = Game::COLS);
|
void startLineClear(const std::vector<int>& rows, int gridX, int gridY, int blockSize, int gridCols = Game::COLS, int gapPx = 0, int gapAfterCol = 0);
|
||||||
|
|
||||||
// Update and render the effect
|
// Update and render the effect
|
||||||
bool update(float deltaTime); // Returns true if effect is complete
|
bool update(float deltaTime); // Returns true if effect is complete
|
||||||
void render(SDL_Renderer* renderer, SDL_Texture* blocksTex, int gridX, int gridY, int blockSize);
|
void render(SDL_Renderer* renderer, SDL_Texture* blocksTex, int gridX, int gridY, int blockSize, int gapPx = 0, int gapAfterCol = 0);
|
||||||
float getRowDropOffset(int row) const;
|
float getRowDropOffset(int row) const;
|
||||||
|
|
||||||
// Audio
|
// Audio
|
||||||
@ -121,4 +121,6 @@ private:
|
|||||||
float dropProgress = 0.0f;
|
float dropProgress = 0.0f;
|
||||||
int dropBlockSize = 0;
|
int dropBlockSize = 0;
|
||||||
int effectGridCols = Game::COLS;
|
int effectGridCols = Game::COLS;
|
||||||
|
int effectGapPx = 0;
|
||||||
|
int effectGapAfterCol = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
#include "GameRenderer.h"
|
#include "GameRenderer.h"
|
||||||
|
|
||||||
|
#include "SyncLineRenderer.h"
|
||||||
#include "../../gameplay/core/Game.h"
|
#include "../../gameplay/core/Game.h"
|
||||||
#include "../../gameplay/coop/CoopGame.h"
|
#include "../../gameplay/coop/CoopGame.h"
|
||||||
#include "../../app/Fireworks.h"
|
#include "../../app/Fireworks.h"
|
||||||
@ -1852,6 +1854,9 @@ void GameRenderer::renderCoopPlayingState(
|
|||||||
) {
|
) {
|
||||||
if (!renderer || !game || !pixelFont) return;
|
if (!renderer || !game || !pixelFont) return;
|
||||||
|
|
||||||
|
static SyncLineRenderer s_syncLine;
|
||||||
|
static bool s_lastHadCompletedLines = false;
|
||||||
|
|
||||||
static Uint32 s_lastCoopTick = SDL_GetTicks();
|
static Uint32 s_lastCoopTick = SDL_GetTicks();
|
||||||
Uint32 nowTicks = SDL_GetTicks();
|
Uint32 nowTicks = SDL_GetTicks();
|
||||||
float deltaMs = static_cast<float>(nowTicks - s_lastCoopTick);
|
float deltaMs = static_cast<float>(nowTicks - s_lastCoopTick);
|
||||||
@ -1860,6 +1865,9 @@ void GameRenderer::renderCoopPlayingState(
|
|||||||
deltaMs = 16.0f;
|
deltaMs = 16.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const float deltaSeconds = std::clamp(deltaMs / 1000.0f, 0.0f, 0.033f);
|
||||||
|
s_syncLine.Update(deltaSeconds);
|
||||||
|
|
||||||
const bool smoothScrollEnabled = Settings::instance().isSmoothScrollEnabled();
|
const bool smoothScrollEnabled = Settings::instance().isSmoothScrollEnabled();
|
||||||
struct SmoothState { bool initialized{false}; uint64_t seq{0}; float visualX{0.0f}; float visualY{0.0f}; };
|
struct SmoothState { bool initialized{false}; uint64_t seq{0}; float visualX{0.0f}; float visualY{0.0f}; };
|
||||||
static SmoothState s_leftSmooth{};
|
static SmoothState s_leftSmooth{};
|
||||||
@ -1889,15 +1897,19 @@ void GameRenderer::renderCoopPlayingState(
|
|||||||
SDL_RenderFillRect(renderer, &fr);
|
SDL_RenderFillRect(renderer, &fr);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static constexpr float COOP_GAP_PX = 10.0f;
|
||||||
|
|
||||||
const float availableWidth = logicalW - (MIN_MARGIN * 2) - (PANEL_WIDTH * 2) - (PANEL_SPACING * 2);
|
const float availableWidth = logicalW - (MIN_MARGIN * 2) - (PANEL_WIDTH * 2) - (PANEL_SPACING * 2);
|
||||||
const float availableHeight = logicalH - TOP_MARGIN - BOTTOM_MARGIN - NEXT_PANEL_HEIGHT;
|
const float availableHeight = logicalH - TOP_MARGIN - BOTTOM_MARGIN - NEXT_PANEL_HEIGHT;
|
||||||
|
|
||||||
const float maxBlockSizeW = availableWidth / CoopGame::COLS;
|
const float usableGridWidth = std::max(0.0f, availableWidth - COOP_GAP_PX);
|
||||||
|
const float maxBlockSizeW = usableGridWidth / CoopGame::COLS;
|
||||||
const float maxBlockSizeH = availableHeight / CoopGame::ROWS;
|
const float maxBlockSizeH = availableHeight / CoopGame::ROWS;
|
||||||
const float BLOCK_SIZE = std::min(maxBlockSizeW, maxBlockSizeH);
|
const float BLOCK_SIZE = std::min(maxBlockSizeW, maxBlockSizeH);
|
||||||
const float finalBlockSize = std::max(16.0f, std::min(BLOCK_SIZE, 36.0f));
|
const float finalBlockSize = std::max(16.0f, std::min(BLOCK_SIZE, 36.0f));
|
||||||
|
|
||||||
const float GRID_W = CoopGame::COLS * finalBlockSize;
|
const float HALF_W = 10.0f * finalBlockSize;
|
||||||
|
const float GRID_W = CoopGame::COLS * finalBlockSize + COOP_GAP_PX;
|
||||||
const float GRID_H = CoopGame::ROWS * finalBlockSize;
|
const float GRID_H = CoopGame::ROWS * finalBlockSize;
|
||||||
|
|
||||||
const float totalContentHeight = NEXT_PANEL_HEIGHT + GRID_H;
|
const float totalContentHeight = NEXT_PANEL_HEIGHT + GRID_H;
|
||||||
@ -1923,7 +1935,7 @@ void GameRenderer::renderCoopPlayingState(
|
|||||||
// Handle line clearing effects (defer to LineEffect like single-player)
|
// Handle line clearing effects (defer to LineEffect like single-player)
|
||||||
if (game->hasCompletedLines() && lineEffect && !lineEffect->isActive()) {
|
if (game->hasCompletedLines() && lineEffect && !lineEffect->isActive()) {
|
||||||
auto completedLines = game->getCompletedLines();
|
auto completedLines = game->getCompletedLines();
|
||||||
lineEffect->startLineClear(completedLines, static_cast<int>(gridX), static_cast<int>(gridY), static_cast<int>(finalBlockSize), CoopGame::COLS);
|
lineEffect->startLineClear(completedLines, static_cast<int>(gridX), static_cast<int>(gridY), static_cast<int>(finalBlockSize), CoopGame::COLS, static_cast<int>(COOP_GAP_PX), 10);
|
||||||
if (completedLines.size() == 4) {
|
if (completedLines.size() == 4) {
|
||||||
AppFireworks::spawn(gridX + GRID_W * 0.5f, gridY + GRID_H * 0.5f);
|
AppFireworks::spawn(gridX + GRID_W * 0.5f, gridY + GRID_H * 0.5f);
|
||||||
}
|
}
|
||||||
@ -1935,28 +1947,39 @@ void GameRenderer::renderCoopPlayingState(
|
|||||||
rowDropOffsets[y] = (lineEffect ? lineEffect->getRowDropOffset(y) : 0.0f);
|
rowDropOffsets[y] = (lineEffect ? lineEffect->getRowDropOffset(y) : 0.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grid backdrop and border
|
// Grid backdrop and border (one border around both halves)
|
||||||
drawRectWithOffset(gridX - 3 - contentOffsetX, gridY - 3 - contentOffsetY, GRID_W + 6, GRID_H + 6, {100, 120, 200, 255});
|
drawRectWithOffset(gridX - 3 - contentOffsetX, gridY - 3 - contentOffsetY, GRID_W + 6, GRID_H + 6, {100, 120, 200, 255});
|
||||||
|
// Background for left+right halves
|
||||||
drawRectWithOffset(gridX - contentOffsetX, gridY - contentOffsetY, GRID_W, GRID_H, {20, 25, 35, 255});
|
drawRectWithOffset(gridX - contentOffsetX, gridY - contentOffsetY, GRID_W, GRID_H, {20, 25, 35, 255});
|
||||||
|
// Gap background (slightly darker so the 10px separation reads clearly)
|
||||||
|
drawRectWithOffset(gridX + HALF_W - contentOffsetX, gridY - contentOffsetY, COOP_GAP_PX, GRID_H, {12, 14, 18, 255});
|
||||||
|
|
||||||
// Divider line between halves (between columns 9 and 10)
|
// Sync divider line centered in the gap between halves.
|
||||||
float dividerX = gridX + finalBlockSize * 10.0f;
|
const float dividerCenterX = gridX + HALF_W + (COOP_GAP_PX * 0.5f);
|
||||||
SDL_SetRenderDrawColor(renderer, 180, 210, 255, 235);
|
s_syncLine.SetRect(SDL_FRect{ dividerCenterX - 2.0f, gridY, 4.0f, GRID_H });
|
||||||
SDL_FRect divider{ dividerX - 2.0f, gridY, 4.0f, GRID_H };
|
|
||||||
SDL_RenderFillRect(renderer, ÷r);
|
|
||||||
SDL_SetRenderDrawColor(renderer, 40, 80, 150, 140);
|
|
||||||
SDL_FRect dividerGlow{ dividerX - 4.0f, gridY, 8.0f, GRID_H };
|
|
||||||
SDL_RenderFillRect(renderer, ÷rGlow);
|
|
||||||
|
|
||||||
// Grid lines
|
auto cellX = [&](int col) -> float {
|
||||||
|
float x = gridX + col * finalBlockSize;
|
||||||
|
if (col >= 10) {
|
||||||
|
x += COOP_GAP_PX;
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Grid lines (draw per-half so the gap is clean)
|
||||||
SDL_SetRenderDrawColor(renderer, 40, 45, 60, 255);
|
SDL_SetRenderDrawColor(renderer, 40, 45, 60, 255);
|
||||||
for (int x = 1; x < CoopGame::COLS; ++x) {
|
for (int x = 1; x < 10; ++x) {
|
||||||
float lineX = gridX + x * finalBlockSize;
|
float lineX = gridX + x * finalBlockSize;
|
||||||
SDL_RenderLine(renderer, lineX, gridY, lineX, gridY + GRID_H);
|
SDL_RenderLine(renderer, lineX, gridY, lineX, gridY + GRID_H);
|
||||||
}
|
}
|
||||||
|
for (int x = 1; x < 10; ++x) {
|
||||||
|
float lineX = gridX + HALF_W + COOP_GAP_PX + x * finalBlockSize;
|
||||||
|
SDL_RenderLine(renderer, lineX, gridY, lineX, gridY + GRID_H);
|
||||||
|
}
|
||||||
for (int y = 1; y < CoopGame::ROWS; ++y) {
|
for (int y = 1; y < CoopGame::ROWS; ++y) {
|
||||||
float lineY = gridY + y * finalBlockSize;
|
float lineY = gridY + y * finalBlockSize;
|
||||||
SDL_RenderLine(renderer, gridX, lineY, gridX + GRID_W, lineY);
|
SDL_RenderLine(renderer, gridX, lineY, gridX + HALF_W, lineY);
|
||||||
|
SDL_RenderLine(renderer, gridX + HALF_W + COOP_GAP_PX, lineY, gridX + HALF_W + COOP_GAP_PX + HALF_W, lineY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// In-grid 3D starfield + ambient sparkles (match classic feel, per-half)
|
// In-grid 3D starfield + ambient sparkles (match classic feel, per-half)
|
||||||
@ -1970,9 +1993,9 @@ void GameRenderer::renderCoopPlayingState(
|
|||||||
static float s_leftSparkleSpawnAcc = 0.0f;
|
static float s_leftSparkleSpawnAcc = 0.0f;
|
||||||
static float s_rightSparkleSpawnAcc = 0.0f;
|
static float s_rightSparkleSpawnAcc = 0.0f;
|
||||||
|
|
||||||
float halfW = GRID_W * 0.5f;
|
float halfW = HALF_W;
|
||||||
const float leftGridX = gridX;
|
const float leftGridX = gridX;
|
||||||
const float rightGridX = gridX + halfW;
|
const float rightGridX = gridX + HALF_W + COOP_GAP_PX;
|
||||||
|
|
||||||
Uint32 sparkNow = nowTicks;
|
Uint32 sparkNow = nowTicks;
|
||||||
float sparkDeltaMs = static_cast<float>(sparkNow - s_lastCoopSparkTick);
|
float sparkDeltaMs = static_cast<float>(sparkNow - s_lastCoopSparkTick);
|
||||||
@ -2185,24 +2208,57 @@ void GameRenderer::renderCoopPlayingState(
|
|||||||
// Half-row feedback: lightly tint rows where one side is filled, brighter where both are pending clear
|
// Half-row feedback: lightly tint rows where one side is filled, brighter where both are pending clear
|
||||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||||
const auto& rowStates = game->rowHalfStates();
|
const auto& rowStates = game->rowHalfStates();
|
||||||
|
|
||||||
|
bool leftReady = false;
|
||||||
|
bool rightReady = false;
|
||||||
|
bool synced = false;
|
||||||
|
|
||||||
for (int y = 0; y < CoopGame::ROWS; ++y) {
|
for (int y = 0; y < CoopGame::ROWS; ++y) {
|
||||||
const auto& rs = rowStates[y];
|
const auto& rs = rowStates[y];
|
||||||
float rowY = gridY + y * finalBlockSize;
|
float rowY = gridY + y * finalBlockSize;
|
||||||
|
|
||||||
|
if (rs.leftFull && rs.rightFull) {
|
||||||
|
synced = true;
|
||||||
|
} else {
|
||||||
|
leftReady = leftReady || (rs.leftFull && !rs.rightFull);
|
||||||
|
rightReady = rightReady || (rs.rightFull && !rs.leftFull);
|
||||||
|
}
|
||||||
|
|
||||||
if (rs.leftFull && rs.rightFull) {
|
if (rs.leftFull && rs.rightFull) {
|
||||||
SDL_SetRenderDrawColor(renderer, 140, 210, 255, 45);
|
SDL_SetRenderDrawColor(renderer, 140, 210, 255, 45);
|
||||||
SDL_FRect fr{gridX, rowY, GRID_W, finalBlockSize};
|
SDL_FRect frL{gridX, rowY, HALF_W, finalBlockSize};
|
||||||
SDL_RenderFillRect(renderer, &fr);
|
SDL_RenderFillRect(renderer, &frL);
|
||||||
|
SDL_FRect frR{gridX + HALF_W + COOP_GAP_PX, rowY, HALF_W, finalBlockSize};
|
||||||
|
SDL_RenderFillRect(renderer, &frR);
|
||||||
} else if (rs.leftFull ^ rs.rightFull) {
|
} else if (rs.leftFull ^ rs.rightFull) {
|
||||||
SDL_SetRenderDrawColor(renderer, 90, 140, 220, 35);
|
SDL_SetRenderDrawColor(renderer, 90, 140, 220, 35);
|
||||||
float w = GRID_W * 0.5f;
|
float w = HALF_W;
|
||||||
float x = rs.leftFull ? gridX : gridX + w;
|
float x = rs.leftFull ? gridX : (gridX + HALF_W + COOP_GAP_PX);
|
||||||
SDL_FRect fr{x, rowY, w, finalBlockSize};
|
SDL_FRect fr{x, rowY, w, finalBlockSize};
|
||||||
SDL_RenderFillRect(renderer, &fr);
|
SDL_RenderFillRect(renderer, &fr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE);
|
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE);
|
||||||
|
|
||||||
|
// Trigger a brief flash exactly when cooperative lines are actually cleared:
|
||||||
|
// `completedLines` remains populated during the LineEffect, then becomes empty
|
||||||
|
// immediately after `CoopGame::clearCompletedLines()` is invoked.
|
||||||
|
const bool hasCompletedLines = game->hasCompletedLines();
|
||||||
|
if (s_lastHadCompletedLines && !hasCompletedLines) {
|
||||||
|
s_syncLine.TriggerClearFlash();
|
||||||
|
}
|
||||||
|
s_lastHadCompletedLines = hasCompletedLines;
|
||||||
|
|
||||||
|
if (synced) {
|
||||||
|
s_syncLine.SetState(SyncState::Synced);
|
||||||
|
} else if (leftReady) {
|
||||||
|
s_syncLine.SetState(SyncState::LeftReady);
|
||||||
|
} else if (rightReady) {
|
||||||
|
s_syncLine.SetState(SyncState::RightReady);
|
||||||
|
} else {
|
||||||
|
s_syncLine.SetState(SyncState::Idle);
|
||||||
|
}
|
||||||
|
|
||||||
// Hard-drop impact shake (match classic feel)
|
// Hard-drop impact shake (match classic feel)
|
||||||
float impactStrength = 0.0f;
|
float impactStrength = 0.0f;
|
||||||
float impactEased = 0.0f;
|
float impactEased = 0.0f;
|
||||||
@ -2243,7 +2299,7 @@ void GameRenderer::renderCoopPlayingState(
|
|||||||
for (int x = 0; x < CoopGame::COLS; ++x) {
|
for (int x = 0; x < CoopGame::COLS; ++x) {
|
||||||
const auto& cell = board[y * CoopGame::COLS + x];
|
const auto& cell = board[y * CoopGame::COLS + x];
|
||||||
if (!cell.occupied || cell.value <= 0) continue;
|
if (!cell.occupied || cell.value <= 0) continue;
|
||||||
float px = gridX + x * finalBlockSize;
|
float px = cellX(x);
|
||||||
float py = gridY + y * finalBlockSize + dropOffset;
|
float py = gridY + y * finalBlockSize + dropOffset;
|
||||||
|
|
||||||
const int cellIdx = y * CoopGame::COLS + x;
|
const int cellIdx = y * CoopGame::COLS + x;
|
||||||
@ -2398,7 +2454,7 @@ void GameRenderer::renderCoopPlayingState(
|
|||||||
// cases like vertical I where some blocks are already visible at spawn.
|
// cases like vertical I where some blocks are already visible at spawn.
|
||||||
const bool pinToFirstVisibleRow = (livePiece.y + maxCy) < 0;
|
const bool pinToFirstVisibleRow = (livePiece.y + maxCy) < 0;
|
||||||
|
|
||||||
const float baseX = gridX + static_cast<float>(livePiece.x) * sf.tileSize + offsets.first;
|
const float baseX = cellX(livePiece.x) + offsets.first;
|
||||||
float baseY = 0.0f;
|
float baseY = 0.0f;
|
||||||
if (pinToFirstVisibleRow) {
|
if (pinToFirstVisibleRow) {
|
||||||
// Keep the piece visible (topmost filled cell at row 0), but also
|
// Keep the piece visible (topmost filled cell at row 0), but also
|
||||||
@ -2439,7 +2495,7 @@ void GameRenderer::renderCoopPlayingState(
|
|||||||
int pxIdx = p.x + cx;
|
int pxIdx = p.x + cx;
|
||||||
int pyIdx = p.y + cy;
|
int pyIdx = p.y + cy;
|
||||||
if (pyIdx < 0) continue; // don't draw parts above the visible grid
|
if (pyIdx < 0) continue; // don't draw parts above the visible grid
|
||||||
float px = gridX + (float)pxIdx * finalBlockSize + offsets.first;
|
float px = cellX(pxIdx) + offsets.first;
|
||||||
float py = gridY + (float)pyIdx * finalBlockSize + offsets.second;
|
float py = gridY + (float)pyIdx * finalBlockSize + offsets.second;
|
||||||
if (isGhost) {
|
if (isGhost) {
|
||||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||||
@ -2505,15 +2561,19 @@ void GameRenderer::renderCoopPlayingState(
|
|||||||
|
|
||||||
// Draw line clearing effects above pieces (matches single-player)
|
// Draw line clearing effects above pieces (matches single-player)
|
||||||
if (lineEffect && lineEffect->isActive()) {
|
if (lineEffect && lineEffect->isActive()) {
|
||||||
lineEffect->render(renderer, blocksTex, static_cast<int>(gridX), static_cast<int>(gridY), static_cast<int>(finalBlockSize));
|
lineEffect->render(renderer, blocksTex, static_cast<int>(gridX), static_cast<int>(gridY), static_cast<int>(finalBlockSize), static_cast<int>(COOP_GAP_PX), 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render the SYNC divider last so it stays visible above effects/blocks.
|
||||||
|
s_syncLine.Render(renderer);
|
||||||
|
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE);
|
||||||
|
|
||||||
// Next panels (two)
|
// Next panels (two)
|
||||||
const float nextPanelPad = 12.0f;
|
const float nextPanelPad = 12.0f;
|
||||||
const float nextPanelW = (GRID_W * 0.5f) - finalBlockSize * 1.5f;
|
const float nextPanelW = (HALF_W) - finalBlockSize * 1.5f;
|
||||||
const float nextPanelH = NEXT_PANEL_HEIGHT - nextPanelPad * 2.0f;
|
const float nextPanelH = NEXT_PANEL_HEIGHT - nextPanelPad * 2.0f;
|
||||||
float nextLeftX = gridX + finalBlockSize;
|
float nextLeftX = gridX + finalBlockSize;
|
||||||
float nextRightX = gridX + GRID_W - finalBlockSize - nextPanelW;
|
float nextRightX = gridX + HALF_W + COOP_GAP_PX + (HALF_W - finalBlockSize - nextPanelW);
|
||||||
float nextY = contentStartY + contentOffsetY;
|
float nextY = contentStartY + contentOffsetY;
|
||||||
|
|
||||||
auto drawNextPanel = [&](float panelX, float panelY, const CoopGame::Piece& piece) {
|
auto drawNextPanel = [&](float panelX, float panelY, const CoopGame::Piece& piece) {
|
||||||
|
|||||||
79
src/graphics/renderers/SyncLineRenderer.cpp
Normal file
79
src/graphics/renderers/SyncLineRenderer.cpp
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
#include "SyncLineRenderer.h"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
SyncLineRenderer::SyncLineRenderer()
|
||||||
|
: m_state(SyncState::Idle),
|
||||||
|
m_flashTimer(0.0f),
|
||||||
|
m_time(0.0f) {}
|
||||||
|
|
||||||
|
void SyncLineRenderer::SetRect(const SDL_FRect& rect) {
|
||||||
|
m_rect = rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SyncLineRenderer::SetState(SyncState state) {
|
||||||
|
if (state != SyncState::ClearFlash) {
|
||||||
|
m_state = state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SyncLineRenderer::TriggerClearFlash() {
|
||||||
|
m_state = SyncState::ClearFlash;
|
||||||
|
m_flashTimer = FLASH_DURATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SyncLineRenderer::Update(float deltaTime) {
|
||||||
|
m_time += deltaTime;
|
||||||
|
|
||||||
|
if (m_state == SyncState::ClearFlash) {
|
||||||
|
m_flashTimer -= deltaTime;
|
||||||
|
if (m_flashTimer <= 0.0f) {
|
||||||
|
m_state = SyncState::Idle;
|
||||||
|
m_flashTimer = 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Color SyncLineRenderer::GetBaseColor() const {
|
||||||
|
switch (m_state) {
|
||||||
|
case SyncState::LeftReady:
|
||||||
|
case SyncState::RightReady:
|
||||||
|
return SDL_Color{255, 220, 100, 235};
|
||||||
|
|
||||||
|
case SyncState::Synced:
|
||||||
|
return SDL_Color{100, 255, 120, 240};
|
||||||
|
|
||||||
|
case SyncState::ClearFlash:
|
||||||
|
return SDL_Color{255, 255, 255, 255};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return SDL_Color{80, 180, 255, 235};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SyncLineRenderer::Render(SDL_Renderer* renderer) {
|
||||||
|
if (!renderer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||||
|
|
||||||
|
SDL_Color color = GetBaseColor();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
|
||||||
|
SDL_RenderFillRect(renderer, &m_rect);
|
||||||
|
|
||||||
|
if (m_state == SyncState::ClearFlash) {
|
||||||
|
SDL_FRect glow = m_rect;
|
||||||
|
glow.x -= 3;
|
||||||
|
glow.w += 6;
|
||||||
|
|
||||||
|
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 180);
|
||||||
|
SDL_RenderFillRect(renderer, &glow);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/graphics/renderers/SyncLineRenderer.h
Normal file
33
src/graphics/renderers/SyncLineRenderer.h
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
enum class SyncState {
|
||||||
|
Idle,
|
||||||
|
LeftReady,
|
||||||
|
RightReady,
|
||||||
|
Synced,
|
||||||
|
ClearFlash
|
||||||
|
};
|
||||||
|
|
||||||
|
class SyncLineRenderer {
|
||||||
|
public:
|
||||||
|
SyncLineRenderer();
|
||||||
|
|
||||||
|
void SetRect(const SDL_FRect& rect);
|
||||||
|
void SetState(SyncState state);
|
||||||
|
void TriggerClearFlash();
|
||||||
|
|
||||||
|
void Update(float deltaTime);
|
||||||
|
void Render(SDL_Renderer* renderer);
|
||||||
|
|
||||||
|
private:
|
||||||
|
SDL_FRect m_rect{};
|
||||||
|
SyncState m_state;
|
||||||
|
|
||||||
|
float m_flashTimer;
|
||||||
|
float m_time;
|
||||||
|
|
||||||
|
static constexpr float FLASH_DURATION = 0.15f;
|
||||||
|
|
||||||
|
SDL_Color GetBaseColor() const;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user