Compare commits
4 Commits
d93e71e716
...
drug
| Author | SHA1 | Date | |
|---|---|---|---|
| ee3c263547 | |||
| 66c5e0e710 | |||
| 6534996a52 | |||
| 0b45643ef2 |
@@ -4,6 +4,21 @@ project(Letnik3Zadnja)
|
|||||||
set(CMAKE_CXX_STANDARD 23)
|
set(CMAKE_CXX_STANDARD 23)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
if(APPLE AND NOT CMAKE_OSX_SYSROOT)
|
||||||
|
execute_process(
|
||||||
|
COMMAND xcrun --sdk macosx --show-sdk-path
|
||||||
|
OUTPUT_VARIABLE _macos_sdk_path
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||||
|
ERROR_QUIET
|
||||||
|
)
|
||||||
|
|
||||||
|
if(_macos_sdk_path)
|
||||||
|
set(CMAKE_OSX_SYSROOT "${_macos_sdk_path}" CACHE PATH "macOS SDK path" FORCE)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
option(FETCH "Fetch SDL dependencies instead of using system-installed packages" OFF)
|
||||||
|
|
||||||
# Compile flags
|
# Compile flags
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Werror -Wno-unused-parameter -O2")
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Werror -Wno-unused-parameter -O2")
|
||||||
|
|
||||||
@@ -17,10 +32,10 @@ add_executable(${PROJECT_NAME} ${SOURCES})
|
|||||||
target_include_directories(${PROJECT_NAME} PRIVATE include)
|
target_include_directories(${PROJECT_NAME} PRIVATE include)
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# Platform-specific dependency handling
|
# Dependency handling
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
if(WIN32)
|
if(FETCH OR WIN32)
|
||||||
|
|
||||||
# Include FetchContent to download SDL libraries
|
# Include FetchContent to download SDL libraries
|
||||||
include(FetchContent)
|
include(FetchContent)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ GAME_ENTITY(Player)
|
|||||||
void setGroundTexture(std::shared_ptr<Game::Renderer::Texture> tex);
|
void setGroundTexture(std::shared_ptr<Game::Renderer::Texture> tex);
|
||||||
void respawnRandomSea(float landBoundaryX);
|
void respawnRandomSea(float landBoundaryX);
|
||||||
bool isShipMode() const { return mIsShipMode; }
|
bool isShipMode() const { return mIsShipMode; }
|
||||||
|
void setShipMode(bool isShip) { mIsShipMode = isShip; } // Set form state for replay
|
||||||
void onCollisionEnter(Object::Entity* other) override;
|
void onCollisionEnter(Object::Entity* other) override;
|
||||||
private:
|
private:
|
||||||
Object::Sound mSound;
|
Object::Sound mSound;
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <object/ui/uitextbox.hpp>
|
|
||||||
|
|
||||||
namespace Game::AGame {
|
|
||||||
class SampleTextBox : public Object::UITextBox {
|
|
||||||
using Object::UITextBox::UITextBox; // Inherit constructors
|
|
||||||
|
|
||||||
public:
|
|
||||||
~SampleTextBox() override = default;
|
|
||||||
|
|
||||||
void start() override {
|
|
||||||
// Call the base class start to initialize the text box
|
|
||||||
mZIndex = 1000; // Ensure it renders on top of most other entities
|
|
||||||
Object::UITextBox::start();
|
|
||||||
setText("Hello, World!");
|
|
||||||
|
|
||||||
mIsActive = false;
|
|
||||||
mIsVisible = false;
|
|
||||||
}
|
|
||||||
void update(float deltaTime) override {
|
|
||||||
// Call the base class update to handle input and text refreshing
|
|
||||||
Object::UITextBox::update(deltaTime);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -19,7 +19,8 @@ namespace Game {
|
|||||||
enum class GameStateEnum {
|
enum class GameStateEnum {
|
||||||
RUNNING,
|
RUNNING,
|
||||||
PAUSED,
|
PAUSED,
|
||||||
STOPPED
|
STOPPED,
|
||||||
|
REPLAY
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class SharedDataType {
|
enum class SharedDataType {
|
||||||
@@ -59,7 +60,14 @@ namespace Game {
|
|||||||
static void processPendingEntityRemovals();
|
static void processPendingEntityRemovals();
|
||||||
|
|
||||||
static void pushPlayerPosition(Object::Transform transform) { mPlayerTransformHistory.push_back(transform); }
|
static void pushPlayerPosition(Object::Transform transform) { mPlayerTransformHistory.push_back(transform); }
|
||||||
|
static void pushPlayerFormState(bool isShipMode) { mPlayerFormHistory.push_back(isShipMode); }
|
||||||
static void getPlayerPositionHistory(std::vector<Object::Transform>& outHistory) { outHistory = mPlayerTransformHistory; }
|
static void getPlayerPositionHistory(std::vector<Object::Transform>& outHistory) { outHistory = mPlayerTransformHistory; }
|
||||||
|
static void getPlayerFormHistory(std::vector<bool>& outHistory) { outHistory = mPlayerFormHistory; }
|
||||||
|
|
||||||
|
// Replay mode API
|
||||||
|
static bool initReplayMode();
|
||||||
|
static bool playReplayFrame();
|
||||||
|
static void stopReplayMode();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int mTargetUpdatesPerSecond = TARGET_UPDATE_RATE;
|
int mTargetUpdatesPerSecond = TARGET_UPDATE_RATE;
|
||||||
@@ -69,8 +77,17 @@ namespace Game {
|
|||||||
static std::unordered_map<std::string, float> mSharedFloats;
|
static std::unordered_map<std::string, float> mSharedFloats;
|
||||||
static std::unordered_map<std::string, bool> mSharedBools;
|
static std::unordered_map<std::string, bool> mSharedBools;
|
||||||
static std::vector<Object::Transform> mPlayerTransformHistory;
|
static std::vector<Object::Transform> mPlayerTransformHistory;
|
||||||
|
static std::vector<bool> mPlayerFormHistory;
|
||||||
static GameStateEnum mCurrentGameState;
|
static GameStateEnum mCurrentGameState;
|
||||||
float mLastDelta = 0.f;
|
float mLastDelta = 0.f;
|
||||||
|
|
||||||
|
// Replay data
|
||||||
|
struct ReplayFrame {
|
||||||
|
float x, y, rotation, scaleX, scaleY;
|
||||||
|
bool isShipMode;
|
||||||
|
};
|
||||||
|
static std::vector<ReplayFrame> mReplayFrames;
|
||||||
|
static size_t mCurrentReplayFrame;
|
||||||
};
|
};
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
#include <vector>
|
||||||
|
#include <queue>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
namespace Game {
|
namespace Game {
|
||||||
class Input {
|
class Input {
|
||||||
@@ -16,13 +20,23 @@ namespace Game {
|
|||||||
static bool isMouseButtonJustReleased(Uint8 button);
|
static bool isMouseButtonJustReleased(Uint8 button);
|
||||||
static float getMouseX();
|
static float getMouseX();
|
||||||
static float getMouseY();
|
static float getMouseY();
|
||||||
|
|
||||||
|
// Text input from SDL text-input events (pushed by window thread, consumed by game thread)
|
||||||
|
static void pushText(const std::string& utf8);
|
||||||
|
static void consumeText(std::vector<std::string>& out);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static const bool* mCurrentKeyStates;
|
static const bool* mCurrentKeyStates;
|
||||||
static const bool* mPreviousKeyStates;
|
static std::vector<Uint8> mPreviousKeyStates;
|
||||||
static int mNumKeys;
|
static int mNumKeys;
|
||||||
|
static int mPrevNumKeys;
|
||||||
static SDL_MouseButtonFlags mCurrentMouseButtonStates;
|
static SDL_MouseButtonFlags mCurrentMouseButtonStates;
|
||||||
static SDL_MouseButtonFlags mPreviousMouseButtonStates;
|
static SDL_MouseButtonFlags mPreviousMouseButtonStates;
|
||||||
static float mMouseX;
|
static float mMouseX;
|
||||||
static float mMouseY;
|
static float mMouseY;
|
||||||
|
|
||||||
|
// Text input queue and mutex (window thread writes via pushText, game thread reads via consumeText)
|
||||||
|
static std::mutex mTextMutex;
|
||||||
|
static std::vector<std::string> mPendingText;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -25,5 +25,6 @@ namespace Game::Object {
|
|||||||
void* mClickFunction = nullptr;
|
void* mClickFunction = nullptr;
|
||||||
float mX, mY;
|
float mX, mY;
|
||||||
std::string mText;
|
std::string mText;
|
||||||
|
bool mIsHovered = false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -4,62 +4,55 @@
|
|||||||
#include <renderer/font.hpp>
|
#include <renderer/font.hpp>
|
||||||
#include <renderer/texture.hpp>
|
#include <renderer/texture.hpp>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <string>
|
|
||||||
#include <game/input.hpp>
|
#include <game/input.hpp>
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
namespace Game::Object {
|
namespace Game::Object {
|
||||||
|
|
||||||
struct UITextboxConfig {
|
|
||||||
SDL_Color bgColor = {20, 20, 20, 210};
|
|
||||||
SDL_Color borderColor = {110, 110, 110, 255};
|
|
||||||
SDL_Color focusedBorderColor = {200, 175, 70, 255};
|
|
||||||
SDL_Color textColor = {255, 255, 255, 255};
|
|
||||||
SDL_Color focusedTextColor = {255, 240, 180, 255};
|
|
||||||
SDL_Color placeholderColor = {120, 120, 120, 200};
|
|
||||||
float borderThickness = 2.f;
|
|
||||||
float paddingX = 8.f;
|
|
||||||
float paddingY = 4.f;
|
|
||||||
float minWidth = 160.f;
|
|
||||||
float minHeight = 32.f;
|
|
||||||
int maxLength = 0; // 0 = unlimited
|
|
||||||
std::string placeholder = "";
|
|
||||||
float cursorBlinkRate = 0.53f; // seconds per blink phase
|
|
||||||
};
|
|
||||||
|
|
||||||
class UITextBox : public Entity {
|
class UITextBox : public Entity {
|
||||||
public:
|
public:
|
||||||
UITextBox(const std::string& name, std::shared_ptr<Renderer::Font> font,
|
UITextBox(const std::string& name, std::shared_ptr<Renderer::Texture> background, std::shared_ptr<Renderer::Font> font, const Transform& transform, float x = 0.f, float y = 0.f);
|
||||||
const Transform& transform,
|
|
||||||
float x = 0.f, float y = 0.f,
|
|
||||||
UITextboxConfig config = {});
|
|
||||||
~UITextBox() override = default;
|
~UITextBox() override = default;
|
||||||
|
|
||||||
void start() override;
|
void start() override;
|
||||||
void update(float deltaTime) override;
|
void update(float deltaTime) override;
|
||||||
|
|
||||||
|
// Custom render to show both background and text
|
||||||
void render(Game::Renderer::Renderer* renderer, Game::Renderer::RendererConfig config) override;
|
void render(Game::Renderer::Renderer* renderer, Game::Renderer::RendererConfig config) override;
|
||||||
|
|
||||||
void setText(const std::string& text);
|
void setText(const std::string& text);
|
||||||
std::string getText() const;
|
std::string getText() const;
|
||||||
std::string getValue() const;
|
void setPlaceholder(const std::string& placeholder) { mPlaceholder = placeholder; mLastRenderedText.clear(); }
|
||||||
bool isFocused() const;
|
|
||||||
|
// Insert UTF-8 text at the current cursor position (delivered from Input queue)
|
||||||
|
void insertText(const std::string& utf8);
|
||||||
|
|
||||||
|
void setMaxLength(size_t maxLen) { mMaxLength = maxLen; }
|
||||||
|
void setPasswordMode(bool enable) { mPasswordMode = enable; }
|
||||||
|
void setOnFocus(void* fn) { mOnFocus = fn; }
|
||||||
|
|
||||||
void setPosition(float x, float y) { mX = x; mY = y; }
|
void setPosition(float x, float y) { mX = x; mY = y; }
|
||||||
std::pair<float, float> getPosition() const { return {mX, mY}; }
|
std::pair<float, float> getPosition() const { return {mX, mY}; }
|
||||||
|
|
||||||
|
bool isFocused() const { return mIsFocused; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool isMouseInsideBox() const;
|
std::shared_ptr<Renderer::Texture> mBackground; // Background texture for the textbox
|
||||||
void refreshVisualText();
|
std::shared_ptr<Renderer::Font> mFont; // Font used to render text
|
||||||
|
|
||||||
float mX, mY;
|
float mX, mY;
|
||||||
std::string mText;
|
std::string mText;
|
||||||
|
size_t mCursorIndex = 0;
|
||||||
|
float mCursorBlinkTimer = 0.f;
|
||||||
|
bool mShowCursor = true;
|
||||||
|
size_t mMaxLength = 1024;
|
||||||
|
bool mPasswordMode = false;
|
||||||
bool mIsFocused = false;
|
bool mIsFocused = false;
|
||||||
float mBoxWidth = 0.f;
|
void* mOnFocus = nullptr; // optional function pointer
|
||||||
float mBoxHeight = 0.f;
|
std::string mLastRenderedText; // to avoid rebuilding font unnecessarily
|
||||||
bool mNeedsTextRefresh = true;
|
std::string mPlaceholder = "";
|
||||||
UITextboxConfig mConfig;
|
float mReservedPlaceholderWidth = 0.f; // Reserved pixel width for placeholder to avoid layout shifts
|
||||||
|
bool mIsHovered = false;
|
||||||
float mCursorTimer = 0.f;
|
std::mutex mRenderMutex; // Protects mLastRenderedText and mReservedPlaceholderWidth from main-thread updates
|
||||||
bool mCursorVisible = true;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ namespace Game::Renderer {
|
|||||||
|
|
||||||
// Build the texture for the font; Call getSDLTexture() afterwards
|
// Build the texture for the font; Call getSDLTexture() afterwards
|
||||||
void build(SDL_Color color, std::string text);
|
void build(SDL_Color color, std::string text);
|
||||||
// Rebuild GPU-backed texture after a renderer/device reset
|
// Rebuild GPU-backed texture after a renderer/device reset
|
||||||
bool reload(SDL_Renderer* renderer);
|
bool reload(SDL_Renderer* renderer);
|
||||||
|
|
||||||
SDL_Texture* getSDLTexture();
|
SDL_Texture* getSDLTexture();
|
||||||
std::string getId();
|
std::string getId();
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ namespace Game::Renderer {
|
|||||||
float getHeight();
|
float getHeight();
|
||||||
bool isTiled() { return mIsTiled; }
|
bool isTiled() { return mIsTiled; }
|
||||||
void setTiled(bool tiled) { mIsTiled = tiled; }
|
void setTiled(bool tiled) { mIsTiled = tiled; }
|
||||||
|
void setSDLTexture(SDL_Texture* tex) { mTex = tex; }
|
||||||
// Reload GPU-backed texture using a new renderer after device reset
|
// Reload GPU-backed texture using a new renderer after device reset
|
||||||
virtual bool reload(SDL_Renderer* renderer);
|
virtual bool reload(SDL_Renderer* renderer);
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ namespace Game::Window {
|
|||||||
|
|
||||||
static SDL_Window* getSDLWindowBackend() { std::scoped_lock lock(sMutex); return sWindowBackend; }
|
static SDL_Window* getSDLWindowBackend() { std::scoped_lock lock(sMutex); return sWindowBackend; }
|
||||||
Renderer::Renderer* getRenderer() { std::scoped_lock lock(mMutex); return &mRenderer; }
|
Renderer::Renderer* getRenderer() { std::scoped_lock lock(mMutex); return &mRenderer; }
|
||||||
|
// Post a task to be executed on the window/event thread.
|
||||||
|
static void postToMainThread(std::function<void()> fn);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
mutable std::mutex mMutex;
|
mutable std::mutex mMutex;
|
||||||
|
|||||||
@@ -7,12 +7,128 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
|
#include <cstdint>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
#include <utils.hpp>
|
#include <utils.hpp>
|
||||||
|
|
||||||
namespace {
|
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) {
|
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);
|
std::ofstream file("score.txt", std::ios::trunc);
|
||||||
if (!file.is_open()) {
|
if (!file.is_open()) {
|
||||||
WARN("Neuspešno odpiranje score.txt za pisanje");
|
WARN("Neuspešno odpiranje score.txt za pisanje");
|
||||||
@@ -36,17 +152,23 @@ namespace {
|
|||||||
file << "Točke: " << score << "\n";
|
file << "Točke: " << score << "\n";
|
||||||
file << "Datum: " << std::put_time(&localTime, "%Y-%m-%d %H:%M:%S") << "\n";
|
file << "Datum: " << std::put_time(&localTime, "%Y-%m-%d %H:%M:%S") << "\n";
|
||||||
|
|
||||||
// Replay system
|
// Replay system with form state
|
||||||
std::vector<Game::Object::Transform> playerHistory;
|
std::vector<Game::Object::Transform> playerHistory;
|
||||||
|
std::vector<bool> formHistory;
|
||||||
Game::GameManager::getPlayerPositionHistory(playerHistory);
|
Game::GameManager::getPlayerPositionHistory(playerHistory);
|
||||||
|
Game::GameManager::getPlayerFormHistory(formHistory);
|
||||||
|
|
||||||
std::ofstream replayFile("replay.txt", std::ios::trunc);
|
std::ofstream replayFile("replay.txt", std::ios::trunc);
|
||||||
if (!replayFile.is_open()) {
|
if (!replayFile.is_open()) {
|
||||||
WARN("Neuspešno odpiranje replay.txt za pisanje");
|
WARN("Neuspešno odpiranje replay.txt za pisanje");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& transform : playerHistory) {
|
for (size_t i = 0; i < playerHistory.size(); ++i) {
|
||||||
replayFile << transform.x << " " << transform.y << " " << transform.rotation << " " << transform.scaleX << " " << transform.scaleY << "\n";
|
const auto& transform = playerHistory[i];
|
||||||
|
bool isShipMode = (i < formHistory.size()) ? formHistory[i] : true;
|
||||||
|
int shipFlag = isShipMode ? 1 : 0;
|
||||||
|
replayFile << transform.x << " " << transform.y << " " << transform.rotation << " " << transform.scaleX << " " << transform.scaleY << " " << shipFlag << "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG("Zapis končne statistike in replaya igre dokončan");
|
LOG("Zapis končne statistike in replaya igre dokončan");
|
||||||
@@ -67,6 +189,7 @@ namespace Game::AGame {
|
|||||||
GameManager::setSharedData("gameStage", 1);
|
GameManager::setSharedData("gameStage", 1);
|
||||||
GameManager::setSharedData("gameWon", false);
|
GameManager::setSharedData("gameWon", false);
|
||||||
GameManager::setSharedData("gameLost", false);
|
GameManager::setSharedData("gameLost", false);
|
||||||
|
GameManager::setSharedData("leaderboardText", loadLeaderboardText());
|
||||||
|
|
||||||
mZIndex = -1; // Ensure background renders behind other entities
|
mZIndex = -1; // Ensure background renders behind other entities
|
||||||
mTex->setTiled(true); // Set the background texture to be tiled
|
mTex->setTiled(true); // Set the background texture to be tiled
|
||||||
@@ -92,7 +215,11 @@ namespace Game::AGame {
|
|||||||
mTransform.y -= mTex->getHeight() * mTransform.adjustedScaleY() / 2.f;
|
mTransform.y -= mTex->getHeight() * mTransform.adjustedScaleY() / 2.f;
|
||||||
|
|
||||||
LOG("W: " << mW << " H: " << mH);
|
LOG("W: " << mW << " H: " << mH);
|
||||||
spawnLevel(1);
|
|
||||||
|
// Only spawn level entities during normal gameplay, not during replay
|
||||||
|
if (GameManager::getCurrentGameState() != GameStateEnum::REPLAY) {
|
||||||
|
spawnLevel(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Background::render(Game::Renderer::Renderer* renderer, Game::Renderer::RendererConfig config) {
|
void Background::render(Game::Renderer::Renderer* renderer, Game::Renderer::RendererConfig config) {
|
||||||
@@ -270,6 +397,16 @@ namespace Game::AGame {
|
|||||||
void Background::update(float deltaTime) {
|
void Background::update(float deltaTime) {
|
||||||
(void)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 enemyCount = GameManager::getSharedData<int>("enemyActiveCount");
|
||||||
const int trashCount = GameManager::getSharedData<int>("trashActiveCount");
|
const int trashCount = GameManager::getSharedData<int>("trashActiveCount");
|
||||||
const int stage = GameManager::getSharedData<int>("gameStage");
|
const int stage = GameManager::getSharedData<int>("gameStage");
|
||||||
@@ -337,6 +474,7 @@ namespace Game::AGame {
|
|||||||
mPendingLevelStage = stage + 1;
|
mPendingLevelStage = stage + 1;
|
||||||
} else if (!GameManager::getSharedData<bool>("gameWon")) {
|
} else if (!GameManager::getSharedData<bool>("gameWon")) {
|
||||||
writeFinalScoreFile(GameManager::getSharedData<int>("gameScore"));
|
writeFinalScoreFile(GameManager::getSharedData<int>("gameScore"));
|
||||||
|
refreshLeaderboardHudText();
|
||||||
GameManager::setSharedData("gameWon", true);
|
GameManager::setSharedData("gameWon", true);
|
||||||
LOG("Vsi nivoji so zaključeni");
|
LOG("Vsi nivoji so zaključeni");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,13 +33,29 @@ namespace Game::AGame {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (GameManager::getSharedData<bool>("gameLost")) {
|
if (GameManager::getSharedData<bool>("gameLost")) {
|
||||||
setText("Umrl si!");
|
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();
|
anchorTopRight();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (GameManager::getSharedData<bool>("gameWon")) {
|
if (GameManager::getSharedData<bool>("gameWon")) {
|
||||||
setText("Zmagal si!");
|
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();
|
anchorTopRight();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -53,7 +69,10 @@ namespace Game::AGame {
|
|||||||
<< " | Smeti " << GameManager::getSharedData<int>("trashActiveCount")
|
<< " | Smeti " << GameManager::getSharedData<int>("trashActiveCount")
|
||||||
<< " | Sovražniki " << GameManager::getSharedData<int>("enemyActiveCount");
|
<< " | Sovražniki " << GameManager::getSharedData<int>("enemyActiveCount");
|
||||||
|
|
||||||
setText(stream.str());
|
const std::string newHudText = stream.str();
|
||||||
|
if (getText() != newHudText) {
|
||||||
|
Window::Window::postToMainThread([this, newHudText]() { setText(newHudText); });
|
||||||
|
}
|
||||||
anchorTopRight();
|
anchorTopRight();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,8 +162,9 @@ namespace Game::AGame {
|
|||||||
setTexture(mGroundTex);
|
setTexture(mGroundTex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push replay
|
// Push replay (position and form state)
|
||||||
GameManager::pushPlayerPosition(mTransform);
|
GameManager::pushPlayerPosition(mTransform);
|
||||||
|
GameManager::pushPlayerFormState(mIsShipMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::onCollisionEnter(Object::Entity* other) {
|
void Player::onCollisionEnter(Object::Entity* other) {
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
#include <game/gamemanager.hpp>
|
#include <game/gamemanager.hpp>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <game/agame/player.hpp>
|
||||||
|
#include <state/gamestate.hpp>
|
||||||
|
|
||||||
namespace Game {
|
namespace Game {
|
||||||
GameStateEnum GameManager::mCurrentGameState = GameStateEnum::RUNNING;
|
GameStateEnum GameManager::mCurrentGameState = GameStateEnum::RUNNING;
|
||||||
|
std::vector<GameManager::ReplayFrame> GameManager::mReplayFrames;
|
||||||
|
size_t GameManager::mCurrentReplayFrame = 0;
|
||||||
|
|
||||||
void GameManager::run(std::stop_token stopToken) {
|
void GameManager::run(std::stop_token stopToken) {
|
||||||
using namespace std::chrono_literals;
|
using namespace std::chrono_literals;
|
||||||
@@ -27,19 +33,30 @@ namespace Game {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
Input::update(); // Update input states at the start of each frame
|
Input::update(); // Update input states at the start of each frame
|
||||||
auto entities = State::GameState::getInstance().getEntitiesSnapshot();
|
|
||||||
for (auto* entity : entities) {
|
// Handle REPLAY state
|
||||||
if (!entity || !entity->isActive()) {
|
if (mCurrentGameState == GameStateEnum::REPLAY) {
|
||||||
continue;
|
if (!playReplayFrame()) {
|
||||||
|
// No more frames - end replay and pause
|
||||||
|
stopReplayMode();
|
||||||
|
mCurrentGameState = GameStateEnum::PAUSED;
|
||||||
|
LOG("Replay finished - pausing");
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
auto entities = State::GameState::getInstance().getEntitiesSnapshot();
|
||||||
|
for (auto* entity : entities) {
|
||||||
|
if (!entity || !entity->isActive()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Update components first
|
// Update components first
|
||||||
entity->updateComponents(seconds);
|
entity->updateComponents(seconds);
|
||||||
if (!entity->isActive()) {
|
if (!entity->isActive()) {
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
entity->update(seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
entity->update(seconds);
|
|
||||||
}
|
}
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
ERROR("Exception in GameManager thread: " << e.what());
|
ERROR("Exception in GameManager thread: " << e.what());
|
||||||
@@ -68,6 +85,7 @@ namespace Game {
|
|||||||
std::unordered_map<std::string, float> GameManager::mSharedFloats;
|
std::unordered_map<std::string, float> GameManager::mSharedFloats;
|
||||||
std::unordered_map<std::string, bool> GameManager::mSharedBools;
|
std::unordered_map<std::string, bool> GameManager::mSharedBools;
|
||||||
std::vector<Object::Transform> GameManager::mPlayerTransformHistory;
|
std::vector<Object::Transform> GameManager::mPlayerTransformHistory;
|
||||||
|
std::vector<bool> GameManager::mPlayerFormHistory;
|
||||||
|
|
||||||
void GameManager::removeSharedData(const std::string& key, SharedDataType type) {
|
void GameManager::removeSharedData(const std::string& key, SharedDataType type) {
|
||||||
if (type == SharedDataType::STRING) {
|
if (type == SharedDataType::STRING) {
|
||||||
@@ -84,5 +102,80 @@ namespace Game {
|
|||||||
void GameManager::processPendingEntityRemovals() {
|
void GameManager::processPendingEntityRemovals() {
|
||||||
State::GameState::getInstance().processPendingRemovals();
|
State::GameState::getInstance().processPendingRemovals();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool GameManager::initReplayMode() {
|
||||||
|
// Read and parse replay.txt (supports both 5-column legacy and 6-column new format)
|
||||||
|
std::ifstream replayFile("replay.txt");
|
||||||
|
if (!replayFile.is_open()) {
|
||||||
|
WARN("Failed to open replay.txt for reading");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mReplayFrames.clear();
|
||||||
|
mCurrentReplayFrame = 0;
|
||||||
|
std::string line;
|
||||||
|
|
||||||
|
while (std::getline(replayFile, line)) {
|
||||||
|
if (line.empty()) continue;
|
||||||
|
|
||||||
|
std::istringstream iss(line);
|
||||||
|
ReplayFrame frame;
|
||||||
|
int isShip = 0;
|
||||||
|
|
||||||
|
// Try to parse 6 columns first (new format with form state)
|
||||||
|
if (iss >> frame.x >> frame.y >> frame.rotation >> frame.scaleX >> frame.scaleY >> isShip) {
|
||||||
|
frame.isShipMode = (isShip != 0);
|
||||||
|
mReplayFrames.push_back(frame);
|
||||||
|
} else {
|
||||||
|
// Reset stream and try 5-column legacy format
|
||||||
|
iss.clear();
|
||||||
|
iss.seekg(0);
|
||||||
|
if (iss >> frame.x >> frame.y >> frame.rotation >> frame.scaleX >> frame.scaleY) {
|
||||||
|
frame.isShipMode = false; // Default to ground mode for legacy replays
|
||||||
|
mReplayFrames.push_back(frame);
|
||||||
|
} else {
|
||||||
|
WARN("Failed to parse replay line: " << line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
replayFile.close();
|
||||||
|
LOG("Loaded " << mReplayFrames.size() << " replay frames");
|
||||||
|
return !mReplayFrames.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GameManager::playReplayFrame() {
|
||||||
|
if (mReplayFrames.empty() || mCurrentReplayFrame >= mReplayFrames.size()) {
|
||||||
|
LOG("Replay complete: played " << mCurrentReplayFrame << " frames");
|
||||||
|
return false; // Replay finished
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReplayFrame& frame = mReplayFrames[mCurrentReplayFrame];
|
||||||
|
|
||||||
|
// Apply frame to player
|
||||||
|
auto* player = dynamic_cast<AGame::Player*>(State::GameState::getInstance().getEntityByName("Player"));
|
||||||
|
if (player) {
|
||||||
|
player->getTransform()->x = frame.x;
|
||||||
|
player->getTransform()->y = frame.y;
|
||||||
|
player->getTransform()->rotation = frame.rotation;
|
||||||
|
player->getTransform()->scaleX = frame.scaleX;
|
||||||
|
player->getTransform()->scaleY = frame.scaleY;
|
||||||
|
player->setShipMode(frame.isShipMode);
|
||||||
|
|
||||||
|
// Log every 100 frames to track progress
|
||||||
|
if (mCurrentReplayFrame % 100 == 0) {
|
||||||
|
LOG("Playing replay frame " << mCurrentReplayFrame << "/" << mReplayFrames.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mCurrentReplayFrame++;
|
||||||
|
return mCurrentReplayFrame < mReplayFrames.size(); // Return true if more frames exist
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameManager::stopReplayMode() {
|
||||||
|
mReplayFrames.clear();
|
||||||
|
mCurrentReplayFrame = 0;
|
||||||
|
mCurrentGameState = GameStateEnum::STOPPED;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,35 +1,103 @@
|
|||||||
#include <game/input.hpp>
|
#include <game/input.hpp>
|
||||||
|
#include <window/window.hpp>
|
||||||
|
#include <object/ui/uitextbox.hpp>
|
||||||
|
#include <state/gamestate.hpp>
|
||||||
|
|
||||||
namespace Game {
|
namespace Game {
|
||||||
const bool* Input::mCurrentKeyStates = nullptr;
|
const bool* Input::mCurrentKeyStates = nullptr;
|
||||||
const bool* Input::mPreviousKeyStates = nullptr;
|
std::vector<Uint8> Input::mPreviousKeyStates = {};
|
||||||
int Input::mNumKeys = 0;
|
int Input::mNumKeys = 0;
|
||||||
|
int Input::mPrevNumKeys = 0;
|
||||||
SDL_MouseButtonFlags Input::mCurrentMouseButtonStates = 0;
|
SDL_MouseButtonFlags Input::mCurrentMouseButtonStates = 0;
|
||||||
SDL_MouseButtonFlags Input::mPreviousMouseButtonStates = 0;
|
SDL_MouseButtonFlags Input::mPreviousMouseButtonStates = 0;
|
||||||
float Input::mMouseX = 0.0f;
|
float Input::mMouseX = 0.0f;
|
||||||
float Input::mMouseY = 0.0f;
|
float Input::mMouseY = 0.0f;
|
||||||
|
std::mutex Input::mTextMutex;
|
||||||
|
std::vector<std::string> Input::mPendingText;
|
||||||
|
|
||||||
void Input::update() {
|
void Input::update() {
|
||||||
mPreviousKeyStates = mCurrentKeyStates;
|
// Copy the previous keyboard state (snapshot) so we can detect just-pressed
|
||||||
|
if (mCurrentKeyStates && mNumKeys > 0) {
|
||||||
|
mPreviousKeyStates.assign(mCurrentKeyStates, mCurrentKeyStates + mNumKeys);
|
||||||
|
mPrevNumKeys = mNumKeys;
|
||||||
|
} else {
|
||||||
|
// If we don't have a previous snapshot, initialize previous vector to zeros with current size
|
||||||
|
if (mNumKeys > 0) mPreviousKeyStates.assign(mNumKeys, 0);
|
||||||
|
mPrevNumKeys = mNumKeys;
|
||||||
|
}
|
||||||
|
|
||||||
mCurrentKeyStates = SDL_GetKeyboardState(&mNumKeys);
|
mCurrentKeyStates = SDL_GetKeyboardState(&mNumKeys);
|
||||||
|
|
||||||
mPreviousMouseButtonStates = mCurrentMouseButtonStates;
|
mPreviousMouseButtonStates = mCurrentMouseButtonStates;
|
||||||
mCurrentMouseButtonStates = SDL_GetMouseState(&mMouseX, &mMouseY);
|
float rawMouseX = 0.0f;
|
||||||
|
float rawMouseY = 0.0f;
|
||||||
|
mCurrentMouseButtonStates = SDL_GetMouseState(&rawMouseX, &rawMouseY);
|
||||||
|
|
||||||
|
// Convert mouse coordinates from real display/window pixels into
|
||||||
|
// centered logical game coordinates (1280x720 world space).
|
||||||
|
static constexpr float LOGICAL_WIDTH = 1280.0f;
|
||||||
|
static constexpr float LOGICAL_HEIGHT = 720.0f;
|
||||||
|
|
||||||
|
float displayW = LOGICAL_WIDTH;
|
||||||
|
float displayH = LOGICAL_HEIGHT;
|
||||||
|
|
||||||
|
SDL_Window* sdlWindow = Window::Window::getSDLWindowBackend();
|
||||||
|
if (sdlWindow) {
|
||||||
|
int windowPixelsW = static_cast<int>(LOGICAL_WIDTH);
|
||||||
|
int windowPixelsH = static_cast<int>(LOGICAL_HEIGHT);
|
||||||
|
SDL_GetWindowSizeInPixels(sdlWindow, &windowPixelsW, &windowPixelsH);
|
||||||
|
|
||||||
|
SDL_DisplayID displayId = SDL_GetDisplayForWindow(sdlWindow);
|
||||||
|
const SDL_DisplayMode* displayMode = SDL_GetCurrentDisplayMode(displayId);
|
||||||
|
if (displayMode && displayMode->w > 0 && displayMode->h > 0) {
|
||||||
|
displayW = static_cast<float>(displayMode->w);
|
||||||
|
displayH = static_cast<float>(displayMode->h);
|
||||||
|
} else if (windowPixelsW > 0 && windowPixelsH > 0) {
|
||||||
|
displayW = static_cast<float>(windowPixelsW);
|
||||||
|
displayH = static_cast<float>(windowPixelsH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const float invScaleX = LOGICAL_WIDTH / displayW;
|
||||||
|
const float invScaleY = LOGICAL_HEIGHT / displayH;
|
||||||
|
mMouseX = rawMouseX * invScaleX - (LOGICAL_WIDTH * 0.5f);
|
||||||
|
mMouseY = rawMouseY * invScaleY - (LOGICAL_HEIGHT * 0.5f);
|
||||||
|
|
||||||
|
// Consume and deliver queued text input from window thread to focused textbox
|
||||||
|
std::vector<std::string> texts;
|
||||||
|
consumeText(texts);
|
||||||
|
if (!texts.empty()) {
|
||||||
|
auto entities = State::GameState::getInstance().getEntitiesSnapshot();
|
||||||
|
for (auto* e : entities) {
|
||||||
|
if (!e) continue;
|
||||||
|
auto* tb = dynamic_cast<Game::Object::UITextBox*>(e);
|
||||||
|
if (tb && tb->isFocused()) {
|
||||||
|
for (auto& s : texts) {
|
||||||
|
tb->insertText(s);
|
||||||
|
}
|
||||||
|
break; // Deliver to first focused textbox only
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Input::isKeyPressed(SDL_Scancode key) {
|
bool Input::isKeyPressed(SDL_Scancode key) {
|
||||||
if (key < 0 || key >= mNumKeys) return false;
|
if (key < 0 || key >= mNumKeys) return false;
|
||||||
return mCurrentKeyStates[key];
|
return mCurrentKeyStates && mCurrentKeyStates[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Input::isKeyJustPressed(SDL_Scancode key) {
|
bool Input::isKeyJustPressed(SDL_Scancode key) {
|
||||||
if (key < 0 || key >= mNumKeys) return false;
|
if (key < 0 || key >= mNumKeys) return false;
|
||||||
return mCurrentKeyStates[key] && (!mPreviousKeyStates || !mPreviousKeyStates[key]);
|
bool cur = mCurrentKeyStates && mCurrentKeyStates[key];
|
||||||
|
bool prev = (key < static_cast<int>(mPreviousKeyStates.size())) ? static_cast<bool>(mPreviousKeyStates[key]) : false;
|
||||||
|
return cur && !prev;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Input::isKeyJustReleased(SDL_Scancode key) {
|
bool Input::isKeyJustReleased(SDL_Scancode key) {
|
||||||
if (key < 0 || key >= mNumKeys) return false;
|
if (key < 0 || key >= mNumKeys) return false;
|
||||||
return (!mCurrentKeyStates[key]) && mPreviousKeyStates && mPreviousKeyStates[key];
|
bool cur = mCurrentKeyStates && mCurrentKeyStates[key];
|
||||||
|
bool prev = (key < static_cast<int>(mPreviousKeyStates.size())) ? static_cast<bool>(mPreviousKeyStates[key]) : false;
|
||||||
|
return !cur && prev;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Input::isMouseButtonPressed(Uint8 button) {
|
bool Input::isMouseButtonPressed(Uint8 button) {
|
||||||
@@ -51,4 +119,14 @@ namespace Game {
|
|||||||
float Input::getMouseY() {
|
float Input::getMouseY() {
|
||||||
return mMouseY;
|
return mMouseY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Input::pushText(const std::string& utf8) {
|
||||||
|
std::scoped_lock lock(mTextMutex);
|
||||||
|
mPendingText.push_back(utf8);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Input::consumeText(std::vector<std::string>& out) {
|
||||||
|
std::scoped_lock lock(mTextMutex);
|
||||||
|
out.swap(mPendingText);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
262
src/main.cpp
262
src/main.cpp
@@ -11,46 +11,268 @@
|
|||||||
#include <renderer/texture.hpp>
|
#include <renderer/texture.hpp>
|
||||||
#include <renderer/font.hpp>
|
#include <renderer/font.hpp>
|
||||||
#include <object/ui/uitextbox.hpp>
|
#include <object/ui/uitextbox.hpp>
|
||||||
#include <game/agame/sampletextbox.hpp>
|
#include <object/ui/uibutton.hpp>
|
||||||
|
#include <object/ui/uitext.hpp>
|
||||||
#include <object/components/boxcollider.hpp>
|
#include <object/components/boxcollider.hpp>
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
using namespace Game;
|
using namespace Game;
|
||||||
|
|
||||||
int main() {
|
// Global SDL renderer pointer used by menu callback to create textures/fonts
|
||||||
PLNIMP("Letnik3Zadnja - Licenca: LGPLv2.1-only, CC BY-SA 4.0");
|
static SDL_Renderer* gSDLRenderer = nullptr;
|
||||||
// Prompt for player name before initializing the window/engine
|
static Uint32 gStartGameEventType = static_cast<Uint32>(-1);
|
||||||
std::string playerName;
|
static std::atomic_bool gStartGameQueued = false;
|
||||||
std::cout << "Vnesi uporabniško ime (pusti prazno za 'Igralec'): ";
|
|
||||||
std::getline(std::cin, playerName);
|
namespace {
|
||||||
if (playerName.empty()) playerName = "Igralec";
|
// Create a solid-color texture (e.g., for textbox backgrounds)
|
||||||
|
std::shared_ptr<Game::Renderer::Texture> createSolidColorTexture(SDL_Renderer* renderer, int width, int height, Uint8 r, Uint8 g, Uint8 b, Uint8 a) {
|
||||||
|
// Create an SDL surface filled with the specified color
|
||||||
|
SDL_Surface* surf = SDL_CreateSurface(width, height, SDL_PIXELFORMAT_ARGB8888);
|
||||||
|
if (!surf) return nullptr;
|
||||||
|
|
||||||
|
Uint32 color = SDL_MapSurfaceRGB(surf, r, g, b);
|
||||||
|
SDL_FillSurfaceRect(surf, nullptr, color);
|
||||||
|
|
||||||
|
// Create texture from surface
|
||||||
|
SDL_Texture* tex = SDL_CreateTextureFromSurface(renderer, surf);
|
||||||
|
SDL_DestroySurface(surf);
|
||||||
|
|
||||||
|
if (!tex) return nullptr;
|
||||||
|
|
||||||
|
// Set the texture to blend with alpha so it composites correctly
|
||||||
|
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
|
||||||
|
SDL_SetTextureAlphaMod(tex, a);
|
||||||
|
|
||||||
|
// Wrap the SDL_Texture in a Texture object
|
||||||
|
auto texturePtr = std::make_shared<Game::Renderer::Texture>("textbox_background");
|
||||||
|
texturePtr->setSDLTexture(tex); // Set the texture via protected setter
|
||||||
|
return texturePtr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void performStartGameTransition() {
|
||||||
|
// Read name from the UI textbox
|
||||||
|
auto* tbEntity = dynamic_cast<Game::Object::UITextBox*>(State::GameState::getInstance().getEntityByName("NameBox"));
|
||||||
|
std::string playerName = "Igralec";
|
||||||
|
if (tbEntity) {
|
||||||
|
auto txt = tbEntity->getText();
|
||||||
|
if (!txt.empty()) playerName = txt;
|
||||||
|
}
|
||||||
|
|
||||||
Game::GameManager::setSharedData<std::string>("playerName", playerName);
|
Game::GameManager::setSharedData<std::string>("playerName", playerName);
|
||||||
|
|
||||||
Window::Window window = Window::Window();
|
// Spawn the main game entities now that we have a player name
|
||||||
window.init(1280, 720, "Game Window");
|
// Background
|
||||||
|
State::GameState::getInstance().addEntity(std::make_unique<AGame::Background>("BG", std::make_shared<Game::Renderer::Texture>("../resources/bgtest.png", gSDLRenderer), Object::DEFAULT_TRANSFORM));
|
||||||
State::GameState::getInstance().addEntity(std::make_unique<AGame::Background>("BG", std::make_shared<Game::Renderer::Texture>("../resources/bgtest.png", window.getRenderer()->getSDLRenderer()), Object::DEFAULT_TRANSFORM));
|
|
||||||
|
|
||||||
|
// Player
|
||||||
auto* player = dynamic_cast<AGame::Player*>(
|
auto* player = dynamic_cast<AGame::Player*>(
|
||||||
State::GameState::getInstance().addEntity(
|
State::GameState::getInstance().addEntity(
|
||||||
std::make_unique<AGame::Player>(
|
std::make_unique<AGame::Player>(
|
||||||
"Player",
|
"Player",
|
||||||
std::make_shared<Game::Renderer::Texture>(
|
std::make_shared<Game::Renderer::Texture>("../resources/l3ladja.png", gSDLRenderer),
|
||||||
"../resources/l3ladja.png",
|
|
||||||
window.getRenderer()->getSDLRenderer()
|
|
||||||
),
|
|
||||||
Object::DEFAULT_TRANSFORM
|
Object::DEFAULT_TRANSFORM
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
if (player) {
|
if (player) {
|
||||||
player->addComponent<Object::Components::BoxCollider>();
|
player->addComponent<Object::Components::BoxCollider>();
|
||||||
player->setShipTexture(std::make_shared<Game::Renderer::Texture>("../resources/l3ladja.png", window.getRenderer()->getSDLRenderer()));
|
player->setShipTexture(std::make_shared<Game::Renderer::Texture>("../resources/l3ladja.png", gSDLRenderer));
|
||||||
player->setGroundTexture(std::make_shared<Game::Renderer::Texture>("../resources/l3player.png", window.getRenderer()->getSDLRenderer()));
|
player->setGroundTexture(std::make_shared<Game::Renderer::Texture>("../resources/l3player.png", gSDLRenderer));
|
||||||
}
|
}
|
||||||
|
|
||||||
State::GameState::getInstance().addEntity(std::make_unique<AGame::HUDText>("HUD", std::make_shared<Game::Renderer::Font>("../resources/roboto.ttf", window.getRenderer()->getSDLRenderer(), 60, "HUDFont"), Object::Transform{0.f, 0.f, 0.f, 1.f, 1.f}, 320.f, 40.f));
|
// HUD
|
||||||
|
State::GameState::getInstance().addEntity(std::make_unique<AGame::HUDText>("HUD", std::make_shared<Game::Renderer::Font>("../resources/roboto.ttf", gSDLRenderer, 60, "HUDFont"), Object::Transform{0.f, 0.f, 0.f, 1.f, 1.f}, 320.f, 40.f));
|
||||||
|
|
||||||
|
// Remove menu entities safely at end-of-frame to avoid invalidating
|
||||||
|
// pointers currently being iterated by the game update snapshot.
|
||||||
|
auto deactivateAndQueueRemoval = [](const std::string& entityName) {
|
||||||
|
if (auto* entity = State::GameState::getInstance().getEntityByName(entityName)) {
|
||||||
|
entity->setActive(false);
|
||||||
|
State::GameState::getInstance().queueEntityRemoval(entityName);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
deactivateAndQueueRemoval("Title");
|
||||||
|
deactivateAndQueueRemoval("NameBox");
|
||||||
|
deactivateAndQueueRemoval("StartButton");
|
||||||
|
deactivateAndQueueRemoval("ReplayButton");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void performReplayTransition() {
|
||||||
|
// Clear all existing entities to start fresh
|
||||||
|
auto allEntities = State::GameState::getInstance().getEntitiesSnapshot();
|
||||||
|
for (auto* entity : allEntities) {
|
||||||
|
if (entity && entity->getName() != "BG" && entity->getName() != "Player" && entity->getName() != "HUD") {
|
||||||
|
entity->setActive(false);
|
||||||
|
State::GameState::getInstance().queueEntityRemoval(entity->getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawn minimal game entities for replay (only Background and Player, no HUD)
|
||||||
|
State::GameState::getInstance().addEntity(std::make_unique<AGame::Background>("BG", std::make_shared<Game::Renderer::Texture>("../resources/bgtest.png", gSDLRenderer), Object::DEFAULT_TRANSFORM));
|
||||||
|
|
||||||
|
auto* player = dynamic_cast<AGame::Player*>(
|
||||||
|
State::GameState::getInstance().addEntity(
|
||||||
|
std::make_unique<AGame::Player>(
|
||||||
|
"Player",
|
||||||
|
std::make_shared<Game::Renderer::Texture>("../resources/l3ladja.png", gSDLRenderer),
|
||||||
|
Object::DEFAULT_TRANSFORM
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (player) {
|
||||||
|
player->addComponent<Object::Components::BoxCollider>();
|
||||||
|
player->setShipTexture(std::make_shared<Game::Renderer::Texture>("../resources/l3ladja.png", gSDLRenderer));
|
||||||
|
player->setGroundTexture(std::make_shared<Game::Renderer::Texture>("../resources/l3player.png", gSDLRenderer));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize and start replay
|
||||||
|
if (GameManager::initReplayMode()) {
|
||||||
|
GameManager::setCurrentGameState(GameStateEnum::REPLAY);
|
||||||
|
LOG("Replay mode started");
|
||||||
|
} else {
|
||||||
|
WARN("Failed to initialize replay mode");
|
||||||
|
GameManager::setCurrentGameState(GameStateEnum::STOPPED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove menu entities
|
||||||
|
auto deactivateAndQueueRemoval = [](const std::string& entityName) {
|
||||||
|
if (auto* entity = State::GameState::getInstance().getEntityByName(entityName)) {
|
||||||
|
entity->setActive(false);
|
||||||
|
State::GameState::getInstance().queueEntityRemoval(entityName);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
deactivateAndQueueRemoval("Title");
|
||||||
|
deactivateAndQueueRemoval("NameBox");
|
||||||
|
deactivateAndQueueRemoval("StartButton");
|
||||||
|
deactivateAndQueueRemoval("ReplayButton");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void startGameCallback() {
|
||||||
|
// UIButton clicks are handled on the game thread. Queue a custom SDL user
|
||||||
|
// event so entity/texture creation happens on the window/event thread.
|
||||||
|
bool alreadyQueued = gStartGameQueued.exchange(true);
|
||||||
|
if (alreadyQueued) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gStartGameEventType == static_cast<Uint32>(-1)) {
|
||||||
|
// Fallback if custom events are unavailable.
|
||||||
|
performStartGameTransition();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Event event{};
|
||||||
|
event.type = gStartGameEventType;
|
||||||
|
event.user.data1 = reinterpret_cast<void*>(&performStartGameTransition);
|
||||||
|
if (!SDL_PushEvent(&event)) {
|
||||||
|
WARN("Failed to push start-game user event: " << SDL_GetError());
|
||||||
|
performStartGameTransition();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void replayGameCallback() {
|
||||||
|
// UIButton clicks are handled on the game thread. Queue a custom SDL user
|
||||||
|
// event so entity/texture creation happens on the window/event thread.
|
||||||
|
bool alreadyQueued = gStartGameQueued.exchange(true);
|
||||||
|
if (alreadyQueued) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gStartGameEventType == static_cast<Uint32>(-1)) {
|
||||||
|
// Fallback if custom events are unavailable.
|
||||||
|
performReplayTransition();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Event event{};
|
||||||
|
event.type = gStartGameEventType;
|
||||||
|
event.user.data1 = reinterpret_cast<void*>(&performReplayTransition);
|
||||||
|
if (!SDL_PushEvent(&event)) {
|
||||||
|
WARN("Failed to push replay user event: " << SDL_GetError());
|
||||||
|
performReplayTransition();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
PLNIMP("Letnik3Zadnja - Licenca: LGPLv2.1-only, CC BY-SA 4.0");
|
||||||
|
|
||||||
|
Window::Window window = Window::Window();
|
||||||
|
window.init(1280, 720, "Game Window");
|
||||||
|
|
||||||
|
// Make SDL renderer available to callbacks
|
||||||
|
gSDLRenderer = window.getRenderer()->getSDLRenderer();
|
||||||
|
|
||||||
|
// Register a custom event used to perform menu -> game transition on the
|
||||||
|
// window/event thread.
|
||||||
|
gStartGameEventType = SDL_RegisterEvents(1);
|
||||||
|
|
||||||
|
// Separate fonts for each UI element (each font owns its own texture).
|
||||||
|
// Reusing one font object would make all UI labels share one texture.
|
||||||
|
auto titleFont = std::make_shared<Game::Renderer::Font>("../resources/roboto.ttf", gSDLRenderer, 72, "TitleFont");
|
||||||
|
auto nameBoxFont = std::make_shared<Game::Renderer::Font>("../resources/roboto.ttf", gSDLRenderer, 58, "NameBoxFont");
|
||||||
|
auto startButtonFont = std::make_shared<Game::Renderer::Font>("../resources/roboto.ttf", gSDLRenderer, 72, "StartButtonFont");
|
||||||
|
auto replayButtonFont = std::make_shared<Game::Renderer::Font>("../resources/roboto.ttf", gSDLRenderer, 60, "ReplayButtonFont");
|
||||||
|
|
||||||
|
// Determine display resolution and convert to centered logical coordinates for UI layout
|
||||||
|
static constexpr float LOGICAL_WIDTH = 1280.0f;
|
||||||
|
static constexpr float LOGICAL_HEIGHT = 720.0f;
|
||||||
|
|
||||||
|
float displayW = LOGICAL_WIDTH;
|
||||||
|
float displayH = LOGICAL_HEIGHT;
|
||||||
|
|
||||||
|
SDL_Window* sdlWin = Window::Window::getSDLWindowBackend();
|
||||||
|
if (sdlWin) {
|
||||||
|
int windowPixelsW = static_cast<int>(LOGICAL_WIDTH);
|
||||||
|
int windowPixelsH = static_cast<int>(LOGICAL_HEIGHT);
|
||||||
|
SDL_GetWindowSizeInPixels(sdlWin, &windowPixelsW, &windowPixelsH);
|
||||||
|
|
||||||
|
SDL_DisplayID displayId = SDL_GetDisplayForWindow(sdlWin);
|
||||||
|
const SDL_DisplayMode* displayMode = SDL_GetCurrentDisplayMode(displayId);
|
||||||
|
if (displayMode && displayMode->w > 0 && displayMode->h > 0) {
|
||||||
|
displayW = static_cast<float>(displayMode->w);
|
||||||
|
displayH = static_cast<float>(displayMode->h);
|
||||||
|
} else if (windowPixelsW > 0 && windowPixelsH > 0) {
|
||||||
|
displayW = static_cast<float>(windowPixelsW);
|
||||||
|
displayH = static_cast<float>(windowPixelsH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const float centerDisplayX = displayW * 0.5f;
|
||||||
|
const float centerDisplayY = displayH * 0.5f;
|
||||||
|
const float cx = centerDisplayX * (LOGICAL_WIDTH / displayW) - (LOGICAL_WIDTH * 0.5f);
|
||||||
|
const float cy = centerDisplayY * (LOGICAL_HEIGHT / displayH) - (LOGICAL_HEIGHT * 0.5f);
|
||||||
|
|
||||||
|
LOG("cx: " << cx << ", cy: " << cy);
|
||||||
|
|
||||||
|
// Position UI elements relative to center (cx, cy are center offsets in logical space)
|
||||||
|
const float titleY = cy - 160.f; // Title above center
|
||||||
|
const float textboxY = cy; // Textbox at center
|
||||||
|
const float startButtonY = cy + 120.f; // Start button below center
|
||||||
|
const float replayButtonY = cy + 220.f; // Replay button below start button
|
||||||
|
|
||||||
|
// 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("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("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("Začni igro");
|
||||||
|
|
||||||
|
// Replay button (below start button)
|
||||||
|
auto replayBtnTex = std::dynamic_pointer_cast<Game::Renderer::Texture>(replayButtonFont);
|
||||||
|
auto* replayBtnEntity = dynamic_cast<Game::Object::UIButton*>(State::GameState::getInstance().addEntity(std::make_unique<Game::Object::UIButton>("ReplayButton", replayBtnTex, Object::DEFAULT_TRANSFORM, reinterpret_cast<void*>(&replayGameCallback), cx, replayButtonY)));
|
||||||
|
if (replayBtnEntity) replayBtnEntity->setText("Replay");
|
||||||
|
|
||||||
window.run();
|
window.run();
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#include <object/ui/uibutton.hpp>
|
#include <object/ui/uibutton.hpp>
|
||||||
|
#include <window/window.hpp>
|
||||||
|
|
||||||
namespace Game::Object {
|
namespace Game::Object {
|
||||||
UIButton::UIButton(const std::string& name, std::shared_ptr<Renderer::Texture> texture, const Transform& transform, void* clickFunction, float x, float y)
|
UIButton::UIButton(const std::string& name, std::shared_ptr<Renderer::Texture> texture, const Transform& transform, void* clickFunction, float x, float y)
|
||||||
@@ -19,8 +20,21 @@ namespace Game::Object {
|
|||||||
float textTop = mTransform.y;
|
float textTop = mTransform.y;
|
||||||
float textBottom = mTransform.y + mTex->getHeight() * mTransform.adjustedScaleY();
|
float textBottom = mTransform.y + mTex->getHeight() * mTransform.adjustedScaleY();
|
||||||
|
|
||||||
if (mouseX >= textLeft && mouseX <= textRight && mouseY >= textTop && mouseY <= textBottom) {
|
const bool isInside = mouseX >= textLeft && mouseX <= textRight && mouseY >= textTop && mouseY <= textBottom;
|
||||||
std::dynamic_pointer_cast<Renderer::Font>(mTex)->build({200, 200, 200, 255}, mText); // Darken text when hovered
|
|
||||||
|
if (isInside && !mIsHovered) {
|
||||||
|
mIsHovered = true;
|
||||||
|
auto fontPtr = std::dynamic_pointer_cast<Renderer::Font>(mTex);
|
||||||
|
std::string txt = mText;
|
||||||
|
if (fontPtr) Window::Window::postToMainThread([fontPtr, txt]() { fontPtr->build({200,200,200,255}, txt); });
|
||||||
|
} else if (!isInside && mIsHovered) {
|
||||||
|
mIsHovered = false;
|
||||||
|
auto fontPtr = std::dynamic_pointer_cast<Renderer::Font>(mTex);
|
||||||
|
std::string txt = mText;
|
||||||
|
if (fontPtr) Window::Window::postToMainThread([fontPtr, txt]() { fontPtr->build({255,255,255,255}, txt); });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInside) {
|
||||||
if (Input::isMouseButtonJustPressed(SDL_BUTTON_LEFT) && mClickFunction) {
|
if (Input::isMouseButtonJustPressed(SDL_BUTTON_LEFT) && mClickFunction) {
|
||||||
using ClickFnType = void(*)();
|
using ClickFnType = void(*)();
|
||||||
ClickFnType clickFn = reinterpret_cast<ClickFnType>(mClickFunction);
|
ClickFnType clickFn = reinterpret_cast<ClickFnType>(mClickFunction);
|
||||||
@@ -32,7 +46,9 @@ namespace Game::Object {
|
|||||||
|
|
||||||
void UIButton::setText(const std::string& text) {
|
void UIButton::setText(const std::string& text) {
|
||||||
mText = text;
|
mText = text;
|
||||||
std::dynamic_pointer_cast<Renderer::Font>(mTex)->build({255, 255, 255, 255}, text);
|
auto fontPtr = std::dynamic_pointer_cast<Renderer::Font>(mTex);
|
||||||
|
std::string txt = text;
|
||||||
|
if (fontPtr) Window::Window::postToMainThread([fontPtr, txt]() { fontPtr->build({255,255,255,255}, txt); });
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string UIButton::getText() const {
|
std::string UIButton::getText() const {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include <object/ui/uitext.hpp>
|
#include <object/ui/uitext.hpp>
|
||||||
|
#include <window/window.hpp>
|
||||||
|
|
||||||
namespace Game::Object {
|
namespace Game::Object {
|
||||||
UIText::UIText(const std::string& name, std::shared_ptr<Renderer::Font> font, const Transform& transform, float x, float y)
|
UIText::UIText(const std::string& name, std::shared_ptr<Renderer::Font> font, const Transform& transform, float x, float y)
|
||||||
@@ -31,7 +32,15 @@ namespace Game::Object {
|
|||||||
|
|
||||||
void UIText::setText(const std::string& text) {
|
void UIText::setText(const std::string& text) {
|
||||||
mText = text;
|
mText = text;
|
||||||
std::dynamic_pointer_cast<Renderer::Font>(mTex)->build({255, 255, 255, 255}, text);
|
// Schedule font texture build on the window thread to avoid
|
||||||
|
// creating/destroying renderer resources from the game thread.
|
||||||
|
auto fontPtr = std::dynamic_pointer_cast<Renderer::Font>(mTex);
|
||||||
|
if (fontPtr) {
|
||||||
|
std::string t = text;
|
||||||
|
Window::Window::postToMainThread([fontPtr, t]() mutable {
|
||||||
|
fontPtr->build({255,255,255,255}, t);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string UIText::getText() const {
|
std::string UIText::getText() const {
|
||||||
|
|||||||
@@ -1,164 +1,297 @@
|
|||||||
#include <object/ui/uitextbox.hpp>
|
#include <object/ui/uitextbox.hpp>
|
||||||
#include <renderer/renderer.hpp>
|
#include <renderer/renderer.hpp>
|
||||||
#include <object/camera.hpp>
|
#include <window/window.hpp>
|
||||||
|
|
||||||
namespace Game::Object {
|
namespace Game::Object {
|
||||||
|
UITextBox::UITextBox(const std::string& name, std::shared_ptr<Renderer::Texture> background, std::shared_ptr<Renderer::Font> font, const Transform& transform, float x, float y)
|
||||||
UITextBox::UITextBox(const std::string& name, std::shared_ptr<Renderer::Font> font,
|
: Entity(name, background, transform), mBackground(background), mFont(font), mX(x), mY(y) {
|
||||||
const Transform& transform, float x, float y, UITextboxConfig config)
|
if (!mBackground) {
|
||||||
: Entity(name, font, transform), mX(x), mY(y), mConfig(config) { }
|
// Fallback to font texture if background is not provided
|
||||||
|
mBackground = std::dynamic_pointer_cast<Renderer::Texture>(mFont);
|
||||||
|
setTexture(mBackground);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void UITextBox::start() {
|
void UITextBox::start() {
|
||||||
// Compute visual box size first (respecting min sizes and scale), then center the box
|
// Build initial text texture (placeholder if present) so textbox has a
|
||||||
mBoxWidth = static_cast<float>(mTex ? mTex->getWidth() : 0.f) * mTransform.adjustedScaleX();
|
// visible indicator and stable dimensions for click hit-testing.
|
||||||
mBoxHeight = static_cast<float>(mTex ? mTex->getHeight() : 0.f) * mTransform.adjustedScaleY();
|
const std::string initialDisplay = (mText.empty() && !mPlaceholder.empty()) ? mPlaceholder : (mPasswordMode ? std::string(mText.size(), '*') : mText);
|
||||||
|
if (mFont && !initialDisplay.empty()) {
|
||||||
|
auto fontPtr = mFont;
|
||||||
|
std::string txt = initialDisplay;
|
||||||
|
Window::Window::postToMainThread([fontPtr, txt]() { fontPtr->build({255,255,255,255}, txt); });
|
||||||
|
mLastRenderedText = initialDisplay;
|
||||||
|
}
|
||||||
|
|
||||||
if (mBoxWidth < mConfig.minWidth) mBoxWidth = mConfig.minWidth;
|
// Center the textbox on the requested position
|
||||||
if (mBoxHeight < mConfig.minHeight) mBoxHeight = mConfig.minHeight;
|
if (mBackground) {
|
||||||
|
mTransform.x = mX - mBackground->getWidth() * mTransform.adjustedScaleX() / 2.f;
|
||||||
|
mTransform.y = mY - mBackground->getHeight() * mTransform.adjustedScaleY() / 2.f;
|
||||||
|
}
|
||||||
|
// Ensure cursor index is valid
|
||||||
|
mCursorIndex = std::min(mCursorIndex, mText.size());
|
||||||
|
}
|
||||||
|
|
||||||
// Center using the computed box dimensions rather than raw texture size so padding/min sizes are respected
|
static char scancodeToChar(SDL_Scancode scancode, bool shift) {
|
||||||
mTransform.x = mX - mBoxWidth / 2.f;
|
// Basic mapping for letters, digits and common symbols
|
||||||
mTransform.y = mY - mBoxHeight / 2.f;
|
if (scancode >= SDL_SCANCODE_A && scancode <= SDL_SCANCODE_Z) {
|
||||||
|
char c = 'a' + (scancode - SDL_SCANCODE_A);
|
||||||
refreshVisualText();
|
if (shift) c = static_cast<char>(std::toupper(c));
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
if (scancode >= SDL_SCANCODE_1 && scancode <= SDL_SCANCODE_0) {
|
||||||
|
// Note: SDL_SCANCODE_1..SDL_SCANCODE_0 is not contiguous for '0', handle digits separately
|
||||||
|
}
|
||||||
|
switch (scancode) {
|
||||||
|
case SDL_SCANCODE_SPACE: return ' ';
|
||||||
|
case SDL_SCANCODE_COMMA: return shift ? '<' : ',';
|
||||||
|
case SDL_SCANCODE_PERIOD: return shift ? '>' : '.';
|
||||||
|
case SDL_SCANCODE_MINUS: return shift ? '_' : '-';
|
||||||
|
case SDL_SCANCODE_EQUALS: return shift ? '+' : '=';
|
||||||
|
case SDL_SCANCODE_SEMICOLON: return shift ? ':' : ';';
|
||||||
|
case SDL_SCANCODE_APOSTROPHE: return shift ? '"' : '\'';
|
||||||
|
case SDL_SCANCODE_SLASH: return shift ? '?' : '/';
|
||||||
|
case SDL_SCANCODE_BACKSLASH: return shift ? '|' : '\\';
|
||||||
|
case SDL_SCANCODE_LEFTBRACKET: return shift ? '{' : '[';
|
||||||
|
case SDL_SCANCODE_RIGHTBRACKET: return shift ? '}' : ']';
|
||||||
|
case SDL_SCANCODE_GRAVE: return shift ? '~' : '`';
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
// Digits
|
||||||
|
if (scancode >= SDL_SCANCODE_1 && scancode <= SDL_SCANCODE_9) {
|
||||||
|
const char numsShift[] = {')','!','@','#','$','%','^','&','*'};
|
||||||
|
int idx = scancode - SDL_SCANCODE_1;
|
||||||
|
return shift ? numsShift[idx] : ('1' + idx);
|
||||||
|
}
|
||||||
|
if (scancode == SDL_SCANCODE_0) return shift ? ')' : '0';
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITextBox::update(float deltaTime) {
|
void UITextBox::update(float deltaTime) {
|
||||||
if (!mIsActive) return;
|
if (!mIsActive) return;
|
||||||
|
|
||||||
if (Input::isMouseButtonJustPressed(SDL_BUTTON_LEFT)) {
|
// Cursor blink
|
||||||
mIsFocused = isMouseInsideBox();
|
mCursorBlinkTimer += deltaTime;
|
||||||
mNeedsTextRefresh = true;
|
if (mCursorBlinkTimer >= 0.5f) {
|
||||||
|
mShowCursor = !mShowCursor;
|
||||||
|
mCursorBlinkTimer = 0.f;
|
||||||
|
}
|
||||||
|
|
||||||
|
float mouseX = Input::getMouseX();
|
||||||
|
float mouseY = Input::getMouseY();
|
||||||
|
float left = mTransform.x;
|
||||||
|
float right = mTransform.x + (mBackground ? mBackground->getWidth() * mTransform.adjustedScaleX() : 0.f);
|
||||||
|
float top = mTransform.y;
|
||||||
|
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)
|
||||||
|
Window::Window::postToMainThread([]() {
|
||||||
|
SDL_Window* win = Window::Window::getSDLWindowBackend();
|
||||||
|
if (win) SDL_StartTextInput(win);
|
||||||
|
});
|
||||||
|
if (mOnFocus) {
|
||||||
|
using FnType = void(*)();
|
||||||
|
FnType fn = reinterpret_cast<FnType>(mOnFocus);
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
} else if (Input::isMouseButtonJustPressed(SDL_BUTTON_LEFT) && !inside) {
|
||||||
|
mIsFocused = false;
|
||||||
|
// Disable SDL text input on main thread
|
||||||
|
Window::Window::postToMainThread([]() {
|
||||||
|
SDL_Window* win = Window::Window::getSDLWindowBackend();
|
||||||
|
if (win) SDL_StopTextInput(win);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mIsFocused) {
|
if (mIsFocused) {
|
||||||
// Cursor blink
|
// Handle special keys
|
||||||
mCursorTimer += deltaTime;
|
if (Input::isKeyJustPressed(SDL_SCANCODE_BACKSPACE)) {
|
||||||
if (mCursorTimer >= mConfig.cursorBlinkRate) {
|
if (mCursorIndex > 0 && !mText.empty()) {
|
||||||
mCursorTimer = 0.f;
|
mText.erase(mCursorIndex - 1, 1);
|
||||||
mCursorVisible = !mCursorVisible;
|
mCursorIndex = std::max<size_t>(0, mCursorIndex - 1);
|
||||||
mNeedsTextRefresh = true;
|
}
|
||||||
|
}
|
||||||
|
if (Input::isKeyJustPressed(SDL_SCANCODE_LEFT)) {
|
||||||
|
if (mCursorIndex > 0) mCursorIndex--;
|
||||||
|
}
|
||||||
|
if (Input::isKeyJustPressed(SDL_SCANCODE_RIGHT)) {
|
||||||
|
if (mCursorIndex < mText.size()) mCursorIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Input::isKeyJustPressed(SDL_SCANCODE_BACKSPACE) && !mText.empty()) {
|
// Character insertion from scancodes
|
||||||
mText.pop_back();
|
|
||||||
mNeedsTextRefresh = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Input::isKeyJustPressed(SDL_SCANCODE_RETURN) || Input::isKeyJustPressed(SDL_SCANCODE_KP_ENTER)) {
|
bool shift = Input::isKeyPressed(SDL_SCANCODE_LSHIFT) || Input::isKeyPressed(SDL_SCANCODE_RSHIFT);
|
||||||
mIsFocused = false;
|
|
||||||
mCursorTimer = 0.f;
|
|
||||||
mCursorVisible = true;
|
|
||||||
mNeedsTextRefresh = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int key = 0; key < SDL_SCANCODE_COUNT; ++key) {
|
// Letters A-Z
|
||||||
SDL_Scancode scancode = static_cast<SDL_Scancode>(key);
|
for (int sc = SDL_SCANCODE_A; sc <= SDL_SCANCODE_Z; ++sc) {
|
||||||
if (!Input::isKeyJustPressed(scancode)) continue;
|
if (Input::isKeyJustPressed(static_cast<SDL_Scancode>(sc))) {
|
||||||
|
char c = scancodeToChar(static_cast<SDL_Scancode>(sc), shift);
|
||||||
SDL_Keycode keycode = SDL_GetKeyFromScancode(scancode, SDL_GetModState(), true);
|
if (c && mText.size() < mMaxLength) {
|
||||||
if (keycode >= 32 && keycode <= 126) {
|
mText.insert(mCursorIndex, 1, c);
|
||||||
if (mConfig.maxLength == 0 || static_cast<int>(mText.size()) < mConfig.maxLength) {
|
mCursorIndex++;
|
||||||
mText.push_back(static_cast<char>(keycode));
|
|
||||||
mNeedsTextRefresh = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Digits 1-9 and 0
|
||||||
|
for (int sc = SDL_SCANCODE_1; sc <= SDL_SCANCODE_9; ++sc) {
|
||||||
|
if (Input::isKeyJustPressed(static_cast<SDL_Scancode>(sc))) {
|
||||||
|
char c = scancodeToChar(static_cast<SDL_Scancode>(sc), shift);
|
||||||
|
if (c && mText.size() < mMaxLength) {
|
||||||
|
mText.insert(mCursorIndex, 1, c);
|
||||||
|
mCursorIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Input::isKeyJustPressed(SDL_SCANCODE_0)) {
|
||||||
|
char c = scancodeToChar(SDL_SCANCODE_0, shift);
|
||||||
|
if (c && mText.size() < mMaxLength) {
|
||||||
|
mText.insert(mCursorIndex, 1, c);
|
||||||
|
mCursorIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Space and a few other keys
|
||||||
|
if (Input::isKeyJustPressed(SDL_SCANCODE_SPACE)) {
|
||||||
|
if (mText.size() < mMaxLength) {
|
||||||
|
mText.insert(mCursorIndex, 1, ' ');
|
||||||
|
mCursorIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mNeedsTextRefresh) {
|
// Update rendered text only when needed
|
||||||
refreshVisualText();
|
std::string display = mPasswordMode ? std::string(mText.size(), '*') : mText;
|
||||||
|
if (display.empty() && !mIsFocused && !mPlaceholder.empty()) {
|
||||||
|
display = mPlaceholder;
|
||||||
|
}
|
||||||
|
if (display != mLastRenderedText) {
|
||||||
|
if (mFont) {
|
||||||
|
auto fontPtr = mFont;
|
||||||
|
std::string txt = display;
|
||||||
|
Window::Window::postToMainThread([fontPtr, txt]() { fontPtr->build({255,255,255,255}, txt); });
|
||||||
|
mLastRenderedText = display;
|
||||||
|
|
||||||
|
if (mBackground) {
|
||||||
|
mTransform.x = mX - mBackground->getWidth() * mTransform.adjustedScaleX() / 2.f;
|
||||||
|
mTransform.y = mY - mBackground->getHeight() * mTransform.adjustedScaleY() / 2.f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When textbox is focused and display is empty, reserve placeholder width by rendering transparent placeholder
|
||||||
|
// This prevents the textbox from shifting when placeholder disappears on focus
|
||||||
|
if (mIsFocused && display.empty() && !mPlaceholder.empty() && mFont && mReservedPlaceholderWidth == 0.f) {
|
||||||
|
auto fontPtr = mFont;
|
||||||
|
std::string placeholder = mPlaceholder;
|
||||||
|
// Post task to build placeholder with full transparency (alpha=0) to reserve width
|
||||||
|
Window::Window::postToMainThread([fontPtr, placeholder, this]() {
|
||||||
|
fontPtr->build({255,255,255,0}, placeholder); // alpha=0 for transparency
|
||||||
|
// Store the width of the placeholder texture for layout stability
|
||||||
|
float w = static_cast<float>(fontPtr->getWidth());
|
||||||
|
{
|
||||||
|
std::scoped_lock lock(mRenderMutex);
|
||||||
|
mReservedPlaceholderWidth = w;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITextBox::render(Game::Renderer::Renderer* renderer, Game::Renderer::RendererConfig config) {
|
void UITextBox::render(Game::Renderer::Renderer* renderer, Game::Renderer::RendererConfig config) {
|
||||||
if (!mIsVisible) return;
|
if (!mIsActive || !mIsVisible) return;
|
||||||
|
|
||||||
SDL_Renderer* r = renderer->getSDLRenderer();
|
// Render background texture if available
|
||||||
|
if (mBackground) {
|
||||||
|
SDL_FRect dst;
|
||||||
|
SDL_GetTextureSize(mBackground->getSDLTexture(), &dst.w, &dst.h);
|
||||||
|
dst.w *= mTransform.scaleX * UNIVERSAL_SCALE_COEFFICIENT;
|
||||||
|
dst.h *= mTransform.scaleY * UNIVERSAL_SCALE_COEFFICIENT;
|
||||||
|
dst.x = mTransform.x - config.camX + config.screenW / 2.f;
|
||||||
|
dst.y = mTransform.y - config.camY + config.screenH / 2.f;
|
||||||
|
|
||||||
const float bx = mTransform.x - mConfig.paddingX - config.camX + config.screenW / 2.f;
|
SDL_FPoint center;
|
||||||
const float by = mTransform.y - mConfig.paddingY - config.camY + config.screenH / 2.f;
|
center.x = dst.w / 2.f;
|
||||||
const float bw = mBoxWidth + 2.f * mConfig.paddingX;
|
center.y = dst.h / 2.f;
|
||||||
const float bh = mBoxHeight + 2.f * mConfig.paddingY;
|
|
||||||
const float t = mConfig.borderThickness;
|
|
||||||
|
|
||||||
// Background
|
SDL_RenderTextureRotated(
|
||||||
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_BLEND);
|
renderer->getSDLRenderer(),
|
||||||
const SDL_Color& bg = mConfig.bgColor;
|
mBackground->getSDLTexture(),
|
||||||
SDL_SetRenderDrawColor(r, bg.r, bg.g, bg.b, bg.a);
|
nullptr,
|
||||||
const SDL_FRect bgRect = {bx, by, bw, bh};
|
&dst,
|
||||||
SDL_RenderFillRect(r, &bgRect);
|
mTransform.rotation,
|
||||||
|
¢er,
|
||||||
|
mIsFlipped ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE
|
||||||
|
);
|
||||||
|
|
||||||
// Border (4 filled rects for configurable thickness)
|
if (mIsHovered) {
|
||||||
const SDL_Color& bc = mIsFocused ? mConfig.focusedBorderColor : mConfig.borderColor;
|
Uint8 prevR = 0;
|
||||||
SDL_SetRenderDrawColor(r, bc.r, bc.g, bc.b, bc.a);
|
Uint8 prevG = 0;
|
||||||
const SDL_FRect borders[4] = {
|
Uint8 prevB = 0;
|
||||||
{bx, by, bw, t }, // top
|
Uint8 prevA = 0;
|
||||||
{bx, by + bh - t, bw, t }, // bottom
|
SDL_BlendMode prevBlendMode = SDL_BLENDMODE_NONE;
|
||||||
{bx, by, t, bh}, // left
|
|
||||||
{bx + bw - t, by, t, bh}, // right
|
|
||||||
};
|
|
||||||
SDL_RenderFillRects(r, borders, 4);
|
|
||||||
|
|
||||||
// Text (or placeholder) via base render
|
SDL_GetRenderDrawColor(renderer->getSDLRenderer(), &prevR, &prevG, &prevB, &prevA);
|
||||||
Entity::render(renderer, config);
|
SDL_GetRenderDrawBlendMode(renderer->getSDLRenderer(), &prevBlendMode);
|
||||||
}
|
|
||||||
|
|
||||||
void UITextBox::setText(const std::string& text) {
|
SDL_SetRenderDrawBlendMode(renderer->getSDLRenderer(), SDL_BLENDMODE_BLEND);
|
||||||
mText = text;
|
SDL_SetRenderDrawColor(renderer->getSDLRenderer(), 0, 0, 0, 28);
|
||||||
mNeedsTextRefresh = true;
|
SDL_RenderFillRect(renderer->getSDLRenderer(), &dst);
|
||||||
refreshVisualText();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string UITextBox::getText() const { return mText; }
|
SDL_SetRenderDrawColor(renderer->getSDLRenderer(), prevR, prevG, prevB, prevA);
|
||||||
std::string UITextBox::getValue() const { return mText; }
|
SDL_SetRenderDrawBlendMode(renderer->getSDLRenderer(), prevBlendMode);
|
||||||
bool UITextBox::isFocused() const { return mIsFocused; }
|
|
||||||
|
|
||||||
bool UITextBox::isMouseInsideBox() const {
|
|
||||||
// Get screen-space mouse coordinates
|
|
||||||
const float screenMouseX = Input::getMouseX();
|
|
||||||
const float screenMouseY = Input::getMouseY();
|
|
||||||
|
|
||||||
// Get window dimensions
|
|
||||||
int windowW = 0, windowH = 0;
|
|
||||||
SDL_GetWindowSizeInPixels(SDL_GetMouseFocus(), &windowW, &windowH);
|
|
||||||
|
|
||||||
// Get camera position
|
|
||||||
float camX = 0.f, camY = 0.f;
|
|
||||||
Object::Camera::getInstance().getPosition(camX, camY);
|
|
||||||
|
|
||||||
// Convert screen coordinates to world coordinates
|
|
||||||
const float worldMouseX = screenMouseX - windowW / 2.f + camX;
|
|
||||||
const float worldMouseY = screenMouseY - windowH / 2.f + camY;
|
|
||||||
|
|
||||||
// Check bounds in world space
|
|
||||||
const float left = mTransform.x - mConfig.paddingX;
|
|
||||||
const float right = left + mBoxWidth + 2.f * mConfig.paddingX;
|
|
||||||
const float top = mTransform.y - mConfig.paddingY;
|
|
||||||
const float bottom = top + mBoxHeight + 2.f * mConfig.paddingY;
|
|
||||||
|
|
||||||
return worldMouseX >= left && worldMouseX <= right && worldMouseY >= top && worldMouseY <= bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
void UITextBox::refreshVisualText() {
|
|
||||||
const bool showPlaceholder = mText.empty() && !mIsFocused && !mConfig.placeholder.empty();
|
|
||||||
|
|
||||||
SDL_Color color;
|
|
||||||
std::string rendered;
|
|
||||||
|
|
||||||
if (showPlaceholder) {
|
|
||||||
color = mConfig.placeholderColor;
|
|
||||||
rendered = mConfig.placeholder;
|
|
||||||
} else {
|
|
||||||
color = mIsFocused ? mConfig.focusedTextColor : mConfig.textColor;
|
|
||||||
rendered = mText.empty() ? " " : mText;
|
|
||||||
if (mIsFocused && mCursorVisible) {
|
|
||||||
rendered += "_";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::dynamic_pointer_cast<Renderer::Font>(mTex)->build(color, rendered);
|
// Render font texture (text) on top of background
|
||||||
mNeedsTextRefresh = false;
|
if (mFont && mFont->getSDLTexture()) {
|
||||||
|
SDL_FRect dst;
|
||||||
|
SDL_GetTextureSize(mFont->getSDLTexture(), &dst.w, &dst.h);
|
||||||
|
dst.w *= mTransform.scaleX * UNIVERSAL_SCALE_COEFFICIENT;
|
||||||
|
dst.h *= mTransform.scaleY * UNIVERSAL_SCALE_COEFFICIENT;
|
||||||
|
dst.x = mTransform.x - config.camX + config.screenW / 2.f;
|
||||||
|
dst.y = mTransform.y - config.camY + config.screenH / 2.f;
|
||||||
|
|
||||||
|
SDL_FPoint center;
|
||||||
|
center.x = dst.w / 2.f;
|
||||||
|
center.y = dst.h / 2.f;
|
||||||
|
|
||||||
|
SDL_RenderTextureRotated(
|
||||||
|
renderer->getSDLRenderer(),
|
||||||
|
mFont->getSDLTexture(),
|
||||||
|
nullptr,
|
||||||
|
&dst,
|
||||||
|
mTransform.rotation,
|
||||||
|
¢er,
|
||||||
|
mIsFlipped ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UITextBox::setText(const std::string& text) {
|
||||||
|
mText = text.substr(0, mMaxLength);
|
||||||
|
mCursorIndex = std::min(mText.size(), mCursorIndex);
|
||||||
|
mLastRenderedText.clear(); // force rebuild
|
||||||
|
}
|
||||||
|
|
||||||
|
void UITextBox::insertText(const std::string& utf8) {
|
||||||
|
// Insert UTF-8 text at cursor position, respecting max length
|
||||||
|
if (utf8.empty() || mText.size() >= mMaxLength) return;
|
||||||
|
|
||||||
|
size_t availableSpace = mMaxLength - mText.size();
|
||||||
|
size_t insertLen = std::min(utf8.size(), availableSpace);
|
||||||
|
mText.insert(mCursorIndex, utf8.substr(0, insertLen));
|
||||||
|
mCursorIndex += insertLen;
|
||||||
|
mLastRenderedText.clear(); // force rebuild on next update
|
||||||
|
mShowCursor = true; // Show cursor immediately when text inserted
|
||||||
|
mCursorBlinkTimer = 0.f; // Reset blink timer
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string UITextBox::getText() const {
|
||||||
|
return mText;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
#include <renderer/font.hpp>
|
#include <renderer/font.hpp>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace Game::Renderer {
|
namespace Game::Renderer {
|
||||||
Font::Font(const std::string& path, SDL_Renderer* renderer, int ptSize, std::string id)
|
Font::Font(const std::string& path, SDL_Renderer* renderer, int ptSize, std::string id)
|
||||||
@@ -40,12 +43,106 @@ namespace Game::Renderer {
|
|||||||
mLastText = text;
|
mLastText = text;
|
||||||
mLastColor = color;
|
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) {
|
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;
|
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
|
// Convert to texture
|
||||||
mTex = SDL_CreateTextureFromSurface(mRenderer, surf);
|
mTex = SDL_CreateTextureFromSurface(mRenderer, surf);
|
||||||
SDL_DestroySurface(surf);
|
SDL_DestroySurface(surf);
|
||||||
@@ -54,6 +151,12 @@ namespace Game::Renderer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Text surfaces contain transparency around glyphs. If blending is not
|
||||||
|
// enabled, transparent pixels can appear as opaque black boxes.
|
||||||
|
if (!SDL_SetTextureBlendMode(mTex, SDL_BLENDMODE_BLEND)) {
|
||||||
|
WARN("Failed to set blend mode to BLEND for font '" << mId << "': " << SDL_GetError());
|
||||||
|
}
|
||||||
|
|
||||||
// Fonts look better with linear filtering than with nearest-neighbor scaling.
|
// Fonts look better with linear filtering than with nearest-neighbor scaling.
|
||||||
if (!SDL_SetTextureScaleMode(mTex, SDL_SCALEMODE_LINEAR)) {
|
if (!SDL_SetTextureScaleMode(mTex, SDL_SCALEMODE_LINEAR)) {
|
||||||
WARN("Failed to set texture scale mode to LINEAR for font '" << mId << "': " << SDL_GetError());
|
WARN("Failed to set texture scale mode to LINEAR for font '" << mId << "': " << SDL_GetError());
|
||||||
|
|||||||
@@ -2,9 +2,18 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <renderer/texture.hpp>
|
#include <renderer/texture.hpp>
|
||||||
|
#include <game/input.hpp>
|
||||||
|
|
||||||
namespace Game::Window {
|
namespace Game::Window {
|
||||||
std::mutex Window::sMutex;
|
std::mutex Window::sMutex;
|
||||||
|
// Tasks posted from other threads to run on the window thread
|
||||||
|
std::mutex sTasksMutex;
|
||||||
|
std::vector<std::function<void()>> sPostedTasks;
|
||||||
|
|
||||||
|
void Window::postToMainThread(std::function<void()> fn) {
|
||||||
|
std::scoped_lock lock(sTasksMutex);
|
||||||
|
sPostedTasks.push_back(std::move(fn));
|
||||||
|
}
|
||||||
|
|
||||||
Window::Window() : mWindow(nullptr), mRenderer(), mGameManager(), mRunning(false) { }
|
Window::Window() : mWindow(nullptr), mRenderer(), mGameManager(), mRunning(false) { }
|
||||||
|
|
||||||
@@ -88,9 +97,34 @@ namespace Game::Window {
|
|||||||
SDL_Event event;
|
SDL_Event event;
|
||||||
while (mRunning) {
|
while (mRunning) {
|
||||||
const auto frameStart = std::chrono::steady_clock::now();
|
const auto frameStart = std::chrono::steady_clock::now();
|
||||||
|
// Execute any tasks posted to the main/window thread
|
||||||
|
std::vector<std::function<void()>> tasks;
|
||||||
|
{
|
||||||
|
std::scoped_lock lock(sTasksMutex);
|
||||||
|
tasks.swap(sPostedTasks);
|
||||||
|
}
|
||||||
|
for (auto& t : tasks) {
|
||||||
|
try { t(); } catch (...) { WARN("Exception in posted main-thread task"); }
|
||||||
|
}
|
||||||
|
|
||||||
SDL_PumpEvents();
|
SDL_PumpEvents();
|
||||||
while (SDL_PollEvent(&event)) {
|
while (SDL_PollEvent(&event)) {
|
||||||
|
// Forward SDL text input events to the Input queue
|
||||||
|
if (event.type == SDL_EVENT_TEXT_INPUT) {
|
||||||
|
if (event.text.text && event.text.text[0]) {
|
||||||
|
Game::Input::pushText(std::string(event.text.text));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run custom callbacks on the window/event thread.
|
||||||
|
if (event.type >= SDL_EVENT_USER && event.user.data1) {
|
||||||
|
using UserEventFn = void(*)();
|
||||||
|
UserEventFn fn = reinterpret_cast<UserEventFn>(event.user.data1);
|
||||||
|
fn();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (event.type == SDL_EVENT_QUIT) {
|
if (event.type == SDL_EVENT_QUIT) {
|
||||||
mRunning = false;
|
mRunning = false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user