leaderboard

This commit is contained in:
2026-05-20 07:32:57 +02:00
parent 66c5e0e710
commit ee3c263547
6 changed files with 266 additions and 11 deletions

View File

@@ -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
};
}

View File

@@ -7,12 +7,128 @@
#include <algorithm>
#include <chrono>
#include <ctime>
#include <cstdint>
#include <fstream>
#include <iomanip>
#include <sstream>
#include <string_view>
#include <vector>
#include <utils.hpp>
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<std::int32_t>(score);
return entry;
}
std::vector<LeaderboardEntry> loadLeaderboardEntries() {
std::vector<LeaderboardEntry> 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<char*>(&entry.score), sizeof(entry.score));
if (!file) {
break;
}
entries.push_back(entry);
}
return entries;
}
void saveLeaderboardEntries(const std::vector<LeaderboardEntry>& 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<const char*>(&entry.score), sizeof(entry.score));
}
}
void upsertLeaderboardEntry(std::vector<LeaderboardEntry>& 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<LeaderboardEntry>& 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<LeaderboardEntry> leaderboardEntries = loadLeaderboardEntries();
std::string leaderboardPlayerName = Game::GameManager::getSharedData<std::string>("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<bool>("gameLost")) {
refreshLeaderboardHudText();
return;
}
if (GameManager::getSharedData<bool>("gameWon")) {
refreshLeaderboardHudText();
return;
}
const int enemyCount = GameManager::getSharedData<int>("enemyActiveCount");
const int trashCount = GameManager::getSharedData<int>("trashActiveCount");
const int stage = GameManager::getSharedData<int>("gameStage");
@@ -347,6 +474,7 @@ namespace Game::AGame {
mPendingLevelStage = stage + 1;
} else if (!GameManager::getSharedData<bool>("gameWon")) {
writeFinalScoreFile(GameManager::getSharedData<int>("gameScore"));
refreshLeaderboardHudText();
GameManager::setSharedData("gameWon", true);
LOG("Vsi nivoji so zaključeni");
}

View File

@@ -33,18 +33,28 @@ namespace Game::AGame {
};
if (GameManager::getSharedData<bool>("gameLost")) {
if (getText() != "Umrl si!") {
const std::string s = "Umrl si!";
Window::Window::postToMainThread([this, s]() { setText(s); });
const std::string leaderboardText = GameManager::getSharedData<std::string>("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<bool>("gameWon")) {
if (getText() != "Zmagal si!") {
const std::string s = "Zmagal si!";
Window::Window::postToMainThread([this, s]() { setText(s); });
const std::string leaderboardText = GameManager::getSharedData<std::string>("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;

View File

@@ -255,17 +255,17 @@ int main() {
// Title text (centered)
auto* title = dynamic_cast<Game::Object::UIText*>(State::GameState::getInstance().addEntity(std::make_unique<Game::Object::UIText>("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<Game::Object::UITextBox*>(State::GameState::getInstance().addEntity(std::make_unique<Game::Object::UITextBox>("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<Game::Renderer::Texture>(startButtonFont);
auto* btnEntity = dynamic_cast<Game::Object::UIButton*>(State::GameState::getInstance().addEntity(std::make_unique<Game::Object::UIButton>("StartButton", btnTex, Object::DEFAULT_TRANSFORM, reinterpret_cast<void*>(&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<Game::Renderer::Texture>(replayButtonFont);

View File

@@ -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 {
&center,
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

View File

@@ -1,4 +1,7 @@
#include <renderer/font.hpp>
#include <algorithm>
#include <string_view>
#include <vector>
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<std::string_view> 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<LineSurface> 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);