Compare commits
7 Commits
d9769bdbbb
...
drug
| Author | SHA1 | Date | |
|---|---|---|---|
| ee3c263547 | |||
| 66c5e0e710 | |||
| 6534996a52 | |||
| 0b45643ef2 | |||
| d93e71e716 | |||
| 8ff3e29374 | |||
| 892d8f22eb |
@@ -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)
|
||||||
|
|||||||
5
TODO.txt
Normal file
5
TODO.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Smeti random premikanje - ok
|
||||||
|
vnos imena kot text box
|
||||||
|
neko sledenje igralcu (nasprotnikov)
|
||||||
|
zavezniki naj nekaj delajo
|
||||||
|
fullscreen
|
||||||
@@ -27,7 +27,7 @@ GAME_ENTITY(Background)
|
|||||||
int mPendingLevelStage = 0;
|
int mPendingLevelStage = 0;
|
||||||
// Periodic friendly spawn settings
|
// Periodic friendly spawn settings
|
||||||
float mFriendlySpawnAvgInterval = 6.f; // average seconds between spawns
|
float mFriendlySpawnAvgInterval = 6.f; // average seconds between spawns
|
||||||
int mMaxAutoFriendlies = 7; // hard cap for total active friendlies
|
int mMaxAutoFriendlies = 2; // hard cap for total active friendlies
|
||||||
std::shared_ptr<Game::Renderer::Texture> mSeaTex;
|
std::shared_ptr<Game::Renderer::Texture> mSeaTex;
|
||||||
std::shared_ptr<Game::Renderer::Texture> mEnemyTex;
|
std::shared_ptr<Game::Renderer::Texture> mEnemyTex;
|
||||||
std::shared_ptr<Game::Renderer::Texture> mTrashTex;
|
std::shared_ptr<Game::Renderer::Texture> mTrashTex;
|
||||||
|
|||||||
@@ -16,5 +16,8 @@ namespace Game::AGame {
|
|||||||
float mMoveSpeedY = 0.f;
|
float mMoveSpeedY = 0.f;
|
||||||
float mDirectionChangeTimer = 0.f;
|
float mDirectionChangeTimer = 0.f;
|
||||||
float mShoreSpawnCooldown = 0.f;
|
float mShoreSpawnCooldown = 0.f;
|
||||||
|
bool mFollowingPlayer = false;
|
||||||
|
static constexpr float FOLLOW_DISTANCE = 300.f;
|
||||||
|
static constexpr float FOLLOW_SPEED = 35.f;
|
||||||
END_GAME_ENTITY()
|
END_GAME_ENTITY()
|
||||||
}
|
}
|
||||||
@@ -14,5 +14,11 @@ namespace Game::AGame {
|
|||||||
float mMoveSpeedX = 0.f;
|
float mMoveSpeedX = 0.f;
|
||||||
float mMoveSpeedY = 0.f;
|
float mMoveSpeedY = 0.f;
|
||||||
float mDirectionChangeTimer = 0.f;
|
float mDirectionChangeTimer = 0.f;
|
||||||
|
bool mOnSea = false;
|
||||||
|
static constexpr float CLEANUP_RADIUS = 50.f;
|
||||||
|
static constexpr int CLEANUP_SCORE_BONUS = 5;
|
||||||
|
static constexpr float LAND_SPEED_MIN = 20.f;
|
||||||
|
static constexpr float LAND_SPEED_MAX = 50.f;
|
||||||
|
static constexpr float SEA_SPEED = 14.f;
|
||||||
END_GAME_ENTITY()
|
END_GAME_ENTITY()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -58,6 +59,16 @@ namespace Game {
|
|||||||
static void destroyEntity(T* entity);
|
static void destroyEntity(T* entity);
|
||||||
static void processPendingEntityRemovals();
|
static void processPendingEntityRemovals();
|
||||||
|
|
||||||
|
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 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;
|
||||||
clock::time_point mLastUpdate;
|
clock::time_point mLastUpdate;
|
||||||
@@ -65,8 +76,18 @@ namespace Game {
|
|||||||
static std::unordered_map<std::string, int> mSharedInts;
|
static std::unordered_map<std::string, int> mSharedInts;
|
||||||
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<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();
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ namespace Game::Renderer {
|
|||||||
float camY;
|
float camY;
|
||||||
int screenW;
|
int screenW;
|
||||||
int screenH;
|
int screenH;
|
||||||
|
float scaleX; // Scale from logical (1280) to actual screen width
|
||||||
|
float scaleY; // Scale from logical (720) to actual screen height
|
||||||
} RendererConfig;
|
} RendererConfig;
|
||||||
|
|
||||||
class Renderer {
|
class Renderer {
|
||||||
|
|||||||
@@ -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");
|
||||||
@@ -35,6 +151,29 @@ namespace {
|
|||||||
file << "Igralec: " << playerName << "\n";
|
file << "Igralec: " << playerName << "\n";
|
||||||
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 with form state
|
||||||
|
std::vector<Game::Object::Transform> playerHistory;
|
||||||
|
std::vector<bool> formHistory;
|
||||||
|
Game::GameManager::getPlayerPositionHistory(playerHistory);
|
||||||
|
Game::GameManager::getPlayerFormHistory(formHistory);
|
||||||
|
|
||||||
|
std::ofstream replayFile("replay.txt", std::ios::trunc);
|
||||||
|
if (!replayFile.is_open()) {
|
||||||
|
WARN("Neuspešno odpiranje replay.txt za pisanje");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < playerHistory.size(); ++i) {
|
||||||
|
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");
|
||||||
|
replayFile.close();
|
||||||
|
file.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,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
|
||||||
@@ -57,7 +197,10 @@ namespace Game::AGame {
|
|||||||
mSeaTex->setTiled(true);
|
mSeaTex->setTiled(true);
|
||||||
}
|
}
|
||||||
mTiledScale = 0.5f;
|
mTiledScale = 0.5f;
|
||||||
SDL_GetWindowSizeInPixels(Window::Window::getSDLWindowBackend(), &mW, &mH);
|
|
||||||
|
// Use logical world dimensions (1280×720) not actual screen size
|
||||||
|
mW = 1280;
|
||||||
|
mH = 720;
|
||||||
|
|
||||||
// Land boundary: left 1/3 of map in centered coordinates
|
// Land boundary: left 1/3 of map in centered coordinates
|
||||||
// For 1280px window: -640 (left) + 426.67 (1/3) = -213.33
|
// For 1280px window: -640 (left) + 426.67 (1/3) = -213.33
|
||||||
@@ -72,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) {
|
||||||
@@ -128,9 +275,11 @@ namespace Game::AGame {
|
|||||||
const float viewBottom = mH / 2.f;
|
const float viewBottom = mH / 2.f;
|
||||||
|
|
||||||
Object::Transform tS;
|
Object::Transform tS;
|
||||||
|
tS.x = 0.f;
|
||||||
|
tS.y = 0.f;
|
||||||
tS.rotation = 0.f;
|
tS.rotation = 0.f;
|
||||||
tS.scaleX = 6.f;
|
tS.scaleX = 4.0f;
|
||||||
tS.scaleY = 6.f;
|
tS.scaleY = 4.0f;
|
||||||
|
|
||||||
const float halfFriendlyW = mFriendlyTex->getWidth() * tS.adjustedScaleX() / 2.f;
|
const float halfFriendlyW = mFriendlyTex->getWidth() * tS.adjustedScaleX() / 2.f;
|
||||||
const float halfFriendlyH = mFriendlyTex->getHeight() * tS.adjustedScaleY() / 2.f;
|
const float halfFriendlyH = mFriendlyTex->getHeight() * tS.adjustedScaleY() / 2.f;
|
||||||
@@ -194,8 +343,8 @@ namespace Game::AGame {
|
|||||||
Object::Transform tS;
|
Object::Transform tS;
|
||||||
tS.rotation = 0.f;
|
tS.rotation = 0.f;
|
||||||
|
|
||||||
tS.scaleX = 7.f;
|
tS.scaleX = 4.7f;
|
||||||
tS.scaleY = 7.f;
|
tS.scaleY = 4.7f;
|
||||||
const float halfEnemyW = mEnemyTex->getWidth() * tS.adjustedScaleX() / 2.f;
|
const float halfEnemyW = mEnemyTex->getWidth() * tS.adjustedScaleX() / 2.f;
|
||||||
const float halfEnemyH = mEnemyTex->getHeight() * tS.adjustedScaleY() / 2.f;
|
const float halfEnemyH = mEnemyTex->getHeight() * tS.adjustedScaleY() / 2.f;
|
||||||
for (int i = 0; i < enemyCount; ++i) {
|
for (int i = 0; i < enemyCount; ++i) {
|
||||||
@@ -248,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");
|
||||||
@@ -268,9 +427,11 @@ namespace Game::AGame {
|
|||||||
const bool spawnSea = sideRoll < static_cast<int>(seaProb * 100.f);
|
const bool spawnSea = sideRoll < static_cast<int>(seaProb * 100.f);
|
||||||
|
|
||||||
Object::Transform tS;
|
Object::Transform tS;
|
||||||
|
tS.x = 0.f;
|
||||||
|
tS.y = 0.f;
|
||||||
tS.rotation = 0.f;
|
tS.rotation = 0.f;
|
||||||
tS.scaleX = 6.f;
|
tS.scaleX = 4.0f;
|
||||||
tS.scaleY = 6.f;
|
tS.scaleY = 4.0f;
|
||||||
const float viewLeft = -mW / 2.f;
|
const float viewLeft = -mW / 2.f;
|
||||||
const float viewRight = mW / 2.f;
|
const float viewRight = mW / 2.f;
|
||||||
const float viewTop = -mH / 2.f;
|
const float viewTop = -mH / 2.f;
|
||||||
@@ -313,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");
|
||||||
}
|
}
|
||||||
@@ -320,7 +482,8 @@ namespace Game::AGame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Background::onWindowResized(int newWidth, int newHeight) {
|
void Background::onWindowResized(int newWidth, int newHeight) {
|
||||||
mW = newWidth;
|
// Always maintain logical world dimensions (1280×720)
|
||||||
mH = newHeight;
|
mW = 1280;
|
||||||
|
mH = 720;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,14 +44,31 @@ namespace Game::AGame {
|
|||||||
const float dyv = py - ey;
|
const float dyv = py - ey;
|
||||||
mIsVisible = (dxv * dxv + dyv * dyv) <= (revealRadius * revealRadius);
|
mIsVisible = (dxv * dxv + dyv * dyv) <= (revealRadius * revealRadius);
|
||||||
|
|
||||||
// Semi-random movement with periodic direction changes
|
// Check if player is on land (not in ship mode) and within follow distance
|
||||||
mDirectionChangeTimer += deltaTime;
|
const float distanceToPlayer = std::sqrt(dxv * dxv + dyv * dyv);
|
||||||
if (mDirectionChangeTimer > 2.0f) {
|
const bool playerOnLand = !player->isShipMode();
|
||||||
const float angle = static_cast<float>(Utils::getUtils().rirng32(0, 360)) * 3.14159f / 180.f;
|
const bool withinFollowRange = distanceToPlayer <= FOLLOW_DISTANCE;
|
||||||
const float speed = 20.f + static_cast<float>(Utils::getUtils().rirng32(0, 30));
|
|
||||||
mMoveSpeedX = std::cos(angle) * speed;
|
if (playerOnLand && withinFollowRange) {
|
||||||
mMoveSpeedY = std::sin(angle) * speed;
|
// Follow player: calculate direction and move at constant speed
|
||||||
mDirectionChangeTimer = 0.f;
|
mFollowingPlayer = true;
|
||||||
|
if (distanceToPlayer > 1.f) { // Avoid division by zero
|
||||||
|
mMoveSpeedX = (dxv / distanceToPlayer) * FOLLOW_SPEED;
|
||||||
|
mMoveSpeedY = (dyv / distanceToPlayer) * FOLLOW_SPEED;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Revert to random movement when player is on sea or out of range
|
||||||
|
mFollowingPlayer = false;
|
||||||
|
|
||||||
|
// Semi-random movement with periodic direction changes
|
||||||
|
mDirectionChangeTimer += deltaTime;
|
||||||
|
if (mDirectionChangeTimer > 2.0f) {
|
||||||
|
const float angle = static_cast<float>(Utils::getUtils().rirng32(0, 360)) * 3.14159f / 180.f;
|
||||||
|
const float speed = 20.f + static_cast<float>(Utils::getUtils().rirng32(0, 30));
|
||||||
|
mMoveSpeedX = std::cos(angle) * speed;
|
||||||
|
mMoveSpeedY = std::sin(angle) * speed;
|
||||||
|
mDirectionChangeTimer = 0.f;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move enemy
|
// Move enemy
|
||||||
@@ -68,9 +85,9 @@ namespace Game::AGame {
|
|||||||
const float halfWidth = entityWidth / 2.f;
|
const float halfWidth = entityWidth / 2.f;
|
||||||
const float halfHeight = entityHeight / 2.f;
|
const float halfHeight = entityHeight / 2.f;
|
||||||
|
|
||||||
// Get window dimensions for boundary calculations
|
// Use logical world dimensions (1280×720) not actual screen size
|
||||||
int w = 0, h = 0;
|
constexpr int w = 1280;
|
||||||
SDL_GetWindowSizeInPixels(Window::Window::getSDLWindowBackend(), &w, &h);
|
constexpr int h = 720;
|
||||||
const float leftEdge = -w / 2.f + 25.f;
|
const float leftEdge = -w / 2.f + 25.f;
|
||||||
|
|
||||||
if (mTransform.x - halfWidth < leftEdge) {
|
if (mTransform.x - halfWidth < leftEdge) {
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
#include <game/agame/friendly.hpp>
|
#include <game/agame/friendly.hpp>
|
||||||
#include <game/gamemanager.hpp>
|
#include <game/gamemanager.hpp>
|
||||||
#include <game/agame/player.hpp>
|
#include <game/agame/player.hpp>
|
||||||
|
#include <game/agame/trash.hpp>
|
||||||
#include <object/components/boxcollider.hpp>
|
#include <object/components/boxcollider.hpp>
|
||||||
|
#include <state/gamestate.hpp>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
#include <utils.hpp>
|
#include <utils.hpp>
|
||||||
#include <window/window.hpp>
|
#include <window/window.hpp>
|
||||||
|
|
||||||
@@ -12,26 +15,105 @@ namespace Game::AGame {
|
|||||||
mZIndex = 20;
|
mZIndex = 20;
|
||||||
addComponent<Object::Components::BoxCollider>();
|
addComponent<Object::Components::BoxCollider>();
|
||||||
LOG("Zaveznik zagnan: " << getName());
|
LOG("Zaveznik zagnan: " << getName());
|
||||||
|
|
||||||
|
const float landBoundaryX = GameManager::getSharedData<float>("terrainLandBoundaryX");
|
||||||
|
const float entityWidth = getTexture() ? getTexture()->getWidth() * mTransform.adjustedScaleX() : 0.f;
|
||||||
|
mOnSea = (mTransform.x + entityWidth / 2.f) > landBoundaryX;
|
||||||
|
|
||||||
// Initialize random movement
|
if (!mOnSea) {
|
||||||
const float angle = static_cast<float>(Utils::getUtils().rirng32(0, 360)) * 3.14159f / 180.f;
|
// Initialize random movement for land friendlies
|
||||||
const float speed = 20.f + static_cast<float>(Utils::getUtils().rirng32(0, 30));
|
const float angle = static_cast<float>(Utils::getUtils().rirng32(0, 360)) * 3.14159f / 180.f;
|
||||||
mMoveSpeedX = std::cos(angle) * speed;
|
const float speed = LAND_SPEED_MIN + static_cast<float>(Utils::getUtils().rirng32(0, static_cast<int>(LAND_SPEED_MAX - LAND_SPEED_MIN)));
|
||||||
mMoveSpeedY = std::sin(angle) * speed;
|
mMoveSpeedX = std::cos(angle) * speed;
|
||||||
|
mMoveSpeedY = std::sin(angle) * speed;
|
||||||
|
} else {
|
||||||
|
mMoveSpeedX = 0.f;
|
||||||
|
mMoveSpeedY = 0.f;
|
||||||
|
}
|
||||||
mDirectionChangeTimer = 0.f;
|
mDirectionChangeTimer = 0.f;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Friendly::update(float deltaTime) {
|
void Friendly::update(float deltaTime) {
|
||||||
(void)deltaTime;
|
// Auto-cleanup nearby trash
|
||||||
|
const float fw = getTexture() ? getTexture()->getWidth() * mTransform.adjustedScaleX() : 0.f;
|
||||||
|
const float fh = getTexture() ? getTexture()->getHeight() * mTransform.adjustedScaleY() : 0.f;
|
||||||
|
const float friendlyCenterX = mTransform.x + fw / 2.f;
|
||||||
|
const float friendlyCenterY = mTransform.y + fh / 2.f;
|
||||||
|
|
||||||
|
const float landBoundaryX = GameManager::getSharedData<float>("terrainLandBoundaryX");
|
||||||
|
mOnSea = friendlyCenterX > landBoundaryX;
|
||||||
|
|
||||||
|
if (mOnSea) {
|
||||||
|
auto snapshot = State::GameState::getInstance().getEntitiesSnapshot();
|
||||||
|
Trash* nearestTrash = nullptr;
|
||||||
|
float nearestDistanceSquared = std::numeric_limits<float>::max();
|
||||||
|
|
||||||
|
for (auto* entity : snapshot) {
|
||||||
|
if (!entity) continue;
|
||||||
|
auto* trash = dynamic_cast<Trash*>(entity);
|
||||||
|
if (!trash) continue;
|
||||||
|
|
||||||
|
const float tw = trash->getTexture() ? trash->getTexture()->getWidth() * trash->getTransform()->adjustedScaleX() : 0.f;
|
||||||
|
const float th = trash->getTexture() ? trash->getTexture()->getHeight() * trash->getTransform()->adjustedScaleY() : 0.f;
|
||||||
|
const float trashCenterX = trash->getTransform()->x + tw / 2.f;
|
||||||
|
const float trashCenterY = trash->getTransform()->y + th / 2.f;
|
||||||
|
const float dx = trashCenterX - friendlyCenterX;
|
||||||
|
const float dy = trashCenterY - friendlyCenterY;
|
||||||
|
const float distanceSquared = dx * dx + dy * dy;
|
||||||
|
|
||||||
|
if (distanceSquared < nearestDistanceSquared) {
|
||||||
|
nearestDistanceSquared = distanceSquared;
|
||||||
|
nearestTrash = trash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nearestTrash) {
|
||||||
|
const float tw = nearestTrash->getTexture() ? nearestTrash->getTexture()->getWidth() * nearestTrash->getTransform()->adjustedScaleX() : 0.f;
|
||||||
|
const float th = nearestTrash->getTexture() ? nearestTrash->getTexture()->getHeight() * nearestTrash->getTransform()->adjustedScaleY() : 0.f;
|
||||||
|
const float trashCenterX = nearestTrash->getTransform()->x + tw / 2.f;
|
||||||
|
const float trashCenterY = nearestTrash->getTransform()->y + th / 2.f;
|
||||||
|
const float dx = trashCenterX - friendlyCenterX;
|
||||||
|
const float dy = trashCenterY - friendlyCenterY;
|
||||||
|
const float distance = std::sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
if (distance > 0.0001f) {
|
||||||
|
mMoveSpeedX = (dx / distance) * SEA_SPEED;
|
||||||
|
mMoveSpeedY = (dy / distance) * SEA_SPEED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Semi-random movement with periodic direction changes on land
|
||||||
|
mDirectionChangeTimer += deltaTime;
|
||||||
|
if (mDirectionChangeTimer > 2.0f) {
|
||||||
|
const float angle = static_cast<float>(Utils::getUtils().rirng32(0, 360)) * 3.14159f / 180.f;
|
||||||
|
const float speed = LAND_SPEED_MIN + static_cast<float>(Utils::getUtils().rirng32(0, static_cast<int>(LAND_SPEED_MAX - LAND_SPEED_MIN)));
|
||||||
|
mMoveSpeedX = std::cos(angle) * speed;
|
||||||
|
mMoveSpeedY = std::sin(angle) * speed;
|
||||||
|
mDirectionChangeTimer = 0.f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto snapshot = State::GameState::getInstance().getEntitiesSnapshot();
|
||||||
|
for (auto* entity : snapshot) {
|
||||||
|
if (!entity) continue;
|
||||||
|
auto* trash = dynamic_cast<Trash*>(entity);
|
||||||
|
if (!trash) continue;
|
||||||
|
|
||||||
|
// Calculate distance to trash
|
||||||
|
const float tw = trash->getTexture() ? trash->getTexture()->getWidth() * trash->getTransform()->adjustedScaleX() : 0.f;
|
||||||
|
const float th = trash->getTexture() ? trash->getTexture()->getHeight() * trash->getTransform()->adjustedScaleY() : 0.f;
|
||||||
|
const float trashCenterX = trash->getTransform()->x + tw / 2.f;
|
||||||
|
const float trashCenterY = trash->getTransform()->y + th / 2.f;
|
||||||
|
const float dx = friendlyCenterX - trashCenterX;
|
||||||
|
const float dy = friendlyCenterY - trashCenterY;
|
||||||
|
const float distanceSquared = dx * dx + dy * dy;
|
||||||
|
|
||||||
// Semi-random movement with periodic direction changes
|
if (distanceSquared <= CLEANUP_RADIUS * CLEANUP_RADIUS) {
|
||||||
mDirectionChangeTimer += deltaTime;
|
// Clean up this trash: award points and remove it
|
||||||
if (mDirectionChangeTimer > 2.0f) {
|
GameManager::setSharedData("gameScore", GameManager::getSharedData<int>("gameScore") + CLEANUP_SCORE_BONUS);
|
||||||
const float angle = static_cast<float>(Utils::getUtils().rirng32(0, 360)) * 3.14159f / 180.f;
|
GameManager::setSharedData("trashActiveCount", std::max(0, GameManager::getSharedData<int>("trashActiveCount") - 1));
|
||||||
const float speed = 20.f + static_cast<float>(Utils::getUtils().rirng32(0, 30));
|
GameManager::destroyEntity(trash);
|
||||||
mMoveSpeedX = std::cos(angle) * speed;
|
}
|
||||||
mMoveSpeedY = std::sin(angle) * speed;
|
|
||||||
mDirectionChangeTimer = 0.f;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move friendly
|
// Move friendly
|
||||||
@@ -39,25 +121,26 @@ namespace Game::AGame {
|
|||||||
mTransform.y += mMoveSpeedY * deltaTime;
|
mTransform.y += mMoveSpeedY * deltaTime;
|
||||||
|
|
||||||
// Clamp to land section
|
// Clamp to land section
|
||||||
const float landBoundaryX = GameManager::getSharedData<float>("terrainLandBoundaryX");
|
const float halfWidth = fw / 2.f;
|
||||||
const float entityWidth = getTexture() ? getTexture()->getWidth() * mTransform.adjustedScaleX() : 0.f;
|
const float halfHeight = fh / 2.f;
|
||||||
const float entityHeight = getTexture() ? getTexture()->getHeight() * mTransform.adjustedScaleY() : 0.f;
|
|
||||||
const float halfWidth = entityWidth / 2.f;
|
|
||||||
const float halfHeight = entityHeight / 2.f;
|
|
||||||
|
|
||||||
// Get window dimensions for boundary calculations
|
// Use logical world dimensions (1280×720) not actual screen size
|
||||||
int w = 0, h = 0;
|
constexpr int w = 1280;
|
||||||
SDL_GetWindowSizeInPixels(Window::Window::getSDLWindowBackend(), &w, &h);
|
constexpr int h = 720;
|
||||||
const float leftEdge = -w / 2.f + 25.f;
|
const float leftEdge = -w / 2.f + 25.f;
|
||||||
|
|
||||||
if (mTransform.x - halfWidth < leftEdge) {
|
if (mTransform.x - halfWidth < leftEdge) {
|
||||||
mTransform.x = leftEdge + halfWidth;
|
mTransform.x = leftEdge + halfWidth;
|
||||||
mMoveSpeedX = std::abs(mMoveSpeedX);
|
mMoveSpeedX = std::abs(mMoveSpeedX);
|
||||||
}
|
}
|
||||||
if (mTransform.x + halfWidth > landBoundaryX - 25.f) {
|
if (!mOnSea && mTransform.x + halfWidth > landBoundaryX - 25.f) {
|
||||||
mTransform.x = landBoundaryX - 25.f - halfWidth;
|
mTransform.x = landBoundaryX - 25.f - halfWidth;
|
||||||
mMoveSpeedX = -std::abs(mMoveSpeedX);
|
mMoveSpeedX = -std::abs(mMoveSpeedX);
|
||||||
}
|
}
|
||||||
|
if (mOnSea && mTransform.x - halfWidth < landBoundaryX + 25.f) {
|
||||||
|
mTransform.x = landBoundaryX + 25.f + halfWidth;
|
||||||
|
mMoveSpeedX = std::abs(mMoveSpeedX);
|
||||||
|
}
|
||||||
if (mTransform.y - halfHeight < -h / 2.f + 25.f) {
|
if (mTransform.y - halfHeight < -h / 2.f + 25.f) {
|
||||||
mTransform.y = -h / 2.f + 25.f + halfHeight;
|
mTransform.y = -h / 2.f + 25.f + halfHeight;
|
||||||
mMoveSpeedY = std::abs(mMoveSpeedY);
|
mMoveSpeedY = std::abs(mMoveSpeedY);
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ namespace Game::AGame {
|
|||||||
void HUDText::update(float deltaTime) {
|
void HUDText::update(float deltaTime) {
|
||||||
(void)deltaTime;
|
(void)deltaTime;
|
||||||
|
|
||||||
int windowW = 0;
|
// Use logical world dimensions (1280×720) not actual screen size
|
||||||
int windowH = 0;
|
constexpr int windowW = 1280;
|
||||||
SDL_GetWindowSizeInPixels(Window::Window::getSDLWindowBackend(), &windowW, &windowH);
|
constexpr int windowH = 720;
|
||||||
float camX = 0.f;
|
float camX = 0.f;
|
||||||
float camY = 0.f;
|
float camY = 0.f;
|
||||||
Object::Camera::getInstance().getPosition(camX, camY);
|
Object::Camera::getInstance().getPosition(camX, camY);
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ namespace Game::AGame {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int w = 0;
|
// Use logical world dimensions (1280×720) not actual screen size
|
||||||
int h = 0;
|
constexpr int w = 1280;
|
||||||
SDL_GetWindowSizeInPixels(Window::Window::getSDLWindowBackend(), &w, &h);
|
constexpr int h = 720;
|
||||||
|
|
||||||
const float halfWidth = spawnTex->getWidth() * mTransform.adjustedScaleX() / 2.f;
|
const float halfWidth = spawnTex->getWidth() * mTransform.adjustedScaleX() / 2.f;
|
||||||
const float halfHeight = spawnTex->getHeight() * mTransform.adjustedScaleY() / 2.f;
|
const float halfHeight = spawnTex->getHeight() * mTransform.adjustedScaleY() / 2.f;
|
||||||
@@ -64,8 +64,8 @@ namespace Game::AGame {
|
|||||||
Game::GameManager::setSharedData("gameStage", 1);
|
Game::GameManager::setSharedData("gameStage", 1);
|
||||||
Game::GameManager::setSharedData("gameScore", 0);
|
Game::GameManager::setSharedData("gameScore", 0);
|
||||||
|
|
||||||
mTransform.scaleX = 8.f;
|
mTransform.scaleX = 5.3f;
|
||||||
mTransform.scaleY = 8.f;
|
mTransform.scaleY = 5.3f;
|
||||||
|
|
||||||
if (!mShipTex) {
|
if (!mShipTex) {
|
||||||
mShipTex = mTex;
|
mShipTex = mTex;
|
||||||
@@ -74,9 +74,9 @@ namespace Game::AGame {
|
|||||||
mGroundTex = mTex;
|
mGroundTex = mTex;
|
||||||
}
|
}
|
||||||
|
|
||||||
int w = 0;
|
// Use logical world dimensions (1280×720) not actual screen size
|
||||||
int h = 0;
|
constexpr int w = 1280;
|
||||||
SDL_GetWindowSizeInPixels(Window::Window::getSDLWindowBackend(), &w, &h);
|
constexpr int h = 720;
|
||||||
|
|
||||||
const float halfWidth = mTex->getWidth() * mTransform.adjustedScaleX() / 2.f;
|
const float halfWidth = mTex->getWidth() * mTransform.adjustedScaleX() / 2.f;
|
||||||
const float halfHeight = mTex->getHeight() * mTransform.adjustedScaleY() / 2.f;
|
const float halfHeight = mTex->getHeight() * mTransform.adjustedScaleY() / 2.f;
|
||||||
@@ -134,9 +134,9 @@ namespace Game::AGame {
|
|||||||
if (Input::isKeyPressed(SDL_SCANCODE_D)) { mTransform.x += mSpeed * deltaTime; mIsFlipped = true; }
|
if (Input::isKeyPressed(SDL_SCANCODE_D)) { mTransform.x += mSpeed * deltaTime; mIsFlipped = true; }
|
||||||
mSpeed = Input::isKeyPressed(SDL_SCANCODE_LSHIFT) ? 400.f : 200.f;
|
mSpeed = Input::isKeyPressed(SDL_SCANCODE_LSHIFT) ? 400.f : 200.f;
|
||||||
|
|
||||||
int w = 0;
|
// Use logical world dimensions (1280×720) not actual screen size
|
||||||
int h = 0;
|
constexpr int w = 1280;
|
||||||
SDL_GetWindowSizeInPixels(Window::Window::getSDLWindowBackend(), &w, &h);
|
constexpr int h = 720;
|
||||||
const float entityWidth = mTex ? mTex->getWidth() * mTransform.adjustedScaleX() : 0.f;
|
const float entityWidth = mTex ? mTex->getWidth() * mTransform.adjustedScaleX() : 0.f;
|
||||||
const float entityHeight = mTex ? mTex->getHeight() * mTransform.adjustedScaleY() : 0.f;
|
const float entityHeight = mTex ? mTex->getHeight() * mTransform.adjustedScaleY() : 0.f;
|
||||||
const float minX = -w / 2.f;
|
const float minX = -w / 2.f;
|
||||||
@@ -161,6 +161,10 @@ namespace Game::AGame {
|
|||||||
} else if (!mIsShipMode && mGroundTex) {
|
} else if (!mIsShipMode && mGroundTex) {
|
||||||
setTexture(mGroundTex);
|
setTexture(mGroundTex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Push replay (position and form state)
|
||||||
|
GameManager::pushPlayerPosition(mTransform);
|
||||||
|
GameManager::pushPlayerFormState(mIsShipMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::onCollisionEnter(Object::Entity* other) {
|
void Player::onCollisionEnter(Object::Entity* other) {
|
||||||
|
|||||||
@@ -11,15 +11,20 @@ namespace Game::AGame {
|
|||||||
|
|
||||||
void Trash::update(float deltaTime) {
|
void Trash::update(float deltaTime) {
|
||||||
(void)deltaTime;
|
(void)deltaTime;
|
||||||
if (mSeaOnly) {
|
/*if (mSeaOnly) {
|
||||||
const float landBoundaryX = GameManager::getSharedData<float>("terrainLandBoundaryX");
|
const float landBoundaryX = GameManager::getSharedData<float>("terrainLandBoundaryX");
|
||||||
const float margin = 25.f;
|
const float margin = 25.f;
|
||||||
const float halfWidth = getTexture() ? getTexture()->getWidth() * mTransform.adjustedScaleX() / 2.f : 0.f;
|
const float halfWidth = getTexture() ? getTexture()->getWidth() * mTransform.adjustedScaleX() / 2.f : 0.f;
|
||||||
if (mTransform.x - halfWidth < landBoundaryX + margin) {
|
if (mTransform.x - halfWidth < landBoundaryX + margin) {
|
||||||
mTransform.x = landBoundaryX + margin + halfWidth;
|
mTransform.x = landBoundaryX + margin + halfWidth;
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
return;
|
|
||||||
|
// Naključno premikanje
|
||||||
|
mTransform.x += static_cast<float>(Utils::getUtils().rirng32(-50, 50)) * deltaTime;
|
||||||
|
mTransform.y += static_cast<float>(Utils::getUtils().rirng32(-50, 50)) * deltaTime;
|
||||||
|
|
||||||
|
//return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Trash::onCollisionEnter(Object::Entity* other) {
|
void Trash::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());
|
||||||
@@ -67,6 +84,8 @@ namespace Game {
|
|||||||
std::unordered_map<std::string, int> GameManager::mSharedInts;
|
std::unordered_map<std::string, int> GameManager::mSharedInts;
|
||||||
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<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) {
|
||||||
@@ -83,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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
263
src/main.cpp
263
src/main.cpp
@@ -11,35 +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;
|
||||||
|
|
||||||
|
// Global SDL renderer pointer used by menu callback to create textures/fonts
|
||||||
|
static SDL_Renderer* gSDLRenderer = nullptr;
|
||||||
|
static Uint32 gStartGameEventType = static_cast<Uint32>(-1);
|
||||||
|
static std::atomic_bool gStartGameQueued = false;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Spawn the main game entities now that we have a player name
|
||||||
|
// Background
|
||||||
|
State::GameState::getInstance().addEntity(std::make_unique<AGame::Background>("BG", std::make_shared<Game::Renderer::Texture>("../resources/bgtest.png", gSDLRenderer), Object::DEFAULT_TRANSFORM));
|
||||||
|
|
||||||
|
// Player
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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() {
|
int main() {
|
||||||
PLNIMP("Letnik3Zadnja - Licenca: LGPLv2.1-only, CC BY-SA 4.0");
|
PLNIMP("Letnik3Zadnja - Licenca: LGPLv2.1-only, CC BY-SA 4.0");
|
||||||
// Prompt for player name before initializing the window/engine
|
|
||||||
std::string playerName;
|
|
||||||
std::cout << "Vnesi uporabniško ime (pusti prazno za 'Igralec'): ";
|
|
||||||
std::getline(std::cin, playerName);
|
|
||||||
if (playerName.empty()) playerName = "Igralec";
|
|
||||||
Game::GameManager::setSharedData<std::string>("playerName", playerName);
|
|
||||||
|
|
||||||
Window::Window window = Window::Window();
|
Window::Window window = Window::Window();
|
||||||
window.init(1280, 720, "Game Window");
|
window.init(1280, 720, "Game Window");
|
||||||
|
|
||||||
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));
|
// Make SDL renderer available to callbacks
|
||||||
|
gSDLRenderer = window.getRenderer()->getSDLRenderer();
|
||||||
|
|
||||||
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", window.getRenderer()->getSDLRenderer()), Object::DEFAULT_TRANSFORM)));
|
// Register a custom event used to perform menu -> game transition on the
|
||||||
if (player) {
|
// window/event thread.
|
||||||
player->addComponent<Object::Components::BoxCollider>();
|
gStartGameEventType = SDL_RegisterEvents(1);
|
||||||
player->setShipTexture(std::make_shared<Game::Renderer::Texture>("../resources/l3ladja.png", window.getRenderer()->getSDLRenderer()));
|
|
||||||
player->setGroundTexture(std::make_shared<Game::Renderer::Texture>("../resources/l3player.png", window.getRenderer()->getSDLRenderer()));
|
// 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
State::GameState::getInstance().addEntity(std::make_unique<AGame::HUDText>("HUD", std::make_shared<Game::Renderer::Font>("../resources/roboto.ttf", window.getRenderer()->getSDLRenderer(), 72, "HUDFont"), Object::Transform{0.f, 0.f, 0.f, 1.f, 1.f}, 320.f, 40.f));
|
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;
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@ namespace Game::Object {
|
|||||||
SDL_GetTextureSize(mTex->getSDLTexture(), &w, &h);
|
SDL_GetTextureSize(mTex->getSDLTexture(), &w, &h);
|
||||||
|
|
||||||
SDL_FRect dst;
|
SDL_FRect dst;
|
||||||
dst.w = w * mTransform.scaleX * UNIVERSAL_SCALE_COEFFICIENT; // 1.f is HUGE, so this is just a constant to make the default scale more reasonable
|
dst.w = w * mTransform.scaleX * UNIVERSAL_SCALE_COEFFICIENT;
|
||||||
dst.h = h * mTransform.scaleY * UNIVERSAL_SCALE_COEFFICIENT;
|
dst.h = h * mTransform.scaleY * UNIVERSAL_SCALE_COEFFICIENT;
|
||||||
|
|
||||||
// Top-left origin; Account for camera position (center the camera on the screen)
|
// Top-left origin; Account for camera position (center the camera on the screen)
|
||||||
@@ -73,7 +73,7 @@ namespace Game::Object {
|
|||||||
SDL_GetTextureSize(mTex->getSDLTexture(), &tileW, &tileH);
|
SDL_GetTextureSize(mTex->getSDLTexture(), &tileW, &tileH);
|
||||||
|
|
||||||
SDL_FRect dst;
|
SDL_FRect dst;
|
||||||
dst.w = tileW * mTiledScale; // Tile size is the original texture size multiplied by the universal scale coefficient (ignoring the entity's scale, since that only affects how many times the texture is tiled, not the size of each tile)
|
dst.w = tileW * mTiledScale;
|
||||||
dst.h = tileH * mTiledScale;
|
dst.h = tileH * mTiledScale;
|
||||||
|
|
||||||
// Top-left origin; Account for camera position (center the camera on the screen)
|
// Top-left origin; Account for camera position (center the camera on the screen)
|
||||||
|
|||||||
@@ -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,147 +1,297 @@
|
|||||||
#include <object/ui/uitextbox.hpp>
|
#include <object/ui/uitextbox.hpp>
|
||||||
#include <renderer/renderer.hpp>
|
#include <renderer/renderer.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() {
|
||||||
mTransform.x = mX - mTex->getWidth() * mTransform.adjustedScaleX() / 2.f;
|
// Build initial text texture (placeholder if present) so textbox has a
|
||||||
mTransform.y = mY - mTex->getHeight() * mTransform.adjustedScaleY() / 2.f;
|
// visible indicator and stable dimensions for click hit-testing.
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
mBoxWidth = static_cast<float>(mTex->getWidth()) * mTransform.adjustedScaleX();
|
// Center the textbox on the requested position
|
||||||
mBoxHeight = static_cast<float>(mTex->getHeight()) * mTransform.adjustedScaleY();
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
if (mBoxWidth < mConfig.minWidth) mBoxWidth = mConfig.minWidth;
|
static char scancodeToChar(SDL_Scancode scancode, bool shift) {
|
||||||
if (mBoxHeight < mConfig.minHeight) mBoxHeight = mConfig.minHeight;
|
// Basic mapping for letters, digits and common symbols
|
||||||
|
if (scancode >= SDL_SCANCODE_A && scancode <= SDL_SCANCODE_Z) {
|
||||||
refreshVisualText();
|
char c = 'a' + (scancode - SDL_SCANCODE_A);
|
||||||
|
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 {
|
|
||||||
const float mouseX = Input::getMouseX();
|
|
||||||
const float mouseY = Input::getMouseY();
|
|
||||||
|
|
||||||
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 mouseX >= left && mouseX <= right && mouseY >= top && mouseY <= 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());
|
||||||
|
|||||||
@@ -60,8 +60,18 @@ namespace Game::Renderer {
|
|||||||
Object::Camera::getInstance().getPosition(camX, camY);
|
Object::Camera::getInstance().getPosition(camX, camY);
|
||||||
int screenW, screenH;
|
int screenW, screenH;
|
||||||
SDL_GetWindowSizeInPixels(Window::Window::getSDLWindowBackend(), &screenW, &screenH);
|
SDL_GetWindowSizeInPixels(Window::Window::getSDLWindowBackend(), &screenW, &screenH);
|
||||||
// Pass the config to avoid wasting time recalculating it for every entity, since it's not gonna change during the frame
|
|
||||||
RendererConfig config{ camX, camY, screenW, screenH };
|
// Logical game world is 1280x720; scale rendering if fullscreen is at different resolution
|
||||||
|
static constexpr int LOGICAL_WIDTH = 1280;
|
||||||
|
static constexpr int LOGICAL_HEIGHT = 720;
|
||||||
|
const float scaleX = static_cast<float>(screenW) / LOGICAL_WIDTH;
|
||||||
|
const float scaleY = static_cast<float>(screenH) / LOGICAL_HEIGHT;
|
||||||
|
|
||||||
|
// Apply SDL render scale to handle fullscreen scaling
|
||||||
|
SDL_SetRenderScale(mRenderer, scaleX, scaleY);
|
||||||
|
|
||||||
|
// Pass the config WITHOUT scaling factors (SDL handles it now)
|
||||||
|
RendererConfig config{ camX, camY, LOGICAL_WIDTH, LOGICAL_HEIGHT, 1.0f, 1.0f };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Game::State::GameState::getInstance().withEntitiesLocked([&](auto& entities) {
|
Game::State::GameState::getInstance().withEntitiesLocked([&](auto& entities) {
|
||||||
@@ -89,6 +99,9 @@ namespace Game::Renderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mPresent();
|
mPresent();
|
||||||
|
|
||||||
|
// Reset render scale for next frame
|
||||||
|
SDL_SetRenderScale(mRenderer, 1.0f, 1.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Renderer::mClear() {
|
void Renderer::mClear() {
|
||||||
|
|||||||
@@ -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) { }
|
||||||
|
|
||||||
@@ -41,7 +50,7 @@ namespace Game::Window {
|
|||||||
|
|
||||||
Audio::Audio::getInstance().init();
|
Audio::Audio::getInstance().init();
|
||||||
|
|
||||||
mWindow = SDL_CreateWindow(title.c_str(), width, height, 0);
|
mWindow = SDL_CreateWindow(title.c_str(), width, height, SDL_WINDOW_FULLSCREEN);
|
||||||
if (!mWindow) {
|
if (!mWindow) {
|
||||||
ERROR("Failed to create window: " << SDL_GetError());
|
ERROR("Failed to create window: " << SDL_GetError());
|
||||||
SDL_Quit();
|
SDL_Quit();
|
||||||
@@ -88,15 +97,36 @@ 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)) {
|
||||||
if (event.type == SDL_EVENT_QUIT) {
|
// Forward SDL text input events to the Input queue
|
||||||
mRunning = false;
|
if (event.type == SDL_EVENT_TEXT_INPUT) {
|
||||||
|
if (event.text.text && event.text.text[0]) {
|
||||||
|
Game::Input::pushText(std::string(event.text.text));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type == SDL_EVENT_WINDOW_ENTER_FULLSCREEN) {
|
// Run custom callbacks on the window/event thread.
|
||||||
SDL_SetWindowFullscreen(mWindow, false);
|
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) {
|
||||||
|
mRunning = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Window resize event - update the renderer's viewport
|
// Window resize event - update the renderer's viewport
|
||||||
|
|||||||
Reference in New Issue
Block a user