leaderboard
This commit is contained in:
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user