From ee3c26354748ca9b29a42e85d4c71a949d2002a3 Mon Sep 17 00:00:00 2001 From: DcruBro Date: Wed, 20 May 2026 07:32:57 +0200 Subject: [PATCH] leaderboard --- include/object/ui/uitextbox.hpp | 1 + src/game/agame/background.cpp | 128 ++++++++++++++++++++++++++++++++ src/game/agame/hudtext.cpp | 22 ++++-- src/main.cpp | 6 +- src/object/ui/uitextbox.cpp | 19 +++++ src/renderer/font.cpp | 101 ++++++++++++++++++++++++- 6 files changed, 266 insertions(+), 11 deletions(-) diff --git a/include/object/ui/uitextbox.hpp b/include/object/ui/uitextbox.hpp index ff167eb..d6d1eca 100644 --- a/include/object/ui/uitextbox.hpp +++ b/include/object/ui/uitextbox.hpp @@ -52,6 +52,7 @@ namespace Game::Object { std::string mLastRenderedText; // to avoid rebuilding font unnecessarily std::string mPlaceholder = ""; float mReservedPlaceholderWidth = 0.f; // Reserved pixel width for placeholder to avoid layout shifts + bool mIsHovered = false; std::mutex mRenderMutex; // Protects mLastRenderedText and mReservedPlaceholderWidth from main-thread updates }; } diff --git a/src/game/agame/background.cpp b/src/game/agame/background.cpp index 28f9848..ffdb21c 100644 --- a/src/game/agame/background.cpp +++ b/src/game/agame/background.cpp @@ -7,12 +7,128 @@ #include #include #include +#include #include #include +#include +#include +#include #include namespace { + constexpr const char* kLeaderboardFile = "leaderboard.bin"; + constexpr std::size_t kLeaderboardNameCapacity = 24; + + struct LeaderboardEntry { + char name[kLeaderboardNameCapacity + 1]{}; + std::int32_t score = 0; + }; + + std::string trimLeaderboardName(const std::string& name) { + return name.substr(0, kLeaderboardNameCapacity); + } + + LeaderboardEntry makeLeaderboardEntry(const std::string& name, int score) { + LeaderboardEntry entry{}; + const std::string truncatedName = trimLeaderboardName(name); + std::copy(truncatedName.begin(), truncatedName.end(), entry.name); + entry.score = static_cast(score); + return entry; + } + + std::vector loadLeaderboardEntries() { + std::vector entries; + + std::ifstream file(kLeaderboardFile, std::ios::binary); + if (!file.is_open()) { + return entries; + } + + while (true) { + LeaderboardEntry entry{}; + file.read(entry.name, sizeof(entry.name)); + if (!file) { + break; + } + + file.read(reinterpret_cast(&entry.score), sizeof(entry.score)); + if (!file) { + break; + } + + entries.push_back(entry); + } + + return entries; + } + + void saveLeaderboardEntries(const std::vector& entries) { + std::ofstream file(kLeaderboardFile, std::ios::binary | std::ios::trunc); + if (!file.is_open()) { + WARN("Neuspešno odpiranje leaderboard.bin za pisanje"); + return; + } + + for (const auto& entry : entries) { + file.write(entry.name, sizeof(entry.name)); + file.write(reinterpret_cast(&entry.score), sizeof(entry.score)); + } + } + + void upsertLeaderboardEntry(std::vector& entries, const std::string& playerName, int score) { + const std::string truncatedName = trimLeaderboardName(playerName); + entries.erase( + std::remove_if(entries.begin(), entries.end(), [&](const LeaderboardEntry& entry) { + return truncatedName == entry.name; + }), + entries.end() + ); + + entries.push_back(makeLeaderboardEntry(truncatedName, score)); + + std::sort(entries.begin(), entries.end(), [](const LeaderboardEntry& left, const LeaderboardEntry& right) { + if (left.score != right.score) { + return left.score > right.score; + } + + return std::string_view(left.name) < std::string_view(right.name); + }); + } + + std::string formatLeaderboardEntries(const std::vector& entries) { + std::ostringstream stream; + stream << "Lestvica:"; + + if (entries.empty()) { + stream << "\n(brez vpisov)"; + return stream.str(); + } + + for (std::size_t index = 0; index < entries.size(); ++index) { + stream << "\n" << (index + 1) << ". " << entries[index].name << " - " << entries[index].score; + } + + return stream.str(); + } + + std::string loadLeaderboardText() { + return formatLeaderboardEntries(loadLeaderboardEntries()); + } + + void refreshLeaderboardHudText() { + Game::GameManager::setSharedData("leaderboardText", loadLeaderboardText()); + } + void writeFinalScoreFile(int score) { + std::vector leaderboardEntries = loadLeaderboardEntries(); + std::string leaderboardPlayerName = Game::GameManager::getSharedData("playerName"); + if (leaderboardPlayerName.empty()) { + leaderboardPlayerName = "Player"; + } + + upsertLeaderboardEntry(leaderboardEntries, leaderboardPlayerName, score); + saveLeaderboardEntries(leaderboardEntries); + std::ofstream file("score.txt", std::ios::trunc); if (!file.is_open()) { WARN("Neuspešno odpiranje score.txt za pisanje"); @@ -73,6 +189,7 @@ namespace Game::AGame { GameManager::setSharedData("gameStage", 1); GameManager::setSharedData("gameWon", false); GameManager::setSharedData("gameLost", false); + GameManager::setSharedData("leaderboardText", loadLeaderboardText()); mZIndex = -1; // Ensure background renders behind other entities mTex->setTiled(true); // Set the background texture to be tiled @@ -280,6 +397,16 @@ namespace Game::AGame { void Background::update(float deltaTime) { (void)deltaTime; + if (GameManager::getSharedData("gameLost")) { + refreshLeaderboardHudText(); + return; + } + + if (GameManager::getSharedData("gameWon")) { + refreshLeaderboardHudText(); + return; + } + const int enemyCount = GameManager::getSharedData("enemyActiveCount"); const int trashCount = GameManager::getSharedData("trashActiveCount"); const int stage = GameManager::getSharedData("gameStage"); @@ -347,6 +474,7 @@ namespace Game::AGame { mPendingLevelStage = stage + 1; } else if (!GameManager::getSharedData("gameWon")) { writeFinalScoreFile(GameManager::getSharedData("gameScore")); + refreshLeaderboardHudText(); GameManager::setSharedData("gameWon", true); LOG("Vsi nivoji so zaključeni"); } diff --git a/src/game/agame/hudtext.cpp b/src/game/agame/hudtext.cpp index 5cc6ad9..dcc36f8 100644 --- a/src/game/agame/hudtext.cpp +++ b/src/game/agame/hudtext.cpp @@ -33,18 +33,28 @@ namespace Game::AGame { }; if (GameManager::getSharedData("gameLost")) { - if (getText() != "Umrl si!") { - const std::string s = "Umrl si!"; - Window::Window::postToMainThread([this, s]() { setText(s); }); + const std::string leaderboardText = GameManager::getSharedData("leaderboardText"); + std::string endText = "Umrl si!"; + if (!leaderboardText.empty()) { + endText += "\n\n" + leaderboardText; + } + + if (getText() != endText) { + Window::Window::postToMainThread([this, endText]() { setText(endText); }); } anchorTopRight(); return; } if (GameManager::getSharedData("gameWon")) { - if (getText() != "Zmagal si!") { - const std::string s = "Zmagal si!"; - Window::Window::postToMainThread([this, s]() { setText(s); }); + const std::string leaderboardText = GameManager::getSharedData("leaderboardText"); + std::string endText = "Zmagal si!"; + if (!leaderboardText.empty()) { + endText += "\n\n" + leaderboardText; + } + + if (getText() != endText) { + Window::Window::postToMainThread([this, endText]() { setText(endText); }); } anchorTopRight(); return; diff --git a/src/main.cpp b/src/main.cpp index 0c7c38a..1339982 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -255,17 +255,17 @@ int main() { // Title text (centered) auto* title = dynamic_cast(State::GameState::getInstance().addEntity(std::make_unique("Title", titleFont, Object::DEFAULT_TRANSFORM, cx, titleY))); - if (title) title->setText("Game Name"); + if (title) title->setText("Dol s Plastiko!"); // Name input box (larger and more visible) with a light gray background auto textboxBg = createSolidColorTexture(gSDLRenderer, 400, 70, 180, 180, 180, 220); // Larger, slightly darker gray auto* nameBox = dynamic_cast(State::GameState::getInstance().addEntity(std::make_unique("NameBox", textboxBg, nameBoxFont, Object::DEFAULT_TRANSFORM, cx, textboxY))); - if (nameBox) nameBox->setPlaceholder("Enter name..."); + if (nameBox) nameBox->setPlaceholder("Vnesi ime..."); // Start button (below center) auto btnTex = std::dynamic_pointer_cast(startButtonFont); auto* btnEntity = dynamic_cast(State::GameState::getInstance().addEntity(std::make_unique("StartButton", btnTex, Object::DEFAULT_TRANSFORM, reinterpret_cast(&startGameCallback), cx, startButtonY))); - if (btnEntity) btnEntity->setText("Start Game"); + if (btnEntity) btnEntity->setText("Začni igro"); // Replay button (below start button) auto replayBtnTex = std::dynamic_pointer_cast(replayButtonFont); diff --git a/src/object/ui/uitextbox.cpp b/src/object/ui/uitextbox.cpp index 5739737..f7d0ce4 100644 --- a/src/object/ui/uitextbox.cpp +++ b/src/object/ui/uitextbox.cpp @@ -85,6 +85,7 @@ namespace Game::Object { float bottom = mTransform.y + (mBackground ? mBackground->getHeight() * mTransform.adjustedScaleY() : 0.f); bool inside = (mouseX >= left && mouseX <= right && mouseY >= top && mouseY <= bottom); + mIsHovered = inside; if (inside && Input::isMouseButtonJustPressed(SDL_BUTTON_LEFT)) { mIsFocused = true; // Enable SDL text input on main thread (requires SDL_Window pointer) @@ -225,6 +226,24 @@ namespace Game::Object { ¢er, mIsFlipped ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE ); + + if (mIsHovered) { + Uint8 prevR = 0; + Uint8 prevG = 0; + Uint8 prevB = 0; + Uint8 prevA = 0; + SDL_BlendMode prevBlendMode = SDL_BLENDMODE_NONE; + + SDL_GetRenderDrawColor(renderer->getSDLRenderer(), &prevR, &prevG, &prevB, &prevA); + SDL_GetRenderDrawBlendMode(renderer->getSDLRenderer(), &prevBlendMode); + + SDL_SetRenderDrawBlendMode(renderer->getSDLRenderer(), SDL_BLENDMODE_BLEND); + SDL_SetRenderDrawColor(renderer->getSDLRenderer(), 0, 0, 0, 28); + SDL_RenderFillRect(renderer->getSDLRenderer(), &dst); + + SDL_SetRenderDrawColor(renderer->getSDLRenderer(), prevR, prevG, prevB, prevA); + SDL_SetRenderDrawBlendMode(renderer->getSDLRenderer(), prevBlendMode); + } } // Render font texture (text) on top of background diff --git a/src/renderer/font.cpp b/src/renderer/font.cpp index 0052512..34ecac7 100644 --- a/src/renderer/font.cpp +++ b/src/renderer/font.cpp @@ -1,4 +1,7 @@ #include +#include +#include +#include namespace Game::Renderer { Font::Font(const std::string& path, SDL_Renderer* renderer, int ptSize, std::string id) @@ -40,12 +43,106 @@ namespace Game::Renderer { mLastText = text; mLastColor = color; - SDL_Surface* surf = TTF_RenderText_Blended(mFont, text.c_str(), text.size(), color); + const int fontLineSkip = TTF_GetFontLineSkip(mFont); + std::vector lines; + std::size_t lineStart = 0; + while (lineStart <= text.size()) { + const std::size_t lineEnd = text.find('\n', lineStart); + if (lineEnd == std::string::npos) { + lines.emplace_back(text.data() + lineStart, text.size() - lineStart); + break; + } + + lines.emplace_back(text.data() + lineStart, lineEnd - lineStart); + lineStart = lineEnd + 1; + if (lineStart == text.size()) { + lines.emplace_back(std::string_view{}); + break; + } + } + + if (lines.empty()) { + lines.emplace_back(std::string_view{}); + } + + struct LineSurface { + SDL_Surface* surface = nullptr; + int width = 0; + int height = 0; + }; + + std::vector lineSurfaces; + lineSurfaces.reserve(lines.size()); + + int maxWidth = 0; + int totalHeight = 0; + + for (const auto line : lines) { + LineSurface lineSurface{}; + if (!line.empty()) { + int measuredWidth = 0; + int measuredHeight = 0; + if (!TTF_GetStringSize(mFont, line.data(), line.size(), &measuredWidth, &measuredHeight)) { + for (auto& storedLine : lineSurfaces) { + if (storedLine.surface) { + SDL_DestroySurface(storedLine.surface); + } + } + ERROR("TTF_GetStringSize Error: " << SDL_GetError() << " (This object may be unusuable)"); + return; + } + + lineSurface.surface = TTF_RenderText_Blended(mFont, line.data(), line.size(), color); + if (!lineSurface.surface) { + for (auto& storedLine : lineSurfaces) { + if (storedLine.surface) { + SDL_DestroySurface(storedLine.surface); + } + } + ERROR("TTF_RenderText_Blended Error: " << SDL_GetError() << " (This object may be unusuable)"); + return; + } + + lineSurface.width = measuredWidth; + lineSurface.height = measuredHeight; + } else { + lineSurface.width = 0; + lineSurface.height = fontLineSkip; + } + + maxWidth = std::max(maxWidth, lineSurface.width); + totalHeight += lineSurface.height; + lineSurfaces.push_back(lineSurface); + } + + if (maxWidth <= 0) { + maxWidth = 1; + } + if (totalHeight <= 0) { + totalHeight = std::max(1, fontLineSkip); + } + + SDL_Surface* surf = SDL_CreateSurface(maxWidth, totalHeight, SDL_PIXELFORMAT_RGBA8888); if (!surf) { - ERROR("TTF_RenderText_Blended Error: " << SDL_GetError() << " (This object may be unusuable)"); + for (auto& storedLine : lineSurfaces) { + if (storedLine.surface) { + SDL_DestroySurface(storedLine.surface); + } + } + ERROR("SDL_CreateSurface Error: " << SDL_GetError() << " (This object may be unusuable)"); return; } + int currentY = 0; + for (auto& lineSurface : lineSurfaces) { + if (lineSurface.surface) { + SDL_Rect dstRect{0, currentY, lineSurface.width, lineSurface.height}; + SDL_BlitSurface(lineSurface.surface, nullptr, surf, &dstRect); + SDL_DestroySurface(lineSurface.surface); + } + currentY += lineSurface.height; + } + // Convert to texture mTex = SDL_CreateTextureFromSurface(mRenderer, surf); SDL_DestroySurface(surf);