highscore fixes
This commit is contained in:
@ -172,6 +172,8 @@ struct TetrisApp::Impl {
|
||||
int hoveredButton = -1; // -1 = none, 0 = play, 1 = level, 2 = settings
|
||||
bool isNewHighScore = false;
|
||||
std::string playerName;
|
||||
std::string player2Name;
|
||||
int highScoreEntryIndex = 0; // 0 = entering player1, 1 = entering player2
|
||||
bool helpOverlayPausedGame = false;
|
||||
|
||||
SDL_Window* window = nullptr;
|
||||
@ -866,22 +868,54 @@ void TetrisApp::Impl::runLoop()
|
||||
}
|
||||
|
||||
if (!showHelpOverlay && state == AppState::GameOver && isNewHighScore && e.type == SDL_EVENT_TEXT_INPUT) {
|
||||
if (playerName.length() < 12) {
|
||||
playerName += e.text.text;
|
||||
// Support single-player and coop two-name entry
|
||||
if (game && game->getMode() == GameMode::Cooperate && coopGame) {
|
||||
if (highScoreEntryIndex == 0) {
|
||||
if (playerName.length() < 12) playerName += e.text.text;
|
||||
} else {
|
||||
if (player2Name.length() < 12) player2Name += e.text.text;
|
||||
}
|
||||
} else {
|
||||
if (playerName.length() < 12) playerName += e.text.text;
|
||||
}
|
||||
}
|
||||
|
||||
if (!showHelpOverlay && state == AppState::GameOver && e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
|
||||
if (isNewHighScore) {
|
||||
if (e.key.scancode == SDL_SCANCODE_BACKSPACE && !playerName.empty()) {
|
||||
playerName.pop_back();
|
||||
} else if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER) {
|
||||
if (playerName.empty()) playerName = "PLAYER";
|
||||
ensureScoresLoaded();
|
||||
scores.submit(game->score(), game->lines(), game->level(), game->elapsed(), playerName);
|
||||
Settings::instance().setPlayerName(playerName);
|
||||
isNewHighScore = false;
|
||||
SDL_StopTextInput(window);
|
||||
if (game && game->getMode() == GameMode::Cooperate && coopGame) {
|
||||
// Two-name entry flow
|
||||
if (e.key.scancode == SDL_SCANCODE_BACKSPACE) {
|
||||
if (highScoreEntryIndex == 0 && !playerName.empty()) playerName.pop_back();
|
||||
else if (highScoreEntryIndex == 1 && !player2Name.empty()) player2Name.pop_back();
|
||||
} else if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER) {
|
||||
if (highScoreEntryIndex == 0) {
|
||||
if (playerName.empty()) playerName = "P1";
|
||||
highScoreEntryIndex = 1; // move to second name
|
||||
} else {
|
||||
if (player2Name.empty()) player2Name = "P2";
|
||||
// Submit combined name
|
||||
std::string combined = playerName + " & " + player2Name;
|
||||
int leftScore = coopGame->score(CoopGame::PlayerSide::Left);
|
||||
int rightScore = coopGame->score(CoopGame::PlayerSide::Right);
|
||||
int combinedScore = leftScore + rightScore;
|
||||
ensureScoresLoaded();
|
||||
scores.submit(combinedScore, coopGame->lines(), coopGame->level(), coopGame->elapsed(), combined);
|
||||
Settings::instance().setPlayerName(playerName);
|
||||
isNewHighScore = false;
|
||||
SDL_StopTextInput(window);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (e.key.scancode == SDL_SCANCODE_BACKSPACE && !playerName.empty()) {
|
||||
playerName.pop_back();
|
||||
} else if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER) {
|
||||
if (playerName.empty()) playerName = "PLAYER";
|
||||
ensureScoresLoaded();
|
||||
scores.submit(game->score(), game->lines(), game->level(), game->elapsed(), playerName);
|
||||
Settings::instance().setPlayerName(playerName);
|
||||
isNewHighScore = false;
|
||||
SDL_StopTextInput(window);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER || e.key.scancode == SDL_SCANCODE_SPACE) {
|
||||
@ -1255,6 +1289,21 @@ void TetrisApp::Impl::runLoop()
|
||||
}
|
||||
|
||||
if (coopGame->isGameOver()) {
|
||||
// Compute combined coop stats for Game Over
|
||||
int leftScore = coopGame->score(CoopGame::PlayerSide::Left);
|
||||
int rightScore = coopGame->score(CoopGame::PlayerSide::Right);
|
||||
int combinedScore = leftScore + rightScore;
|
||||
if (combinedScore > 0) {
|
||||
isNewHighScore = true;
|
||||
playerName.clear();
|
||||
player2Name.clear();
|
||||
highScoreEntryIndex = 0;
|
||||
SDL_StartTextInput(window);
|
||||
} else {
|
||||
isNewHighScore = false;
|
||||
ensureScoresLoaded();
|
||||
scores.submit(combinedScore, coopGame->lines(), coopGame->level(), coopGame->elapsed());
|
||||
}
|
||||
state = AppState::GameOver;
|
||||
stateMgr->setState(state);
|
||||
}
|
||||
@ -1974,13 +2023,29 @@ void TetrisApp::Impl::runLoop()
|
||||
SDL_RenderFillRect(renderer, &boxRect);
|
||||
|
||||
ensureScoresLoaded();
|
||||
bool realHighScore = scores.isHighScore(game->score());
|
||||
// Choose display values based on mode (single-player vs coop)
|
||||
int displayScore = 0;
|
||||
int displayLines = 0;
|
||||
int displayLevel = 0;
|
||||
if (game && game->getMode() == GameMode::Cooperate && coopGame) {
|
||||
int leftScore = coopGame->score(CoopGame::PlayerSide::Left);
|
||||
int rightScore = coopGame->score(CoopGame::PlayerSide::Right);
|
||||
displayScore = leftScore + rightScore;
|
||||
displayLines = coopGame->lines();
|
||||
displayLevel = coopGame->level();
|
||||
} else if (game) {
|
||||
displayScore = game->score();
|
||||
displayLines = game->lines();
|
||||
displayLevel = game->level();
|
||||
}
|
||||
|
||||
bool realHighScore = scores.isHighScore(displayScore);
|
||||
const char* title = realHighScore ? "NEW HIGH SCORE!" : "GAME OVER";
|
||||
int tW=0, tH=0; pixelFont.measure(title, 2.0f, tW, tH);
|
||||
pixelFont.draw(renderer, boxX + (boxW - tW) * 0.5f + contentOffsetX, boxY + 40 + contentOffsetY, title, 2.0f, realHighScore ? SDL_Color{255, 220, 0, 255} : SDL_Color{255, 60, 60, 255});
|
||||
|
||||
char scoreStr[64];
|
||||
snprintf(scoreStr, sizeof(scoreStr), "SCORE: %d", game->score());
|
||||
snprintf(scoreStr, sizeof(scoreStr), "SCORE: %d", displayScore);
|
||||
int sW=0, sH=0; pixelFont.measure(scoreStr, 1.2f, sW, sH);
|
||||
pixelFont.draw(renderer, boxX + (boxW - sW) * 0.5f + contentOffsetX, boxY + 100 + contentOffsetY, scoreStr, 1.2f, {255, 255, 255, 255});
|
||||
|
||||
|
||||
@ -1232,13 +1232,25 @@ void ApplicationManager::setupStateHandlers() {
|
||||
// "GAME OVER" title
|
||||
font.draw(renderer.getSDLRenderer(), LOGICAL_W * 0.5f - 120, 140, "GAME OVER", 3.0f, {255, 80, 60, 255});
|
||||
|
||||
// Game stats
|
||||
// Game stats (single-player or coop combined)
|
||||
char buf[128];
|
||||
std::snprintf(buf, sizeof(buf), "SCORE %d LINES %d LEVEL %d",
|
||||
m_stateContext.game->score(),
|
||||
m_stateContext.game->lines(),
|
||||
m_stateContext.game->level());
|
||||
font.draw(renderer.getSDLRenderer(), LOGICAL_W * 0.5f - 180, 220, buf, 1.2f, {220, 220, 230, 255});
|
||||
if (m_stateContext.game && m_stateContext.game->getMode() == GameMode::Cooperate && m_stateContext.coopGame) {
|
||||
int leftScore = m_stateContext.coopGame->score(::CoopGame::PlayerSide::Left);
|
||||
int rightScore = m_stateContext.coopGame->score(::CoopGame::PlayerSide::Right);
|
||||
int total = leftScore + rightScore;
|
||||
std::snprintf(buf, sizeof(buf), "SCORE %d + %d = %d LINES %d LEVEL %d",
|
||||
leftScore,
|
||||
rightScore,
|
||||
total,
|
||||
m_stateContext.coopGame->lines(),
|
||||
m_stateContext.coopGame->level());
|
||||
} else {
|
||||
std::snprintf(buf, sizeof(buf), "SCORE %d LINES %d LEVEL %d",
|
||||
m_stateContext.game ? m_stateContext.game->score() : 0,
|
||||
m_stateContext.game ? m_stateContext.game->lines() : 0,
|
||||
m_stateContext.game ? m_stateContext.game->level() : 0);
|
||||
}
|
||||
font.draw(renderer.getSDLRenderer(), LOGICAL_W * 0.5f - 220, 220, buf, 1.2f, {220, 220, 230, 255});
|
||||
|
||||
// Instructions
|
||||
font.draw(renderer.getSDLRenderer(), LOGICAL_W * 0.5f - 120, 270, "PRESS ENTER / SPACE", 1.2f, {200, 200, 220, 255});
|
||||
|
||||
@ -408,6 +408,13 @@ void CoopGame::applyLineClearRewards(PlayerState& creditPlayer, int cleared) {
|
||||
_score += base * (_level + 1);
|
||||
creditPlayer.score += base * (creditPlayer.level + 1);
|
||||
|
||||
// Also award a trivial per-line bonus to both players so clears benefit
|
||||
// both participants equally (as requested).
|
||||
if (cleared > 0) {
|
||||
left.score += cleared;
|
||||
right.score += cleared;
|
||||
}
|
||||
|
||||
_lines += cleared;
|
||||
creditPlayer.lines += cleared;
|
||||
|
||||
|
||||
@ -2668,6 +2668,34 @@ void GameRenderer::renderCoopPlayingState(
|
||||
|
||||
drawPlayerScoreboard(CoopGame::PlayerSide::Left, leftColumnLeftX, leftColumnRightX, "PLAYER 1");
|
||||
drawPlayerScoreboard(CoopGame::PlayerSide::Right, rightColumnLeftX, rightColumnRightX, "PLAYER 2");
|
||||
|
||||
// Combined score summary centered under the grid
|
||||
{
|
||||
int leftScore = game->score(CoopGame::PlayerSide::Left);
|
||||
int rightScore = game->score(CoopGame::PlayerSide::Right);
|
||||
int sumScore = leftScore + rightScore;
|
||||
char sumLabel[64];
|
||||
char sumValue[64];
|
||||
std::snprintf(sumLabel, sizeof(sumLabel), "SCORE %d + SCORE %d =", leftScore, rightScore);
|
||||
std::snprintf(sumValue, sizeof(sumValue), "%d", sumScore);
|
||||
|
||||
// Draw label smaller and value larger
|
||||
float labelScale = 0.9f;
|
||||
float valueScale = 1.6f;
|
||||
SDL_Color labelColor = {200, 220, 235, 220};
|
||||
SDL_Color valueColor = {255, 230, 130, 255};
|
||||
|
||||
// Position: centered beneath the grid
|
||||
float centerX = gridX + GRID_W * 0.5f;
|
||||
int lw=0, lh=0; pixelFont->measure(sumLabel, labelScale, lw, lh);
|
||||
int vw=0, vh=0; pixelFont->measure(sumValue, valueScale, vw, vh);
|
||||
float labelX = centerX - static_cast<float>(lw) * 0.5f;
|
||||
float valueX = centerX - static_cast<float>(vw) * 0.5f;
|
||||
float belowY = gridY + GRID_H + 14.0f; // small gap below grid
|
||||
|
||||
pixelFont->draw(renderer, labelX, belowY, sumLabel, labelScale, labelColor);
|
||||
pixelFont->draw(renderer, valueX, belowY + 22.0f, sumValue, valueScale, valueColor);
|
||||
}
|
||||
}
|
||||
|
||||
void GameRenderer::renderExitPopup(
|
||||
|
||||
@ -42,6 +42,7 @@ void ScoreManager::load() {
|
||||
if (value.contains("level")) e.level = value["level"];
|
||||
if (value.contains("timeSec")) e.timeSec = value["timeSec"];
|
||||
if (value.contains("name")) e.name = value["name"];
|
||||
if (value.contains("game_type")) e.gameType = value["game_type"].get<std::string>();
|
||||
scores.push_back(e);
|
||||
}
|
||||
}
|
||||
@ -54,6 +55,7 @@ void ScoreManager::load() {
|
||||
if (value.contains("level")) e.level = value["level"];
|
||||
if (value.contains("timeSec")) e.timeSec = value["timeSec"];
|
||||
if (value.contains("name")) e.name = value["name"];
|
||||
if (value.contains("game_type")) e.gameType = value["game_type"].get<std::string>();
|
||||
scores.push_back(e);
|
||||
}
|
||||
}
|
||||
@ -92,6 +94,8 @@ void ScoreManager::load() {
|
||||
if (!remaining.empty() && remaining[0] == ' ') {
|
||||
e.name = remaining.substr(1); // Remove leading space
|
||||
}
|
||||
// For backward compatibility local files may not include gameType; default is 'classic'
|
||||
e.gameType = "classic";
|
||||
scores.push_back(e);
|
||||
}
|
||||
if (scores.size() >= maxEntries) break;
|
||||
@ -108,11 +112,12 @@ void ScoreManager::load() {
|
||||
void ScoreManager::save() const {
|
||||
std::ofstream f(filePath(), std::ios::trunc);
|
||||
for (auto &e : scores) {
|
||||
f << e.score << ' ' << e.lines << ' ' << e.level << ' ' << e.timeSec << ' ' << e.name << '\n';
|
||||
// Save gameType as trailing token so future loads can preserve it
|
||||
f << e.score << ' ' << e.lines << ' ' << e.level << ' ' << e.timeSec << ' ' << e.name << ' ' << e.gameType << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
void ScoreManager::submit(int score, int lines, int level, double timeSec, const std::string& name) {
|
||||
void ScoreManager::submit(int score, int lines, int level, double timeSec, const std::string& name, const std::string& gameType) {
|
||||
// Add to local list
|
||||
scores.push_back(ScoreEntry{score,lines,level,timeSec, name});
|
||||
std::sort(scores.begin(), scores.end(), [](auto&a,auto&b){return a.score>b.score;});
|
||||
@ -131,6 +136,7 @@ void ScoreManager::submit(int score, int lines, int level, double timeSec, const
|
||||
j["timeSec"] = timeSec;
|
||||
j["name"] = name;
|
||||
j["timestamp"] = std::time(nullptr); // Add timestamp
|
||||
j["game_type"] = gameType;
|
||||
|
||||
// Fire and forget (async) would be better, but for now let's just try to send
|
||||
// We can use std::thread to make it async
|
||||
|
||||
@ -3,14 +3,16 @@
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
struct ScoreEntry { int score{}; int lines{}; int level{}; double timeSec{}; std::string name{"PLAYER"}; };
|
||||
struct ScoreEntry { int score{}; int lines{}; int level{}; double timeSec{}; std::string name{"PLAYER"}; std::string gameType{"classic"}; };
|
||||
|
||||
class ScoreManager {
|
||||
public:
|
||||
explicit ScoreManager(size_t maxScores = 12);
|
||||
void load();
|
||||
void save() const;
|
||||
void submit(int score, int lines, int level, double timeSec, const std::string& name = "PLAYER");
|
||||
// New optional `gameType` parameter will be sent to Firebase as `game_type`.
|
||||
// Allowed values: "classic", "versus", "cooperate", "challenge".
|
||||
void submit(int score, int lines, int level, double timeSec, const std::string& name = "PLAYER", const std::string& gameType = "classic");
|
||||
bool isHighScore(int score) const;
|
||||
const std::vector<ScoreEntry>& all() const { return scores; }
|
||||
private:
|
||||
|
||||
@ -813,7 +813,13 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
|
||||
}
|
||||
static const std::vector<ScoreEntry> EMPTY_SCORES;
|
||||
const auto& hs = ctx.scores ? ctx.scores->all() : EMPTY_SCORES;
|
||||
size_t maxDisplay = std::min(hs.size(), size_t(10)); // display only top 10
|
||||
// Filter highscores to show only classic gameplay entries on the main menu
|
||||
std::vector<ScoreEntry> filtered;
|
||||
filtered.reserve(hs.size());
|
||||
for (const auto &e : hs) {
|
||||
if (e.gameType == "classic") filtered.push_back(e);
|
||||
}
|
||||
size_t maxDisplay = std::min(filtered.size(), size_t(10)); // display only top 10
|
||||
|
||||
// Draw highscores as an inline HUD-like panel (no opaque box), matching Options/Level/Exit style
|
||||
if (useFont) {
|
||||
@ -899,18 +905,18 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
|
||||
char rankStr[8]; std::snprintf(rankStr, sizeof(rankStr), "%zu.", i + 1);
|
||||
useFont->draw(renderer, rankX, y + wave + entryOffset, rankStr, curRowScale, rowColor);
|
||||
|
||||
useFont->draw(renderer, nameXAdj, y + wave + entryOffset, hs[i].name, curRowScale, rowColor);
|
||||
useFont->draw(renderer, nameXAdj, y + wave + entryOffset, filtered[i].name, curRowScale, rowColor);
|
||||
|
||||
char scoreStr[16]; std::snprintf(scoreStr, sizeof(scoreStr), "%d", hs[i].score);
|
||||
char scoreStr[16]; std::snprintf(scoreStr, sizeof(scoreStr), "%d", filtered[i].score);
|
||||
useFont->draw(renderer, scoreX, y + wave + entryOffset, scoreStr, curRowScale, rowColor);
|
||||
|
||||
char linesStr[8]; std::snprintf(linesStr, sizeof(linesStr), "%d", hs[i].lines);
|
||||
char linesStr[8]; std::snprintf(linesStr, sizeof(linesStr), "%d", filtered[i].lines);
|
||||
useFont->draw(renderer, linesX, y + wave + entryOffset, linesStr, curRowScale, rowColor);
|
||||
|
||||
char levelStr[8]; std::snprintf(levelStr, sizeof(levelStr), "%d", hs[i].level);
|
||||
char levelStr[8]; std::snprintf(levelStr, sizeof(levelStr), "%d", filtered[i].level);
|
||||
useFont->draw(renderer, levelX, y + wave + entryOffset, levelStr, curRowScale, rowColor);
|
||||
|
||||
char timeStr[16]; int mins = int(hs[i].timeSec) / 60; int secs = int(hs[i].timeSec) % 60;
|
||||
char timeStr[16]; int mins = int(filtered[i].timeSec) / 60; int secs = int(filtered[i].timeSec) % 60;
|
||||
std::snprintf(timeStr, sizeof(timeStr), "%d:%02d", mins, secs);
|
||||
useFont->draw(renderer, timeX, y + wave + entryOffset, timeStr, curRowScale, rowColor);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user