diff --git a/src/graphics/renderers/GameRenderer.cpp b/src/graphics/renderers/GameRenderer.cpp index d882e45..35b83a8 100644 --- a/src/graphics/renderers/GameRenderer.cpp +++ b/src/graphics/renderers/GameRenderer.cpp @@ -51,6 +51,87 @@ std::vector s_sparkles; float s_sparkleSpawnAcc = 0.0f; } +struct TransportEffectState { + bool active = false; + Uint32 startTick = 0; + float durationMs = 600.0f; + Game::Piece piece; + float startX = 0.0f; // pixel origin of piece local (0,0) + float startY = 0.0f; + float targetX = 0.0f; + float targetY = 0.0f; + float tileSize = 24.0f; +}; + +static TransportEffectState s_transport; + +static float smoothstep(float t) { + t = std::clamp(t, 0.0f, 1.0f); + return t * t * (3.0f - 2.0f * t); +} + +void GameRenderer::startTransportEffect(const Game::Piece& piece, float startX, float startY, float targetX, float targetY, float tileSize, float durationSeconds) { + s_transport.active = true; + s_transport.startTick = SDL_GetTicks(); + s_transport.durationMs = std::max(8.0f, durationSeconds * 1000.0f); + s_transport.piece = piece; + s_transport.startX = startX; + s_transport.startY = startY; + s_transport.targetX = targetX; + s_transport.targetY = targetY; + s_transport.tileSize = tileSize; +} + +// Draw the ongoing transport effect; called every frame from renderPlayingState +static void updateAndDrawTransport(SDL_Renderer* renderer, SDL_Texture* blocksTex) { + if (!s_transport.active) return; + Uint32 now = SDL_GetTicks(); + float elapsed = static_cast(now - s_transport.startTick); + float t = elapsed / s_transport.durationMs; + float eased = smoothstep(std::clamp(t, 0.0f, 1.0f)); + + // Draw trailing particles / beam along the path + const int trailCount = 10; + for (int i = 0; i < trailCount; ++i) { + float p = eased - (static_cast(i) * 0.04f); + if (p <= 0.0f) continue; + p = std::clamp(p, 0.0f, 1.0f); + float px = std::lerp(s_transport.startX, s_transport.targetX, p); + float py = std::lerp(s_transport.startY, s_transport.targetY, p); + + // jitter for sci-fi shimmer + float jitter = static_cast(std::sin((now + i * 37) * 0.01f)) * (s_transport.tileSize * 0.06f); + SDL_FRect r{px + jitter, py - s_transport.tileSize * 0.06f, s_transport.tileSize * 0.18f, s_transport.tileSize * 0.18f}; + SDL_SetTextureColorMod(blocksTex, 255, 255, 255); + SDL_SetTextureAlphaMod(blocksTex, static_cast(std::clamp(255.0f * (0.5f * (1.0f - p)), 0.0f, 255.0f))); + GameRenderer::drawBlockTexturePublic(renderer, blocksTex, r.x, r.y, r.w, static_cast(s_transport.piece.type)); + } + // reset texture alpha to full + if (blocksTex) SDL_SetTextureAlphaMod(blocksTex, 255); + + // Draw the piece itself at interpolated position between start and target + float curX = std::lerp(s_transport.startX, s_transport.targetX, eased); + float curY = std::lerp(s_transport.startY, s_transport.targetY, eased); + + // Render all filled cells of the piece at pixel coordinates + for (int cy = 0; cy < 4; ++cy) { + for (int cx = 0; cx < 4; ++cx) { + if (!Game::cellFilled(s_transport.piece, cx, cy)) continue; + float bx = curX + static_cast(cx) * s_transport.tileSize; + float by = curY + static_cast(cy) * s_transport.tileSize; + // pulse alpha while moving + float pulse = 0.6f + 0.4f * std::sin((now - s_transport.startTick) * 0.02f); + SDL_SetTextureAlphaMod(blocksTex, static_cast(std::clamp(255.0f * pulse * (1.0f - t), 0.0f, 255.0f))); + GameRenderer::drawBlockTexturePublic(renderer, blocksTex, bx, by, s_transport.tileSize, s_transport.piece.type); + } + } + if (blocksTex) SDL_SetTextureAlphaMod(blocksTex, 255); + + if (t >= 1.0f) { + s_transport.active = false; + } +} + // Color constants (copied from main.cpp) static const SDL_Color COLORS[] = { {0, 0, 0, 255}, // 0: BLACK (empty) @@ -119,6 +200,11 @@ void GameRenderer::drawPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, con } } +void GameRenderer::drawBlockTexturePublic(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType) { + // Forward to the private helper + drawBlockTexture(renderer, blocksTex, x, y, size, blockType); +} + void GameRenderer::drawSmallPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, PieceType pieceType, float x, float y, float tileSize) { if (pieceType >= PIECE_COUNT) return; @@ -129,10 +215,29 @@ void GameRenderer::drawSmallPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex previewPiece.x = 0; previewPiece.y = 0; - // Center the piece in the preview area - float offsetX = 0, offsetY = 0; - if (pieceType == 0) { offsetX = tileSize * 0.5f; } // I-piece centering (assuming I = 0) - else if (pieceType == 1) { offsetX = tileSize * 0.5f; } // O-piece centering (assuming O = 1) + // Determine occupied bounds within 4x4 and center inside the 4x4 preview area + int minCx = 4, maxCx = -1, minCy = 4, maxCy = -1; + for (int cy = 0; cy < 4; ++cy) { + for (int cx = 0; cx < 4; ++cx) { + if (Game::cellFilled(previewPiece, cx, cy)) { + minCx = std::min(minCx, cx); + maxCx = std::max(maxCx, cx); + minCy = std::min(minCy, cy); + maxCy = std::max(maxCy, cy); + } + } + } + if (maxCx < minCx) { minCx = 0; maxCx = 0; } + if (maxCy < minCy) { minCy = 0; maxCy = 0; } + + float areaW = 4.0f * tileSize; + float areaH = 4.0f * tileSize; + float pieceW = static_cast(maxCx - minCx + 1) * tileSize; + float pieceH = static_cast(maxCy - minCy + 1) * tileSize; + float offsetX = (areaW - pieceW) * 0.5f - static_cast(minCx) * tileSize; + float offsetY = (areaH - pieceH) * 0.5f - static_cast(minCy) * tileSize; + offsetX = std::round(offsetX); + offsetY = std::round(offsetY); // Use semi-transparent alpha for preview blocks Uint8 previewAlpha = 180; @@ -145,7 +250,7 @@ void GameRenderer::drawSmallPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex if (Game::cellFilled(previewPiece, cx, cy)) { float px = x + offsetX + cx * tileSize; float py = y + offsetY + cy * tileSize; - drawBlockTexture(renderer, blocksTex, px, py, tileSize, pieceType); + GameRenderer::drawBlockTexturePublic(renderer, blocksTex, px, py, tileSize, pieceType); } } } @@ -228,14 +333,22 @@ void GameRenderer::renderNextPanel( const float previewTop = panelY + std::min(labelReserve, panelH * 0.45f); const float previewBottom = panelY + panelH - tileSize * 0.25f; const float previewCenterY = (previewTop + previewBottom) * 0.5f; - const float previewCenterX = panelX + panelW * 0.5f; + const float previewCenterX = std::round(panelX + panelW * 0.5f); const float pieceWidth = static_cast(maxCx - minCx + 1) * tileSize; const float pieceHeight = static_cast(maxCy - minCy + 1) * tileSize; - // Nudge preview slightly to the right so it aligns with the main grid's visual columns - const float previewNudgeX = tileSize * 0.5f; - const float startX = previewCenterX - pieceWidth * 0.5f - static_cast(minCx) * tileSize + previewNudgeX; - const float startY = previewCenterY - pieceHeight * 0.5f - static_cast(minCy) * tileSize; + // Center piece so its local cells fall exactly on grid-aligned pixel columns + float startX = previewCenterX - pieceWidth * 0.5f - static_cast(minCx) * tileSize; + float startY = previewCenterY - pieceHeight * 0.5f - static_cast(minCy) * tileSize; + // Snap horizontal position to the playfield's tile grid so preview cells align exactly + // with the main grid columns. `panelX` was computed as `gridX + tileSize` in caller, + // so derive grid origin as `panelX - tileSize`. + float gridOriginX = panelX - tileSize; + float rel = startX - gridOriginX; + float nearestTile = std::round(rel / tileSize); + startX = gridOriginX + nearestTile * tileSize; + // Round Y to pixel to avoid subpixel artifacts + startY = std::round(startY); for (int cy = 0; cy < 4; ++cy) { for (int cx = 0; cx < 4; ++cx) { @@ -244,7 +357,7 @@ void GameRenderer::renderNextPanel( } const float px = startX + static_cast(cx) * tileSize; const float py = startY + static_cast(cy) * tileSize; - drawBlockTexture(renderer, blocksTex, px, py, tileSize, nextPiece.type); + GameRenderer::drawBlockTexturePublic(renderer, blocksTex, px, py, tileSize, nextPiece.type); } } } @@ -328,9 +441,10 @@ void GameRenderer::renderPlayingState( const float statsH = GRID_H; // Next piece preview position - const float NEXT_PANEL_WIDTH = finalBlockSize * 6.0f; // +1 cell padding on each horizontal side + // Make NEXT panel span the inner area of the grid with a 1-cell margin on both sides + const float NEXT_PANEL_WIDTH = GRID_W - finalBlockSize * 2.0f; // leave 1 cell on left and right const float NEXT_PANEL_HEIGHT = finalBlockSize * 3.0f; - const float NEXT_PANEL_X = gridX + (GRID_W - NEXT_PANEL_WIDTH) * 0.5f; + const float NEXT_PANEL_X = gridX + finalBlockSize; // align panel so there's exactly one cell margin const float NEXT_PANEL_Y = gridY - NEXT_PANEL_HEIGHT - 2.0f; // nudge up ~2px // Handle line clearing effects @@ -548,13 +662,14 @@ void GameRenderer::renderPlayingState( renderNextPanel(renderer, pixelFont, blocksTex, game->next(), NEXT_PANEL_X, NEXT_PANEL_Y, NEXT_PANEL_WIDTH, NEXT_PANEL_HEIGHT, finalBlockSize); - // Draw a thin horizontal connector at the bottom of the NEXT panel so it visually - // connects to the top of the grid (appears as a single continuous frame). + // Draw a small filled connector to visually merge NEXT panel and grid border SDL_SetRenderDrawColor(renderer, 60, 80, 160, 255); // same as grid border float connectorY = NEXT_PANEL_Y + NEXT_PANEL_HEIGHT; // bottom of next panel (near grid top) - // Draw a 2px-high filled connector to overwrite any existing grid border pixels SDL_FRect connRect{ NEXT_PANEL_X, connectorY - 1.0f, NEXT_PANEL_WIDTH, 2.0f }; SDL_RenderFillRect(renderer, &connRect); + + // Draw transport effect if active (renders the moving piece and trail) + updateAndDrawTransport(renderer, blocksTex); // Precompute row drop offsets (line collapse effect) std::array rowDropOffsets{}; diff --git a/src/graphics/renderers/GameRenderer.h b/src/graphics/renderers/GameRenderer.h index 6d96a6a..cadccb5 100644 --- a/src/graphics/renderers/GameRenderer.h +++ b/src/graphics/renderers/GameRenderer.h @@ -48,12 +48,22 @@ public: int selectedButton ); + // Public wrapper that forwards to the private tile-drawing helper. Use this if + // calling from non-member helper functions (e.g. visual effects) that cannot + // access private class members. + static void drawBlockTexturePublic(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType); + private: // Helper functions for drawing game elements static void drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType); static void drawPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, const Game::Piece& piece, float ox, float oy, float tileSize, bool isGhost = false, float pixelOffsetX = 0.0f, float pixelOffsetY = 0.0f); static void drawSmallPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, PieceType pieceType, float x, float y, float tileSize); static void renderNextPanel(SDL_Renderer* renderer, FontAtlas* pixelFont, SDL_Texture* blocksTex, const Game::Piece& nextPiece, float panelX, float panelY, float panelW, float panelH, float tileSize); + // Transport/teleport visual effect: start a sci-fi "transport" animation moving + // a visual copy of `piece` from screen pixel origin (startX,startY) to + // target pixel origin (targetX,targetY). `tileSize` should be the same cell size + // used for the grid. Duration is seconds. + static void startTransportEffect(const Game::Piece& piece, float startX, float startY, float targetX, float targetY, float tileSize, float durationSeconds = 0.6f); // Helper function for drawing rectangles static void drawRect(SDL_Renderer* renderer, float x, float y, float w, float h, SDL_Color c);