From 06aa63f548d22896fe33374a92ddc575acb3f108 Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Sun, 21 Dec 2025 17:52:07 +0100 Subject: [PATCH] fixed score display --- src/gameplay/coop/CoopGame.cpp | 39 ++++++- src/gameplay/coop/CoopGame.h | 17 ++- src/graphics/renderers/GameRenderer.cpp | 136 +++++++++++++++++++++--- 3 files changed, 175 insertions(+), 17 deletions(-) diff --git a/src/gameplay/coop/CoopGame.cpp b/src/gameplay/coop/CoopGame.cpp index 0179fce..0d8abf8 100644 --- a/src/gameplay/coop/CoopGame.cpp +++ b/src/gameplay/coop/CoopGame.cpp @@ -55,6 +55,7 @@ void CoopGame::reset(int startLevel_) { gravityMs = gravityMsForLevel(_level); gameOver = false; pieceSequence = 0; + elapsedMs = 0.0; left = PlayerState{}; right = PlayerState{ PlayerSide::Right }; @@ -67,6 +68,13 @@ void CoopGame::reset(int startLevel_) { ps.fallAcc = 0.0; ps.lockAcc = 0.0; ps.pieceSeq = 0; + ps.score = 0; + ps.lines = 0; + ps.level = startLevel_; + ps.tetrisesMade = 0; + ps.currentCombo = 0; + ps.maxCombo = 0; + ps.comboCount = 0; ps.bag.clear(); ps.next.type = PIECE_COUNT; refillBag(ps); @@ -169,6 +177,7 @@ void CoopGame::hardDrop(PlayerSide side) { } if (moved) { _score += dropped; // 1 point per cell, matches single-player hard drop + ps.score += dropped; hardDropShakeTimerMs = HARD_DROP_SHAKE_DURATION_MS; hardDropFxId++; } @@ -198,6 +207,8 @@ void CoopGame::holdCurrent(PlayerSide side) { void CoopGame::tickGravity(double frameMs) { if (gameOver) return; + elapsedMs += frameMs; + auto stepPlayer = [&](PlayerState& ps) { if (ps.toppedOut) return; double step = ps.softDropping ? std::max(5.0, gravityMs / 5.0) : gravityMs; @@ -214,6 +225,7 @@ void CoopGame::tickGravity(double frameMs) { // Award soft drop points when actively holding down if (ps.softDropping) { _score += 1; + ps.score += 1; } ps.lockAcc = 0.0; } @@ -349,13 +361,14 @@ void CoopGame::lock(PlayerState& ps) { findCompletedLines(); if (!completedLines.empty()) { int cleared = static_cast(completedLines.size()); - applyLineClearRewards(cleared); + applyLineClearRewards(ps, cleared); // Notify audio layer if present (matches single-player behavior) if (soundCallback) soundCallback(cleared); // Leave `completedLines` populated; `clearCompletedLines()` will be // invoked by the state when the LineEffect finishes. } else { _currentCombo = 0; + ps.currentCombo = 0; } spawn(ps); } @@ -379,7 +392,7 @@ void CoopGame::findCompletedLines() { } } -void CoopGame::applyLineClearRewards(int cleared) { +void CoopGame::applyLineClearRewards(PlayerState& creditPlayer, int cleared) { if (cleared <= 0) return; // Base NES scoring scaled by shared level (level 0 => 1x multiplier) @@ -392,8 +405,10 @@ void CoopGame::applyLineClearRewards(int cleared) { default: base = 0; break; } _score += base * (_level + 1); + creditPlayer.score += base * (creditPlayer.level + 1); _lines += cleared; + creditPlayer.lines += cleared; _currentCombo += 1; if (_currentCombo > _maxCombo) _maxCombo = _currentCombo; @@ -404,6 +419,15 @@ void CoopGame::applyLineClearRewards(int cleared) { _tetrisesMade += 1; } + creditPlayer.currentCombo += 1; + if (creditPlayer.currentCombo > creditPlayer.maxCombo) creditPlayer.maxCombo = creditPlayer.currentCombo; + if (cleared > 1) { + creditPlayer.comboCount += 1; + } + if (cleared == 4) { + creditPlayer.tetrisesMade += 1; + } + // Level progression mirrors single-player: threshold after (startLevel+1)*10 then every 10 lines int targetLevel = startLevel; int firstThreshold = (startLevel + 1) * 10; @@ -414,6 +438,17 @@ void CoopGame::applyLineClearRewards(int cleared) { _level = targetLevel; gravityMs = gravityMsForLevel(_level); } + + // Per-player level progression mirrors the shared rules but is driven by + // that player's credited line clears. + { + int pTargetLevel = startLevel; + int pFirstThreshold = (startLevel + 1) * 10; + if (creditPlayer.lines >= pFirstThreshold) { + pTargetLevel = startLevel + 1 + (creditPlayer.lines - pFirstThreshold) / 10; + } + creditPlayer.level = std::max(creditPlayer.level, pTargetLevel); + } } void CoopGame::clearLinesInternal() { diff --git a/src/gameplay/coop/CoopGame.h b/src/gameplay/coop/CoopGame.h index 6b50eb7..331bed5 100644 --- a/src/gameplay/coop/CoopGame.h +++ b/src/gameplay/coop/CoopGame.h @@ -44,6 +44,13 @@ public: bool toppedOut{false}; double fallAcc{0.0}; double lockAcc{0.0}; + int score{0}; + int lines{0}; + int level{0}; + int tetrisesMade{0}; + int currentCombo{0}; + int maxCombo{0}; + int comboCount{0}; std::vector bag{}; // 7-bag queue std::mt19937 rng{ std::random_device{}() }; }; @@ -71,11 +78,17 @@ public: bool canHold(PlayerSide s) const { return player(s).canHold; } bool isGameOver() const { return gameOver; } int score() const { return _score; } + int score(PlayerSide s) const { return player(s).score; } int lines() const { return _lines; } + int lines(PlayerSide s) const { return player(s).lines; } int level() const { return _level; } + int level(PlayerSide s) const { return player(s).level; } int comboCount() const { return _comboCount; } int maxCombo() const { return _maxCombo; } int tetrisesMade() const { return _tetrisesMade; } + int elapsed() const { return static_cast(elapsedMs / 1000.0); } + int elapsed(PlayerSide) const { return elapsed(); } + int startLevelBase() const { return startLevel; } double getGravityMs() const { return gravityMs; } double getFallAccumulator(PlayerSide s) const { return player(s).fallAcc; } bool isSoftDropping(PlayerSide s) const { return player(s).softDropping; } @@ -113,6 +126,8 @@ private: double gravityGlobalMultiplier{1.0}; bool gameOver{false}; + double elapsedMs{0.0}; + std::vector completedLines; // Impact FX @@ -136,7 +151,7 @@ private: void findCompletedLines(); void clearLinesInternal(); void updateRowStates(); - void applyLineClearRewards(int cleared); + void applyLineClearRewards(PlayerState& creditPlayer, int cleared); double gravityMsForLevel(int level) const; int columnMin(PlayerSide s) const { return s == PlayerSide::Left ? 0 : 10; } int columnMax(PlayerSide s) const { return s == PlayerSide::Left ? 9 : 19; } diff --git a/src/graphics/renderers/GameRenderer.cpp b/src/graphics/renderers/GameRenderer.cpp index ed4c304..1869064 100644 --- a/src/graphics/renderers/GameRenderer.cpp +++ b/src/graphics/renderers/GameRenderer.cpp @@ -1369,6 +1369,7 @@ void GameRenderer::renderPlayingState( double sp_gravityMs = game->getGravityMs(); double sp_fallAcc = game->getFallAccumulator(); int sp_soft = game->isSoftDropping() ? 1 : 0; + /* SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "SP OFFSETS: seq=%llu visX=%.3f targX=%.3f offX=%.2f offY=%.2f gravMs=%.2f fallAcc=%.2f soft=%d", (unsigned long long)s_activePieceSmooth.sequence, s_activePieceSmooth.visualX, @@ -1379,6 +1380,7 @@ void GameRenderer::renderPlayingState( sp_fallAcc, sp_soft ); + */ } // Draw ghost piece (where current piece will land) @@ -1910,19 +1912,13 @@ void GameRenderer::renderCoopPlayingState( const float gridX = layoutStartX + PANEL_WIDTH + PANEL_SPACING + contentOffsetX; const float gridY = contentStartY + NEXT_PANEL_HEIGHT + contentOffsetY; + const float rightPanelX = gridX + GRID_W + PANEL_SPACING; + const float statsY = gridY; const float statsW = PANEL_WIDTH; const float statsH = GRID_H; - // Shared score panel (reuse existing art) - SDL_FRect scorePanelBg{ statsX - 20.0f, gridY - 26.0f, statsW + 40.0f, GRID_H + 52.0f }; - if (statisticsPanelTex) { - SDL_RenderTexture(renderer, statisticsPanelTex, nullptr, &scorePanelBg); - } else if (scorePanelTex) { - SDL_RenderTexture(renderer, scorePanelTex, nullptr, &scorePanelBg); - } else { - drawRectWithOffset(scorePanelBg.x - contentOffsetX, scorePanelBg.y - contentOffsetY, scorePanelBg.w, scorePanelBg.h, SDL_Color{12,18,32,205}); - } + // (Score panels are drawn per-player below using scorePanelTex and classic sizing.) // Handle line clearing effects (defer to LineEffect like single-player) if (game->hasCompletedLines() && lineEffect && !lineEffect->isActive()) { @@ -2357,7 +2353,8 @@ void GameRenderer::renderCoopPlayingState( double gMsDbg = game->getGravityMs(); double accDbg = game->getFallAccumulator(side); int softDbg = game->isSoftDropping(side) ? 1 : 0; - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "COOP %s OFFSETS: seq=%llu visX=%.3f targX=%.3f offX=%.2f offY=%.2f gravMs=%.2f fallAcc=%.2f soft=%d", + /* + SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "COOP %s OFFSETS: seq=%llu visX=%.3f targX=%.3f offX=%.2f offY=%.2f gravMs=%.2f fallAcc=%.2f soft=%d", (side == CoopGame::PlayerSide::Left) ? "L" : "R", (unsigned long long)ss.seq, ss.visualX, @@ -2368,6 +2365,7 @@ void GameRenderer::renderCoopPlayingState( accDbg, softDbg ); + */ } return std::pair{ offsetX, offsetY }; }; @@ -2517,10 +2515,120 @@ void GameRenderer::renderCoopPlayingState( drawNextPanel(nextLeftX, nextY, game->next(CoopGame::PlayerSide::Left)); drawNextPanel(nextRightX, nextY, game->next(CoopGame::PlayerSide::Right)); - // Simple shared score text - char buf[128]; - std::snprintf(buf, sizeof(buf), "SCORE %d LINES %d LEVEL %d", game->score(), game->lines(), game->level()); - pixelFont->draw(renderer, gridX + GRID_W * 0.5f - 140.0f, gridY + GRID_H + 24.0f, buf, 1.2f, SDL_Color{220, 230, 255, 255}); + // Per-player scoreboards (left and right) + auto drawPlayerScoreboard = [&](CoopGame::PlayerSide side, float columnLeftX, float columnRightX, const char* title) { + const SDL_Color labelColor{255, 220, 0, 255}; + const SDL_Color valueColor{255, 255, 255, 255}; + const SDL_Color nextColor{80, 255, 120, 255}; + + // Match classic vertical placement feel + const float contentTopOffset = 0.0f; + const float contentBottomOffset = 290.0f; + const float contentPad = 36.0f; + float scoreContentH = (contentBottomOffset - contentTopOffset) + contentPad; + float baseY = gridY + (GRID_H - scoreContentH) * 0.5f; + + const float statsPanelPadLeft = 40.0f; + const float statsPanelPadRight = 34.0f; + const float statsPanelPadY = 28.0f; + + const float textX = columnLeftX + statsPanelPadLeft; + + char scoreStr[32]; + std::snprintf(scoreStr, sizeof(scoreStr), "%d", game->score(side)); + + char linesStr[16]; + std::snprintf(linesStr, sizeof(linesStr), "%03d", game->lines(side)); + + char levelStr[16]; + std::snprintf(levelStr, sizeof(levelStr), "%02d", game->level(side)); + + // Next level progression (per-player lines) + int startLv = game->startLevelBase(); + int linesDone = game->lines(side); + int firstThreshold = (startLv + 1) * 10; + int nextThreshold = 0; + if (linesDone < firstThreshold) { + nextThreshold = firstThreshold; + } else { + int blocksPast = linesDone - firstThreshold; + nextThreshold = firstThreshold + ((blocksPast / 10) + 1) * 10; + } + int linesForNext = std::max(0, nextThreshold - linesDone); + char nextStr[32]; + std::snprintf(nextStr, sizeof(nextStr), "%d LINES", linesForNext); + + // Time display (shared session time) + int totalSecs = game->elapsed(side); + int mins = totalSecs / 60; + int secs = totalSecs % 60; + char timeStr[16]; + std::snprintf(timeStr, sizeof(timeStr), "%02d:%02d", mins, secs); + + struct StatLine { + const char* text; + float offsetY; + float scale; + SDL_Color color; + }; + + // Keep offsets aligned with classic spacing + std::vector statLines; + statLines.reserve(12); + statLines.push_back({title, 0.0f, 0.95f, SDL_Color{200, 220, 235, 220}}); + statLines.push_back({"SCORE", 30.0f, 1.0f, labelColor}); + statLines.push_back({scoreStr, 55.0f, 0.9f, valueColor}); + statLines.push_back({"LINES", 100.0f, 1.0f, labelColor}); + statLines.push_back({linesStr, 125.0f, 0.9f, valueColor}); + statLines.push_back({"LEVEL", 170.0f, 1.0f, labelColor}); + statLines.push_back({levelStr, 195.0f, 0.9f, valueColor}); + statLines.push_back({"NEXT LVL", 230.0f, 1.0f, labelColor}); + statLines.push_back({nextStr, 255.0f, 0.9f, nextColor}); + statLines.push_back({"TIME", 295.0f, 1.0f, labelColor}); + statLines.push_back({timeStr, 320.0f, 0.9f, valueColor}); + + // Size the panel like classic: measure the text block and fit the background. + float statsContentTop = std::numeric_limits::max(); + float statsContentBottom = std::numeric_limits::lowest(); + float statsContentMaxWidth = 0.0f; + for (const auto& line : statLines) { + int textW = 0; + int textH = 0; + pixelFont->measure(line.text, line.scale, textW, textH); + float y = baseY + line.offsetY; + statsContentTop = std::min(statsContentTop, y); + statsContentBottom = std::max(statsContentBottom, y + static_cast(textH)); + statsContentMaxWidth = std::max(statsContentMaxWidth, static_cast(textW)); + } + + float panelW = statsPanelPadLeft + statsContentMaxWidth + statsPanelPadRight; + float panelH = (statsContentBottom - statsContentTop) + statsPanelPadY * 2.0f; + float panelY = statsContentTop - statsPanelPadY; + // Left player is left-aligned in its column; right player is right-aligned. + float panelX = (side == CoopGame::PlayerSide::Right) ? (columnRightX - panelW) : columnLeftX; + SDL_FRect panelBg{ panelX, panelY, panelW, panelH }; + if (scorePanelTex) { + SDL_RenderTexture(renderer, scorePanelTex, nullptr, &panelBg); + } else { + SDL_SetRenderDrawColor(renderer, 12, 18, 32, 205); + SDL_RenderFillRect(renderer, &panelBg); + } + + float textDrawX = panelX + statsPanelPadLeft; + for (const auto& line : statLines) { + pixelFont->draw(renderer, textDrawX, baseY + line.offsetY, line.text, line.scale, line.color); + } + }; + + // Nudge panels toward the window edges for tighter symmetry. + const float scorePanelEdgeNudge = 20.0f; + const float leftColumnLeftX = statsX - scorePanelEdgeNudge; + const float leftColumnRightX = leftColumnLeftX + statsW; + const float rightColumnLeftX = rightPanelX; + const float rightColumnRightX = rightColumnLeftX + statsW + scorePanelEdgeNudge; + + drawPlayerScoreboard(CoopGame::PlayerSide::Left, leftColumnLeftX, leftColumnRightX, "PLAYER 1"); + drawPlayerScoreboard(CoopGame::PlayerSide::Right, rightColumnLeftX, rightColumnRightX, "PLAYER 2"); } void GameRenderer::renderExitPopup(