diff --git a/src/graphics/renderers/GameRenderer.cpp b/src/graphics/renderers/GameRenderer.cpp index 1869064..09a82ac 100644 --- a/src/graphics/renderers/GameRenderer.cpp +++ b/src/graphics/renderers/GameRenderer.cpp @@ -2282,22 +2282,10 @@ void GameRenderer::renderCoopPlayingState( sf.seq = seq; sf.piece = game->current(side); sf.tileSize = finalBlockSize; - // Target to first visible row (row 0) + // Note: targetX/targetY are recomputed during drawing using the live + // current piece so movement/rotation during the fade stays correct. sf.targetX = gridX + static_cast(sf.piece.x) * finalBlockSize; - // IMPORTANT: In classic mode, pieces can spawn with their first filled - // cell not at cy=0 within the 4x4. To avoid appearing one row too low - // (and then jumping up), align the topmost filled cell to row 0. - int minCy = 4; - for (int cy = 0; cy < 4; ++cy) { - for (int cx = 0; cx < 4; ++cx) { - if (!CoopGame::cellFilled(sf.piece, cx, cy)) continue; - minCy = std::min(minCy, cy); - } - } - if (minCy == 4) { - minCy = 0; - } - sf.targetY = gridY - static_cast(minCy) * finalBlockSize; + sf.targetY = gridY; } else { // Reuse exact horizontal smoothing from single-player constexpr float HORIZONTAL_SMOOTH_MS = 55.0f; @@ -2373,24 +2361,54 @@ void GameRenderer::renderCoopPlayingState( // Draw any active spawn fades (alpha ramp into first row). Draw before // the regular active pieces; while the spawn fade is active the piece's // real position is above the grid and will not be drawn by drawPiece. - auto drawSpawnFadeIfActive = [&](SpawnFadeState &sf) { + auto drawSpawnFadeIfActive = [&](SpawnFadeState &sf, CoopGame::PlayerSide side) { if (!sf.active) return; - Uint32 now = SDL_GetTicks(); - float elapsed = static_cast(now - sf.startTick); + + // If the piece has already changed, stop the fade. + const uint64_t currentSeq = game->currentPieceSequence(side); + if (sf.seq != currentSeq) { + sf.active = false; + return; + } + + const CoopGame::Piece& livePiece = game->current(side); + float elapsed = static_cast(nowTicks - sf.startTick); float t = sf.durationMs <= 0.0f ? 1.0f : std::clamp(elapsed / sf.durationMs, 0.0f, 1.0f); Uint8 alpha = static_cast(std::lround(255.0f * t)); if (blocksTex) SDL_SetTextureAlphaMod(blocksTex, alpha); - // Draw piece at target (first row) + + // Align the topmost filled cell to row 0 (first visible row), like classic. + int minCy = 4; for (int cy = 0; cy < 4; ++cy) { for (int cx = 0; cx < 4; ++cx) { - if (!CoopGame::cellFilled(sf.piece, cx, cy)) continue; + if (!CoopGame::cellFilled(livePiece, cx, cy)) continue; + minCy = std::min(minCy, cy); + } + } + if (minCy == 4) { + minCy = 0; + } + sf.targetX = gridX + static_cast(livePiece.x) * sf.tileSize; + sf.targetY = gridY - static_cast(minCy) * sf.tileSize; + + // Draw the live piece at the fade target. + for (int cy = 0; cy < 4; ++cy) { + for (int cx = 0; cx < 4; ++cx) { + if (!CoopGame::cellFilled(livePiece, cx, cy)) continue; float px = sf.targetX + static_cast(cx) * sf.tileSize; float py = sf.targetY + static_cast(cy) * sf.tileSize; - drawBlockTexturePublic(renderer, blocksTex, px, py, sf.tileSize, sf.piece.type); + drawBlockTexturePublic(renderer, blocksTex, px, py, sf.tileSize, livePiece.type); } } if (blocksTex) SDL_SetTextureAlphaMod(blocksTex, 255); - if (t >= 1.0f) sf.active = false; + + // Critical: only deactivate once the real piece is actually drawable. + // Otherwise (notably for I spawning at y=-2) there can be a brief gap where + // the fade ends but the real piece is still fully above the visible grid. + const bool pieceHasAnyVisibleCell = (livePiece.y + minCy) >= 0; + if (t >= 1.0f && pieceHasAnyVisibleCell) { + sf.active = false; + } }; auto drawPiece = [&](const CoopGame::Piece& p, const std::pair& offsets, bool isGhost) { @@ -2419,8 +2437,8 @@ void GameRenderer::renderCoopPlayingState( const auto leftOffsets = computeOffsets(CoopGame::PlayerSide::Left, s_leftSmooth); const auto rightOffsets = computeOffsets(CoopGame::PlayerSide::Right, s_rightSmooth); // Draw transient spawn fades (if active) into the first visible row - drawSpawnFadeIfActive(s_leftSpawnFade); - drawSpawnFadeIfActive(s_rightSpawnFade); + drawSpawnFadeIfActive(s_leftSpawnFade, CoopGame::PlayerSide::Left); + drawSpawnFadeIfActive(s_rightSpawnFade, CoopGame::PlayerSide::Right); // Draw classic-style ghost pieces (landing position), grid-aligned. // This intentionally does NOT use smoothing offsets.