Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ee3c263547 | |||
| 66c5e0e710 | |||
| 6534996a52 | |||
| 0b45643ef2 | |||
| d93e71e716 | |||
| 8ff3e29374 | |||
| 892d8f22eb | |||
| d9769bdbbb | |||
| c46443e2f4 | |||
| fcc598adb1 | |||
| e4389f035d | |||
| 56d567b77d |
@@ -4,6 +4,21 @@ project(Letnik3Zadnja)
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
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
|
||||
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)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Platform-specific dependency handling
|
||||
# Dependency handling
|
||||
# ------------------------------------------------------------
|
||||
|
||||
if(WIN32)
|
||||
if(FETCH OR WIN32)
|
||||
|
||||
# Include FetchContent to download SDL libraries
|
||||
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
|
||||
@@ -7,16 +7,30 @@
|
||||
#include <game/gamemanager.hpp>
|
||||
#include <game/agame/enemy.hpp>
|
||||
#include <game/agame/trash.hpp>
|
||||
#include <game/agame/friendly.hpp>
|
||||
|
||||
namespace Game::AGame {
|
||||
GAME_ENTITY(Background)
|
||||
public:
|
||||
void render(Game::Renderer::Renderer* renderer, Game::Renderer::RendererConfig config) override;
|
||||
void onWindowResized(int newWidth, int newHeight) override;
|
||||
// Spawn a single trash at the given transform. If seaOnly is true the trash
|
||||
// will be clamped to the sea side of the land boundary.
|
||||
void spawnTrashAt(const Object::Transform& tS, bool seaOnly = false);
|
||||
private:
|
||||
float mEnemySpawnTimer = 0.f;
|
||||
float mTimeToSpawn = 5.f;
|
||||
void spawnLevel(int stage);
|
||||
void spawnFriendly(int stage, int count);
|
||||
int mW, mH;
|
||||
int mMaxLevels = 2;
|
||||
float mLandBoundaryX = 0.f;
|
||||
bool mPendingLevelSpawn = false;
|
||||
int mPendingLevelStage = 0;
|
||||
// Periodic friendly spawn settings
|
||||
float mFriendlySpawnAvgInterval = 6.f; // average seconds between spawns
|
||||
int mMaxAutoFriendlies = 2; // hard cap for total active friendlies
|
||||
std::shared_ptr<Game::Renderer::Texture> mSeaTex;
|
||||
std::shared_ptr<Game::Renderer::Texture> mEnemyTex;
|
||||
std::shared_ptr<Game::Renderer::Texture> mTrashTex;
|
||||
std::shared_ptr<Game::Renderer::Texture> mFriendlyTex;
|
||||
END_GAME_ENTITY()
|
||||
}
|
||||
@@ -10,5 +10,14 @@ namespace Game::AGame {
|
||||
GAME_ENTITY(Enemy)
|
||||
public:
|
||||
void onCollisionEnter(Object::Entity* other) override;
|
||||
bool hasAdjacentEnemy();
|
||||
private:
|
||||
float mMoveSpeedX = 0.f;
|
||||
float mMoveSpeedY = 0.f;
|
||||
float mDirectionChangeTimer = 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()
|
||||
}
|
||||
24
include/game/agame/friendly.hpp
Normal file
24
include/game/agame/friendly.hpp
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <game/gamemanager.hpp>
|
||||
#include <object/entity.hpp>
|
||||
#include <renderer/texture.hpp>
|
||||
#include <renderer/font.hpp>
|
||||
#include <object/sound.hpp>
|
||||
|
||||
namespace Game::AGame {
|
||||
GAME_ENTITY(Friendly)
|
||||
public:
|
||||
void onCollisionEnter(Object::Entity* other) override;
|
||||
private:
|
||||
float mMoveSpeedX = 0.f;
|
||||
float mMoveSpeedY = 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()
|
||||
}
|
||||
14
include/game/agame/hudtext.hpp
Normal file
14
include/game/agame/hudtext.hpp
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <object/ui/uitext.hpp>
|
||||
|
||||
namespace Game::AGame {
|
||||
class HUDText : public Object::UIText {
|
||||
using Object::UIText::UIText;
|
||||
|
||||
public:
|
||||
~HUDText() override = default;
|
||||
void start() override;
|
||||
void update(float deltaTime) override;
|
||||
};
|
||||
}
|
||||
@@ -7,9 +7,22 @@
|
||||
|
||||
namespace Game::AGame {
|
||||
GAME_ENTITY(Player)
|
||||
private:
|
||||
Object::Sound mSound;
|
||||
float mSpeed = 200.f; // Pixels per second
|
||||
[[maybe_unused]] float mHealth = 100.f;
|
||||
public:
|
||||
void setShipTexture(std::shared_ptr<Game::Renderer::Texture> tex);
|
||||
void setGroundTexture(std::shared_ptr<Game::Renderer::Texture> tex);
|
||||
void respawnRandomSea(float landBoundaryX);
|
||||
bool isShipMode() const { return mIsShipMode; }
|
||||
void setShipMode(bool isShip) { mIsShipMode = isShip; } // Set form state for replay
|
||||
void onCollisionEnter(Object::Entity* other) override;
|
||||
private:
|
||||
Object::Sound mSound;
|
||||
float mSpeed = 200.f; // Pixels per second
|
||||
[[maybe_unused]] float mHealth = 100.f;
|
||||
std::shared_ptr<Game::Renderer::Texture> mShipTex;
|
||||
std::shared_ptr<Game::Renderer::Texture> mGroundTex;
|
||||
bool mIsShipMode = true;
|
||||
float mShoreMargin = 40.f;
|
||||
float mStateTransitionCooldown = 1.f;
|
||||
float mStateTransitionCooldownTimer = 0.f;
|
||||
END_GAME_ENTITY()
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -6,7 +6,16 @@
|
||||
#include <renderer/font.hpp>
|
||||
#include <object/sound.hpp>
|
||||
|
||||
namespace Game::AGame {
|
||||
class Player;
|
||||
}
|
||||
|
||||
namespace Game::AGame {
|
||||
GAME_ENTITY(Trash)
|
||||
public:
|
||||
void onCollisionEnter(Object::Entity* other) override;
|
||||
void setSeaOnly(bool v) { mSeaOnly = v; }
|
||||
private:
|
||||
bool mSeaOnly = false;
|
||||
END_GAME_ENTITY()
|
||||
}
|
||||
@@ -19,7 +19,8 @@ namespace Game {
|
||||
enum class GameStateEnum {
|
||||
RUNNING,
|
||||
PAUSED,
|
||||
STOPPED
|
||||
STOPPED,
|
||||
REPLAY
|
||||
};
|
||||
|
||||
enum class SharedDataType {
|
||||
@@ -58,6 +59,16 @@ namespace Game {
|
||||
static void destroyEntity(T* entity);
|
||||
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:
|
||||
int mTargetUpdatesPerSecond = TARGET_UPDATE_RATE;
|
||||
clock::time_point mLastUpdate;
|
||||
@@ -65,8 +76,18 @@ namespace Game {
|
||||
static std::unordered_map<std::string, int> mSharedInts;
|
||||
static std::unordered_map<std::string, float> mSharedFloats;
|
||||
static std::unordered_map<std::string, bool> mSharedBools;
|
||||
static std::vector<Object::Transform> mPlayerTransformHistory;
|
||||
static std::vector<bool> mPlayerFormHistory;
|
||||
static GameStateEnum mCurrentGameState;
|
||||
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>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <vector>
|
||||
#include <queue>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
namespace Game {
|
||||
class Input {
|
||||
@@ -16,13 +20,23 @@ namespace Game {
|
||||
static bool isMouseButtonJustReleased(Uint8 button);
|
||||
static float getMouseX();
|
||||
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:
|
||||
static const bool* mCurrentKeyStates;
|
||||
static const bool* mPreviousKeyStates;
|
||||
static std::vector<Uint8> mPreviousKeyStates;
|
||||
static int mNumKeys;
|
||||
static int mPrevNumKeys;
|
||||
static SDL_MouseButtonFlags mCurrentMouseButtonStates;
|
||||
static SDL_MouseButtonFlags mPreviousMouseButtonStates;
|
||||
static float mMouseX;
|
||||
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;
|
||||
};
|
||||
}
|
||||
@@ -21,10 +21,13 @@ namespace Game::Object::Components {
|
||||
void start(Object::Entity* thisEntity) override;
|
||||
void update(float deltaTime, Object::Entity* thisEntity) override;
|
||||
|
||||
void setScale(float scale) { mScale = scale; }
|
||||
float getScale() const { return mScale; }
|
||||
BoxColliderBounds getBounds() const { return mBounds; }
|
||||
bool isColliding() const { return !mCollidingWith.empty(); }
|
||||
|
||||
private:
|
||||
float mScale = 1.f;
|
||||
BoxColliderBounds mBounds{0.f, 0.f, 0.f, 0.f};
|
||||
std::unordered_set<Object::Entity*> mCollidingWith; // Track collisions per-entity so enter/stay/exit callbacks remain correct with multiple colliders
|
||||
};
|
||||
|
||||
@@ -25,5 +25,6 @@ namespace Game::Object {
|
||||
void* mClickFunction = nullptr;
|
||||
float mX, mY;
|
||||
std::string mText;
|
||||
bool mIsHovered = false;
|
||||
};
|
||||
}
|
||||
@@ -4,62 +4,55 @@
|
||||
#include <renderer/font.hpp>
|
||||
#include <renderer/texture.hpp>
|
||||
#include <utility>
|
||||
#include <string>
|
||||
#include <game/input.hpp>
|
||||
#include <SDL3/SDL.h>
|
||||
#include <mutex>
|
||||
|
||||
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 {
|
||||
public:
|
||||
UITextBox(const std::string& name, std::shared_ptr<Renderer::Font> font,
|
||||
const Transform& transform,
|
||||
float x = 0.f, float y = 0.f,
|
||||
UITextboxConfig config = {});
|
||||
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);
|
||||
~UITextBox() override = default;
|
||||
|
||||
void start() override;
|
||||
void start() 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 setText(const std::string& text);
|
||||
std::string getText() const;
|
||||
std::string getValue() const;
|
||||
bool isFocused() const;
|
||||
void setText(const std::string& text);
|
||||
std::string getText() const;
|
||||
void setPlaceholder(const std::string& placeholder) { mPlaceholder = placeholder; mLastRenderedText.clear(); }
|
||||
|
||||
// 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; }
|
||||
std::pair<float, float> getPosition() const { return {mX, mY}; }
|
||||
|
||||
bool isFocused() const { return mIsFocused; }
|
||||
|
||||
private:
|
||||
bool isMouseInsideBox() const;
|
||||
void refreshVisualText();
|
||||
std::shared_ptr<Renderer::Texture> mBackground; // Background texture for the textbox
|
||||
std::shared_ptr<Renderer::Font> mFont; // Font used to render text
|
||||
|
||||
float mX, mY;
|
||||
std::string mText;
|
||||
size_t mCursorIndex = 0;
|
||||
float mCursorBlinkTimer = 0.f;
|
||||
bool mShowCursor = true;
|
||||
size_t mMaxLength = 1024;
|
||||
bool mPasswordMode = false;
|
||||
bool mIsFocused = false;
|
||||
float mBoxWidth = 0.f;
|
||||
float mBoxHeight = 0.f;
|
||||
bool mNeedsTextRefresh = true;
|
||||
UITextboxConfig mConfig;
|
||||
|
||||
float mCursorTimer = 0.f;
|
||||
bool mCursorVisible = true;
|
||||
void* mOnFocus = nullptr; // optional function pointer
|
||||
std::string mLastRenderedText; // to avoid rebuilding font unnecessarily
|
||||
std::string mPlaceholder = "";
|
||||
float mReservedPlaceholderWidth = 0.f; // Reserved pixel width for placeholder to avoid layout shifts
|
||||
bool mIsHovered = false;
|
||||
std::mutex mRenderMutex; // Protects mLastRenderedText and mReservedPlaceholderWidth from main-thread updates
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,11 +17,16 @@ namespace Game::Renderer {
|
||||
|
||||
// Build the texture for the font; Call getSDLTexture() afterwards
|
||||
void build(SDL_Color color, std::string text);
|
||||
// Rebuild GPU-backed texture after a renderer/device reset
|
||||
bool reload(SDL_Renderer* renderer);
|
||||
|
||||
SDL_Texture* getSDLTexture();
|
||||
std::string getId();
|
||||
private:
|
||||
TTF_Font* mFont;
|
||||
SDL_Renderer* mRenderer;
|
||||
// Remember last build params so we can rebuild fonts on device reset
|
||||
std::string mLastText;
|
||||
SDL_Color mLastColor;
|
||||
};
|
||||
}
|
||||
@@ -11,6 +11,8 @@ namespace Game::Renderer {
|
||||
float camY;
|
||||
int screenW;
|
||||
int screenH;
|
||||
float scaleX; // Scale from logical (1280) to actual screen width
|
||||
float scaleY; // Scale from logical (720) to actual screen height
|
||||
} RendererConfig;
|
||||
|
||||
class Renderer {
|
||||
|
||||
@@ -21,10 +21,16 @@ namespace Game::Renderer {
|
||||
float getHeight();
|
||||
bool isTiled() { return mIsTiled; }
|
||||
void setTiled(bool tiled) { mIsTiled = tiled; }
|
||||
void setSDLTexture(SDL_Texture* tex) { mTex = tex; }
|
||||
// Reload GPU-backed texture using a new renderer after device reset
|
||||
virtual bool reload(SDL_Renderer* renderer);
|
||||
|
||||
protected:
|
||||
SDL_Texture* mTex;
|
||||
std::string mId;
|
||||
// For textures created from disk, store the path so we can reload on device reset
|
||||
std::string mPath;
|
||||
bool mIsFromFile = false;
|
||||
private:
|
||||
bool mIsTiled = false; // Whether the texture is a tileset that should be rendered as a single tile or not
|
||||
};
|
||||
|
||||
@@ -28,6 +28,8 @@ namespace Game::Window {
|
||||
|
||||
static SDL_Window* getSDLWindowBackend() { std::scoped_lock lock(sMutex); return sWindowBackend; }
|
||||
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:
|
||||
mutable std::mutex mMutex;
|
||||
|
||||
BIN
resources/l3friendly.png
Normal file
BIN
resources/l3friendly.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 393 B |
BIN
resources/l3player.png
Normal file
BIN
resources/l3player.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 393 B |
BIN
resources/l3sea.png
Normal file
BIN
resources/l3sea.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 179 B |
@@ -1,18 +1,212 @@
|
||||
#include <game/agame/background.hpp>
|
||||
#include <game/agame/player.hpp>
|
||||
#include <window/window.hpp>
|
||||
#include <state/gamestate.hpp>
|
||||
#include <object/camera.hpp>
|
||||
#include <object/components/boxcollider.hpp>
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
#include <utils.hpp>
|
||||
|
||||
namespace {
|
||||
constexpr const char* kLeaderboardFile = "leaderboard.bin";
|
||||
constexpr std::size_t kLeaderboardNameCapacity = 24;
|
||||
|
||||
struct LeaderboardEntry {
|
||||
char name[kLeaderboardNameCapacity + 1]{};
|
||||
std::int32_t score = 0;
|
||||
};
|
||||
|
||||
std::string trimLeaderboardName(const std::string& name) {
|
||||
return name.substr(0, kLeaderboardNameCapacity);
|
||||
}
|
||||
|
||||
LeaderboardEntry makeLeaderboardEntry(const std::string& name, int score) {
|
||||
LeaderboardEntry entry{};
|
||||
const std::string truncatedName = trimLeaderboardName(name);
|
||||
std::copy(truncatedName.begin(), truncatedName.end(), entry.name);
|
||||
entry.score = static_cast<std::int32_t>(score);
|
||||
return entry;
|
||||
}
|
||||
|
||||
std::vector<LeaderboardEntry> loadLeaderboardEntries() {
|
||||
std::vector<LeaderboardEntry> entries;
|
||||
|
||||
std::ifstream file(kLeaderboardFile, std::ios::binary);
|
||||
if (!file.is_open()) {
|
||||
return entries;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
LeaderboardEntry entry{};
|
||||
file.read(entry.name, sizeof(entry.name));
|
||||
if (!file) {
|
||||
break;
|
||||
}
|
||||
|
||||
file.read(reinterpret_cast<char*>(&entry.score), sizeof(entry.score));
|
||||
if (!file) {
|
||||
break;
|
||||
}
|
||||
|
||||
entries.push_back(entry);
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
void saveLeaderboardEntries(const std::vector<LeaderboardEntry>& entries) {
|
||||
std::ofstream file(kLeaderboardFile, std::ios::binary | std::ios::trunc);
|
||||
if (!file.is_open()) {
|
||||
WARN("Neuspešno odpiranje leaderboard.bin za pisanje");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& entry : entries) {
|
||||
file.write(entry.name, sizeof(entry.name));
|
||||
file.write(reinterpret_cast<const char*>(&entry.score), sizeof(entry.score));
|
||||
}
|
||||
}
|
||||
|
||||
void upsertLeaderboardEntry(std::vector<LeaderboardEntry>& entries, const std::string& playerName, int score) {
|
||||
const std::string truncatedName = trimLeaderboardName(playerName);
|
||||
entries.erase(
|
||||
std::remove_if(entries.begin(), entries.end(), [&](const LeaderboardEntry& entry) {
|
||||
return truncatedName == entry.name;
|
||||
}),
|
||||
entries.end()
|
||||
);
|
||||
|
||||
entries.push_back(makeLeaderboardEntry(truncatedName, score));
|
||||
|
||||
std::sort(entries.begin(), entries.end(), [](const LeaderboardEntry& left, const LeaderboardEntry& right) {
|
||||
if (left.score != right.score) {
|
||||
return left.score > right.score;
|
||||
}
|
||||
|
||||
return std::string_view(left.name) < std::string_view(right.name);
|
||||
});
|
||||
}
|
||||
|
||||
std::string formatLeaderboardEntries(const std::vector<LeaderboardEntry>& entries) {
|
||||
std::ostringstream stream;
|
||||
stream << "Lestvica:";
|
||||
|
||||
if (entries.empty()) {
|
||||
stream << "\n(brez vpisov)";
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < entries.size(); ++index) {
|
||||
stream << "\n" << (index + 1) << ". " << entries[index].name << " - " << entries[index].score;
|
||||
}
|
||||
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
std::string loadLeaderboardText() {
|
||||
return formatLeaderboardEntries(loadLeaderboardEntries());
|
||||
}
|
||||
|
||||
void refreshLeaderboardHudText() {
|
||||
Game::GameManager::setSharedData("leaderboardText", loadLeaderboardText());
|
||||
}
|
||||
|
||||
void writeFinalScoreFile(int score) {
|
||||
std::vector<LeaderboardEntry> leaderboardEntries = loadLeaderboardEntries();
|
||||
std::string leaderboardPlayerName = Game::GameManager::getSharedData<std::string>("playerName");
|
||||
if (leaderboardPlayerName.empty()) {
|
||||
leaderboardPlayerName = "Player";
|
||||
}
|
||||
|
||||
upsertLeaderboardEntry(leaderboardEntries, leaderboardPlayerName, score);
|
||||
saveLeaderboardEntries(leaderboardEntries);
|
||||
|
||||
std::ofstream file("score.txt", std::ios::trunc);
|
||||
if (!file.is_open()) {
|
||||
WARN("Neuspešno odpiranje score.txt za pisanje");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto now = std::chrono::system_clock::now();
|
||||
const std::time_t nowTime = std::chrono::system_clock::to_time_t(now);
|
||||
std::tm localTime{};
|
||||
#if defined(_WIN32)
|
||||
localtime_s(&localTime, &nowTime);
|
||||
#else
|
||||
localtime_r(&nowTime, &localTime);
|
||||
#endif
|
||||
|
||||
std::string playerName = Game::GameManager::getSharedData<std::string>("playerName");
|
||||
if (playerName.empty()) playerName = "Player";
|
||||
|
||||
file << "Končna statistika igre:\n";
|
||||
file << "Igralec: " << playerName << "\n";
|
||||
file << "Točke: " << score << "\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();
|
||||
}
|
||||
}
|
||||
|
||||
namespace Game::AGame {
|
||||
void Background::start() {
|
||||
mSeaTex = std::make_shared<Game::Renderer::Texture>("../resources/l3sea.png", SDL_GetRenderer(Window::Window::getSDLWindowBackend()), "seaTex");
|
||||
mEnemyTex = std::make_shared<Game::Renderer::Texture>("../resources/l3enemy.png", SDL_GetRenderer(Window::Window::getSDLWindowBackend()), "enemyTex");
|
||||
mTrashTex = std::make_shared<Game::Renderer::Texture>("../resources/l3trash.png", SDL_GetRenderer(Window::Window::getSDLWindowBackend()), "trashTex");
|
||||
mFriendlyTex = std::make_shared<Game::Renderer::Texture>("../resources/l3friendly.png", SDL_GetRenderer(Window::Window::getSDLWindowBackend()), "friendlyTex");
|
||||
GameManager::setSharedData("enemyActiveCount", 0);
|
||||
GameManager::setSharedData("trashActiveCount", 0);
|
||||
GameManager::setSharedData("friendlyActiveCount", 0);
|
||||
GameManager::setSharedData("gameStage", 1);
|
||||
GameManager::setSharedData("gameWon", false);
|
||||
GameManager::setSharedData("gameLost", false);
|
||||
GameManager::setSharedData("leaderboardText", loadLeaderboardText());
|
||||
|
||||
mZIndex = -1; // Ensure background renders behind other entities
|
||||
mTex->setTiled(true); // Set the background texture to be tiled
|
||||
if (mSeaTex) {
|
||||
mSeaTex->setTiled(true);
|
||||
}
|
||||
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
|
||||
// For 1280px window: -640 (left) + 426.67 (1/3) = -213.33
|
||||
mLandBoundaryX = -static_cast<float>(mW) / 2.f + static_cast<float>(mW) / 3.f;
|
||||
GameManager::setSharedData("terrainLandBoundaryX", mLandBoundaryX);
|
||||
GameManager::setSharedData("enemyRevealRadius", 260.f);
|
||||
|
||||
mTransform.scaleX *= 10.f;
|
||||
mTransform.scaleY *= 10.f;
|
||||
@@ -21,76 +215,275 @@ namespace Game::AGame {
|
||||
mTransform.y -= mTex->getHeight() * mTransform.adjustedScaleY() / 2.f;
|
||||
|
||||
LOG("W: " << mW << " H: " << mH);
|
||||
|
||||
// Only spawn level entities during normal gameplay, not during replay
|
||||
if (GameManager::getCurrentGameState() != GameStateEnum::REPLAY) {
|
||||
spawnLevel(1);
|
||||
}
|
||||
}
|
||||
|
||||
mTransform.x = mW / 2.f - (mW / 3.f);
|
||||
mTransform.y = -mH;
|
||||
void Background::render(Game::Renderer::Renderer* renderer, Game::Renderer::RendererConfig config) {
|
||||
if (!renderer || !mTex || !mSeaTex) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float worldLeft = -static_cast<float>(mW) / 2.f;
|
||||
const float worldRight = static_cast<float>(mW) / 2.f;
|
||||
const float worldTop = -static_cast<float>(mH) / 2.f;
|
||||
const float worldBottom = static_cast<float>(mH) / 2.f;
|
||||
|
||||
const float landLeft = worldLeft;
|
||||
const float landRight = mLandBoundaryX;
|
||||
const float seaLeft = mLandBoundaryX;
|
||||
const float seaRight = worldRight;
|
||||
|
||||
auto drawSection = [&](const std::shared_ptr<Game::Renderer::Texture>& tex, float startX, float endX) {
|
||||
if (!tex) {
|
||||
return;
|
||||
}
|
||||
|
||||
float tileW = 0.f;
|
||||
float tileH = 0.f;
|
||||
SDL_GetTextureSize(tex->getSDLTexture(), &tileW, &tileH);
|
||||
tileW *= mTiledScale;
|
||||
tileH *= mTiledScale;
|
||||
if (tileW <= 0.f || tileH <= 0.f) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float screenStartX = startX - config.camX + config.screenW / 2.f;
|
||||
const float screenEndX = endX - config.camX + config.screenW / 2.f;
|
||||
const float screenStartY = worldTop - config.camY + config.screenH / 2.f;
|
||||
const float screenEndY = worldBottom - config.camY + config.screenH / 2.f;
|
||||
|
||||
for (float x = screenStartX; x < screenEndX; x += tileW) {
|
||||
for (float y = screenStartY; y < screenEndY; y += tileH) {
|
||||
SDL_FRect dst{ x, y, tileW, tileH };
|
||||
SDL_RenderTexture(renderer->getSDLRenderer(), tex->getSDLTexture(), nullptr, &dst);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
drawSection(mTex, landLeft, landRight);
|
||||
drawSection(mSeaTex, seaLeft, seaRight);
|
||||
}
|
||||
|
||||
void Background::spawnFriendly(int stage, int count) {
|
||||
const float viewLeft = -mW / 2.f;
|
||||
const float viewRight = mW / 2.f;
|
||||
const float viewTop = -mH / 2.f;
|
||||
const float viewBottom = mH / 2.f;
|
||||
|
||||
Object::Transform tS;
|
||||
tS.x = 0.f;
|
||||
tS.y = 0.f;
|
||||
tS.rotation = 0.f;
|
||||
tS.scaleX = 4.0f;
|
||||
tS.scaleY = 4.0f;
|
||||
|
||||
const float halfFriendlyW = mFriendlyTex->getWidth() * tS.adjustedScaleX() / 2.f;
|
||||
const float halfFriendlyH = mFriendlyTex->getHeight() * tS.adjustedScaleY() / 2.f;
|
||||
|
||||
// Split friendlies: most on land, a smaller number may appear on sea
|
||||
// Decide how many friendlies appear on the sea.
|
||||
// For stage 1 keep them on land; for later stages allow at least one on sea
|
||||
int seaCount = 0;
|
||||
if (stage > 1) {
|
||||
seaCount = std::max(1, count / 3); // roughly one third on sea for later stages
|
||||
}
|
||||
int landCount = count - seaCount;
|
||||
|
||||
// Spawn land friendlies (left side)
|
||||
for (int i = 0; i < landCount; ++i) {
|
||||
tS.x = static_cast<float>(Utils::getUtils().rirng32(static_cast<int>(viewLeft + halfFriendlyW + 25.f), static_cast<int>(mLandBoundaryX - halfFriendlyW - 25.f)));
|
||||
tS.y = static_cast<float>(Utils::getUtils().rirng32(static_cast<int>(viewTop + halfFriendlyH + 100.f), static_cast<int>(viewBottom - halfFriendlyH - 100.f)));
|
||||
auto* friendly = State::GameState::getInstance().addEntity(std::make_unique<AGame::Friendly>("Friendly" + std::to_string(stage) + "_L" + std::to_string(i + 1), mFriendlyTex, tS));
|
||||
if (friendly) {
|
||||
if (auto* collider = friendly->getComponent<Object::Components::BoxCollider>()) {
|
||||
collider->setScale(0.75f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Spawn a smaller number of friendlies on the sea (right side)
|
||||
for (int i = 0; i < seaCount; ++i) {
|
||||
tS.x = static_cast<float>(Utils::getUtils().rirng32(static_cast<int>(mLandBoundaryX + halfFriendlyW + 25.f), static_cast<int>(viewRight - halfFriendlyW - 25.f)));
|
||||
tS.y = static_cast<float>(Utils::getUtils().rirng32(static_cast<int>(viewTop + halfFriendlyH + 100.f), static_cast<int>(viewBottom - halfFriendlyH - 100.f)));
|
||||
auto* friendly = State::GameState::getInstance().addEntity(std::make_unique<AGame::Friendly>("Friendly" + std::to_string(stage) + "_S" + std::to_string(i + 1), mFriendlyTex, tS));
|
||||
if (friendly) {
|
||||
if (auto* collider = friendly->getComponent<Object::Components::BoxCollider>()) {
|
||||
collider->setScale(0.75f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Background::spawnLevel(int stage) {
|
||||
const float viewLeft = -mW / 2.f;
|
||||
const float viewRight = mW / 2.f;
|
||||
const float viewTop = -mH / 2.f;
|
||||
const float viewBottom = mH / 2.f;
|
||||
|
||||
const int enemyCount = 2 + stage * 2;
|
||||
const int trashCount = 4 + stage * 3;
|
||||
const int friendlyCount = 1 + (stage - 1);
|
||||
|
||||
GameManager::setSharedData("gameStage", stage);
|
||||
GameManager::setSharedData("enemyActiveCount", enemyCount);
|
||||
GameManager::setSharedData("trashActiveCount", trashCount);
|
||||
GameManager::setSharedData("friendlyActiveCount", friendlyCount);
|
||||
|
||||
if (stage > 1) {
|
||||
auto* player = GameManager::getEntityByName<Player>("Player");
|
||||
if (player) {
|
||||
player->respawnRandomSea(mLandBoundaryX);
|
||||
}
|
||||
}
|
||||
|
||||
Object::Transform tS;
|
||||
tS.rotation = 0.f;
|
||||
|
||||
tS.scaleX = 4.7f;
|
||||
tS.scaleY = 4.7f;
|
||||
const float halfEnemyW = mEnemyTex->getWidth() * tS.adjustedScaleX() / 2.f;
|
||||
const float halfEnemyH = mEnemyTex->getHeight() * tS.adjustedScaleY() / 2.f;
|
||||
for (int i = 0; i < enemyCount; ++i) {
|
||||
tS.x = static_cast<float>(Utils::getUtils().rirng32(static_cast<int>(viewLeft + halfEnemyW + 25.f), static_cast<int>(mLandBoundaryX - halfEnemyW - 25.f)));
|
||||
tS.y = static_cast<float>(Utils::getUtils().rirng32(static_cast<int>(viewTop + halfEnemyH + 100.f), static_cast<int>(viewBottom - halfEnemyH - 100.f)));
|
||||
GameManager::instantiateEntity(std::make_unique<AGame::Enemy>("Enemy" + std::to_string(stage) + "_" + std::to_string(i + 1), mEnemyTex, tS));
|
||||
}
|
||||
|
||||
tS.scaleX = 5.5f;
|
||||
tS.scaleY = 5.5f;
|
||||
const float halfTrashW = mTrashTex->getWidth() * tS.adjustedScaleX() / 2.f;
|
||||
const float halfTrashH = mTrashTex->getHeight() * tS.adjustedScaleY() / 2.f;
|
||||
for (int i = 0; i < trashCount; ++i) {
|
||||
tS.x = static_cast<float>(Utils::getUtils().rirng32(static_cast<int>(mLandBoundaryX + halfTrashW + 25.f), static_cast<int>(viewRight - halfTrashW - 25.f)));
|
||||
tS.y = static_cast<float>(Utils::getUtils().rirng32(static_cast<int>(viewTop + halfTrashH + 100.f), static_cast<int>(viewBottom - halfTrashH - 100.f)));
|
||||
GameManager::instantiateEntity(std::make_unique<AGame::Trash>("Trash" + std::to_string(stage) + "_" + std::to_string(i + 1), mTrashTex, tS));
|
||||
}
|
||||
|
||||
spawnFriendly(stage, friendlyCount);
|
||||
}
|
||||
|
||||
void Background::spawnTrashAt(const Object::Transform& tS, bool seaOnly) {
|
||||
// Prepare transform and ensure sea-only pop is placed on the sea side
|
||||
Object::Transform t = tS;
|
||||
t.rotation = 0.f;
|
||||
t.scaleX = 5.5f;
|
||||
t.scaleY = 5.5f;
|
||||
|
||||
const float halfTrashW = mTrashTex->getWidth() * t.adjustedScaleX() / 2.f;
|
||||
|
||||
if (seaOnly) {
|
||||
const float minSeaX = mLandBoundaryX + halfTrashW + 25.f;
|
||||
if (t.x < minSeaX) t.x = minSeaX;
|
||||
}
|
||||
|
||||
// Create a unique-ish name for the auto-spawned trash
|
||||
const int id = Utils::getUtils().rirng32(0, 1000000);
|
||||
const std::string name = "Trash_Auto_" + std::to_string(id);
|
||||
GameManager::instantiateEntity(std::make_unique<AGame::Trash>(name, mTrashTex, t));
|
||||
|
||||
// If requested, mark the spawned trash as sea-only
|
||||
if (seaOnly) {
|
||||
auto* tr = GameManager::getEntityByName<AGame::Trash>(name);
|
||||
if (tr) tr->setSeaOnly(true);
|
||||
}
|
||||
|
||||
GameManager::setSharedData("trashActiveCount", GameManager::getSharedData<int>("trashActiveCount") + 1);
|
||||
}
|
||||
|
||||
void Background::update(float deltaTime) {
|
||||
mEnemySpawnTimer += deltaTime;
|
||||
(void)deltaTime;
|
||||
|
||||
int cnt = GameManager::getSharedData<int>("enemyActiveCount");
|
||||
if (mEnemySpawnTimer >= mTimeToSpawn && cnt < 5) {
|
||||
mEnemySpawnTimer = 0.f; // RESET
|
||||
GameManager::setSharedData("enemyActiveCount", cnt + 1);
|
||||
// Spawn Enemy on grass
|
||||
Object::Transform tS;
|
||||
tS.scaleY = 7.f;
|
||||
tS.scaleX = 7.f;
|
||||
tS.rotation = 0.f;
|
||||
|
||||
float camX, camY;
|
||||
Object::Camera::getInstance().getPosition(camX, camY);
|
||||
|
||||
const float halfEnemyW = mEnemyTex->getWidth() * tS.adjustedScaleX() / 2.f;
|
||||
const float halfEnemyH = mEnemyTex->getHeight() * tS.adjustedScaleY() / 2.f;
|
||||
|
||||
const float viewLeft = camX - (mW / 2.f);
|
||||
const float viewRight = camX + (mW / 2.f);
|
||||
const float viewTop = camY - (mH / 2.f);
|
||||
const float viewBottom = camY + (mH / 2.f);
|
||||
|
||||
// Right 1/3 of the currently visible screen, in world coordinates.
|
||||
int spawnMinX = static_cast<int>(viewLeft + (2.f * mW / 3.f) + halfEnemyW);
|
||||
int spawnMaxX = static_cast<int>(viewRight - halfEnemyW - 25.f);
|
||||
int spawnMinY = static_cast<int>(viewTop + halfEnemyH + 100.f);
|
||||
int spawnMaxY = static_cast<int>(viewBottom - halfEnemyH - 100.f);
|
||||
|
||||
// Safety for tiny windows / huge sprites.
|
||||
if (spawnMinX > spawnMaxX) spawnMinX = spawnMaxX = static_cast<int>(camX);
|
||||
if (spawnMinY > spawnMaxY) spawnMinY = spawnMaxY = static_cast<int>(camY);
|
||||
|
||||
tS.x = static_cast<float>(Utils::getUtils().rirng32(spawnMinX, spawnMaxX));
|
||||
tS.y = static_cast<float>(Utils::getUtils().rirng32(spawnMinY, spawnMaxY));
|
||||
GameManager::instantiateEntity(std::make_unique<AGame::Enemy>("Enemy" + std::to_string(cnt + 1), mEnemyTex, tS));
|
||||
|
||||
// Spawn Trash at shoreline
|
||||
tS.scaleX = 5.5f;
|
||||
tS.scaleY = 5.5f;
|
||||
tS.rotation = 0.f;
|
||||
tS.x = mTransform.x - 75.f;
|
||||
tS.y = static_cast<float>(Utils::getUtils().rirng32(spawnMinY, spawnMaxY));
|
||||
GameManager::instantiateEntity(std::make_unique<AGame::Trash>("Trash" + std::to_string(cnt + 1), mTrashTex, tS));
|
||||
if (GameManager::getSharedData<bool>("gameLost")) {
|
||||
refreshLeaderboardHudText();
|
||||
return;
|
||||
}
|
||||
|
||||
/*const bool* state = SDL_GetKeyboardState(nullptr);
|
||||
if (state[SDL_SCANCODE_P]) {
|
||||
mTransform.scaleX *= 2.f;
|
||||
mTransform.scaleY *= 2.f;
|
||||
if (GameManager::getSharedData<bool>("gameWon")) {
|
||||
refreshLeaderboardHudText();
|
||||
return;
|
||||
}
|
||||
if (state[SDL_SCANCODE_L]) {
|
||||
mTransform.scaleX *= 0.5f;
|
||||
mTransform.scaleY *= 0.5f;
|
||||
}*/
|
||||
//mTransform.rotation += 1.f; // Rotate clockwise for testing
|
||||
//mTransform.scaleX = 1.f + 1.f * std::sin(RUNNING_TIME() / 0.5f); // Pulsate scale for testing
|
||||
//mTransform.scaleY = 1.f + 0.5f * std::cos(RUNNING_TIME() / 0.5f); // Pulsate scale for testing
|
||||
|
||||
//Object::Camera::getInstance().move(1.f, 0.f);
|
||||
const int enemyCount = GameManager::getSharedData<int>("enemyActiveCount");
|
||||
const int trashCount = GameManager::getSharedData<int>("trashActiveCount");
|
||||
const int stage = GameManager::getSharedData<int>("gameStage");
|
||||
|
||||
// Periodically spawn a friendly on land or sea with a small probability
|
||||
// evaluated each update using deltaTime so the average interval is respected.
|
||||
const int activeFriendlies = GameManager::getSharedData<int>("friendlyActiveCount");
|
||||
if (activeFriendlies < mMaxAutoFriendlies) {
|
||||
// Compute chance = deltaTime / avgInterval
|
||||
const float chance = deltaTime / std::max(0.0001f, mFriendlySpawnAvgInterval);
|
||||
const int thresh = static_cast<int>(chance * 10000.f);
|
||||
if (thresh > 0) {
|
||||
const int roll = Utils::getUtils().rirng32(0, 9999);
|
||||
if (roll < thresh) {
|
||||
// Decide side: sea probability increases with stage
|
||||
float seaProb = (stage > 1) ? 0.3f : 0.1f;
|
||||
const int sideRoll = Utils::getUtils().rirng32(0, 99);
|
||||
const bool spawnSea = sideRoll < static_cast<int>(seaProb * 100.f);
|
||||
|
||||
Object::Transform tS;
|
||||
tS.x = 0.f;
|
||||
tS.y = 0.f;
|
||||
tS.rotation = 0.f;
|
||||
tS.scaleX = 4.0f;
|
||||
tS.scaleY = 4.0f;
|
||||
const float viewLeft = -mW / 2.f;
|
||||
const float viewRight = mW / 2.f;
|
||||
const float viewTop = -mH / 2.f;
|
||||
const float viewBottom = mH / 2.f;
|
||||
const float halfFriendlyW = mFriendlyTex->getWidth() * tS.adjustedScaleX() / 2.f;
|
||||
const float halfFriendlyH = mFriendlyTex->getHeight() * tS.adjustedScaleY() / 2.f;
|
||||
|
||||
if (!spawnSea) {
|
||||
tS.x = static_cast<float>(Utils::getUtils().rirng32(static_cast<int>(viewLeft + halfFriendlyW + 25.f), static_cast<int>(mLandBoundaryX - halfFriendlyW - 25.f)));
|
||||
} else {
|
||||
tS.x = static_cast<float>(Utils::getUtils().rirng32(static_cast<int>(mLandBoundaryX + halfFriendlyW + 25.f), static_cast<int>(viewRight - halfFriendlyW - 25.f)));
|
||||
}
|
||||
tS.y = static_cast<float>(Utils::getUtils().rirng32(static_cast<int>(viewTop + halfFriendlyH + 100.f), static_cast<int>(viewBottom - halfFriendlyH - 100.f)));
|
||||
|
||||
const int id = Utils::getUtils().rirng32(0, 1000000);
|
||||
const std::string name = std::string("Friendly_Auto_") + std::to_string(id);
|
||||
auto* friendly = State::GameState::getInstance().addEntity(std::make_unique<AGame::Friendly>(name, mFriendlyTex, tS));
|
||||
if (friendly) {
|
||||
if (auto* collider = friendly->getComponent<Object::Components::BoxCollider>()) {
|
||||
collider->setScale(0.75f);
|
||||
}
|
||||
GameManager::setSharedData("friendlyActiveCount", GameManager::getSharedData<int>("friendlyActiveCount") + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mPendingLevelSpawn) {
|
||||
if (enemyCount <= 0 && trashCount <= 0) {
|
||||
GameManager::processPendingEntityRemovals();
|
||||
mPendingLevelSpawn = false;
|
||||
spawnLevel(mPendingLevelStage);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (enemyCount <= 0 && trashCount <= 0) {
|
||||
if (stage < mMaxLevels) {
|
||||
mPendingLevelSpawn = true;
|
||||
mPendingLevelStage = stage + 1;
|
||||
} else if (!GameManager::getSharedData<bool>("gameWon")) {
|
||||
writeFinalScoreFile(GameManager::getSharedData<int>("gameScore"));
|
||||
refreshLeaderboardHudText();
|
||||
GameManager::setSharedData("gameWon", true);
|
||||
LOG("Vsi nivoji so zaključeni");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Background::onWindowResized(int newWidth, int newHeight) {
|
||||
mW = newWidth;
|
||||
mH = newHeight;
|
||||
// Always maintain logical world dimensions (1280×720)
|
||||
mW = 1280;
|
||||
mH = 720;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,175 @@
|
||||
#include <game/agame/enemy.hpp>
|
||||
#include <game/agame/background.hpp>
|
||||
#include <object/components/boxcollider.hpp>
|
||||
#include <game/agame/player.hpp>
|
||||
#include <state/gamestate.hpp>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <utils.hpp>
|
||||
#include <game/gamemanager.hpp>
|
||||
#include <window/window.hpp>
|
||||
|
||||
namespace Game::AGame {
|
||||
void Enemy::start() {
|
||||
mZIndex = 20;
|
||||
addComponent<Object::Components::BoxCollider>();
|
||||
LOG("Enemy started: " << getName());
|
||||
LOG("Sovražnik zagnan: " << getName());
|
||||
|
||||
// Initialize random movement
|
||||
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;
|
||||
}
|
||||
|
||||
void Enemy::update(float deltaTime) {
|
||||
return;
|
||||
(void)deltaTime;
|
||||
|
||||
auto* player = GameManager::getEntityByName<Player>("Player");
|
||||
if (!player) {
|
||||
mIsVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Enemies are visible only within a reveal radius around the player
|
||||
const float revealRadius = GameManager::getSharedData<float>("enemyRevealRadius");
|
||||
const float px = player->getTransform()->x + (player->getTexture() ? player->getTexture()->getWidth() * player->getTransform()->adjustedScaleX() / 2.f : 0.f);
|
||||
const float py = player->getTransform()->y + (player->getTexture() ? player->getTexture()->getHeight() * player->getTransform()->adjustedScaleY() / 2.f : 0.f);
|
||||
const float ew = getTexture() ? getTexture()->getWidth() * mTransform.adjustedScaleX() : 0.f;
|
||||
const float eh = getTexture() ? getTexture()->getHeight() * mTransform.adjustedScaleY() : 0.f;
|
||||
const float ex = mTransform.x + ew / 2.f;
|
||||
const float ey = mTransform.y + eh / 2.f;
|
||||
const float dxv = px - ex;
|
||||
const float dyv = py - ey;
|
||||
mIsVisible = (dxv * dxv + dyv * dyv) <= (revealRadius * revealRadius);
|
||||
|
||||
// Check if player is on land (not in ship mode) and within follow distance
|
||||
const float distanceToPlayer = std::sqrt(dxv * dxv + dyv * dyv);
|
||||
const bool playerOnLand = !player->isShipMode();
|
||||
const bool withinFollowRange = distanceToPlayer <= FOLLOW_DISTANCE;
|
||||
|
||||
if (playerOnLand && withinFollowRange) {
|
||||
// Follow player: calculate direction and move at constant speed
|
||||
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
|
||||
mTransform.x += mMoveSpeedX * deltaTime;
|
||||
mTransform.y += mMoveSpeedY * deltaTime;
|
||||
|
||||
// Decrease shoreline-spawn cooldown
|
||||
if (mShoreSpawnCooldown > 0.f) mShoreSpawnCooldown = std::max(0.f, mShoreSpawnCooldown - deltaTime);
|
||||
|
||||
// Clamp to land section
|
||||
const float landBoundaryX = GameManager::getSharedData<float>("terrainLandBoundaryX");
|
||||
const float entityWidth = getTexture() ? getTexture()->getWidth() * mTransform.adjustedScaleX() : 0.f;
|
||||
const float entityHeight = getTexture() ? getTexture()->getHeight() * mTransform.adjustedScaleY() : 0.f;
|
||||
const float halfWidth = entityWidth / 2.f;
|
||||
const float halfHeight = entityHeight / 2.f;
|
||||
|
||||
// Use logical world dimensions (1280×720) not actual screen size
|
||||
constexpr int w = 1280;
|
||||
constexpr int h = 720;
|
||||
const float leftEdge = -w / 2.f + 25.f;
|
||||
|
||||
if (mTransform.x - halfWidth < leftEdge) {
|
||||
mTransform.x = leftEdge + halfWidth;
|
||||
mMoveSpeedX = std::abs(mMoveSpeedX);
|
||||
}
|
||||
if (mTransform.x + halfWidth > landBoundaryX - 25.f) {
|
||||
mTransform.x = landBoundaryX - 25.f - halfWidth;
|
||||
mMoveSpeedX = -std::abs(mMoveSpeedX);
|
||||
|
||||
// Enemy hit the shoreline on the right side; spawn a trash on the sea side
|
||||
if (mShoreSpawnCooldown <= 0.f) {
|
||||
auto* bg = GameManager::getEntityByName<AGame::Background>("BG");
|
||||
if (bg) {
|
||||
Object::Transform t;
|
||||
t.rotation = 0.f;
|
||||
t.scaleX = 5.5f;
|
||||
t.scaleY = 5.5f;
|
||||
// Place trash on sea side at same Y
|
||||
t.y = mTransform.y;
|
||||
t.x = landBoundaryX + 10.f; // will be adjusted inside spawnTrashAt
|
||||
bg->spawnTrashAt(t, true);
|
||||
}
|
||||
mShoreSpawnCooldown = 3.0f; // 3 second cooldown per enemy
|
||||
}
|
||||
}
|
||||
if (mTransform.y - halfHeight < -h / 2.f + 25.f) {
|
||||
mTransform.y = -h / 2.f + 25.f + halfHeight;
|
||||
mMoveSpeedY = std::abs(mMoveSpeedY);
|
||||
}
|
||||
if (mTransform.y + halfHeight > h / 2.f - 25.f) {
|
||||
mTransform.y = h / 2.f - 25.f - halfHeight;
|
||||
mMoveSpeedY = -std::abs(mMoveSpeedY);
|
||||
}
|
||||
}
|
||||
|
||||
bool Enemy::hasAdjacentEnemy() {
|
||||
if (!getTexture()) return false;
|
||||
|
||||
const float detectionRadius = 40.f;
|
||||
const float enemyWidth = getTexture()->getWidth() * mTransform.adjustedScaleX();
|
||||
const float enemyHeight = getTexture()->getHeight() * mTransform.adjustedScaleY();
|
||||
const float centerX = mTransform.x + enemyWidth / 2.f;
|
||||
const float centerY = mTransform.y + enemyHeight / 2.f;
|
||||
|
||||
auto entities = GameManager::getEntityByName<Object::Entity>("Dummy");
|
||||
if (!entities) {
|
||||
auto snapshot = State::GameState::getInstance().getEntitiesSnapshot();
|
||||
for (auto* other : snapshot) {
|
||||
if (!other || other == this || !dynamic_cast<Enemy*>(other)) continue;
|
||||
auto* otherEnemy = dynamic_cast<Enemy*>(other);
|
||||
if (!otherEnemy || !otherEnemy->getTexture()) continue;
|
||||
const float otherWidth = otherEnemy->getTexture()->getWidth() * otherEnemy->getTransform()->adjustedScaleX();
|
||||
const float otherHeight = otherEnemy->getTexture()->getHeight() * otherEnemy->getTransform()->adjustedScaleY();
|
||||
const float otherCenterX = otherEnemy->getTransform()->x + otherWidth / 2.f;
|
||||
const float otherCenterY = otherEnemy->getTransform()->y + otherHeight / 2.f;
|
||||
const float dx = centerX - otherCenterX;
|
||||
const float dy = centerY - otherCenterY;
|
||||
if (dx * dx + dy * dy <= detectionRadius * detectionRadius) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Enemy::onCollisionEnter(Object::Entity* other) {
|
||||
LOG("Enemy '" << getName() << "' collided with '" << other->getName() << "' (onCollisionEnter); Killing myself now!");
|
||||
GameManager::setSharedData("enemyActiveCount", GameManager::getSharedData<int>("enemyActiveCount") - 1);
|
||||
|
||||
// Find in state
|
||||
auto* player = dynamic_cast<Player*>(other);
|
||||
if (!player || !mIsVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasAdjacentEnemy()) {
|
||||
LOG("Igralec je trčil v močno skupino onesnaževalcev; konec igre!");
|
||||
GameManager::setSharedData("gameLost", true);
|
||||
GameManager::destroyEntity(player);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG("Sovražnik '" << getName() << "' je trčil v igralca; odstranjujem onesnaževalca in dodeljujem točke");
|
||||
GameManager::setSharedData("enemyActiveCount", std::max(0, GameManager::getSharedData<int>("enemyActiveCount") - 1));
|
||||
GameManager::setSharedData("gameScore", GameManager::getSharedData<int>("gameScore") + 100);
|
||||
GameManager::destroyEntity(this);
|
||||
}
|
||||
}
|
||||
164
src/game/agame/friendly.cpp
Normal file
164
src/game/agame/friendly.cpp
Normal file
@@ -0,0 +1,164 @@
|
||||
#include <game/agame/friendly.hpp>
|
||||
#include <game/gamemanager.hpp>
|
||||
#include <game/agame/player.hpp>
|
||||
#include <game/agame/trash.hpp>
|
||||
#include <object/components/boxcollider.hpp>
|
||||
#include <state/gamestate.hpp>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <utils.hpp>
|
||||
#include <window/window.hpp>
|
||||
|
||||
namespace Game::AGame {
|
||||
void Friendly::start() {
|
||||
mZIndex = 20;
|
||||
addComponent<Object::Components::BoxCollider>();
|
||||
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;
|
||||
|
||||
if (!mOnSea) {
|
||||
// Initialize random movement for land friendlies
|
||||
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;
|
||||
} else {
|
||||
mMoveSpeedX = 0.f;
|
||||
mMoveSpeedY = 0.f;
|
||||
}
|
||||
mDirectionChangeTimer = 0.f;
|
||||
}
|
||||
|
||||
void Friendly::update(float 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;
|
||||
|
||||
if (distanceSquared <= CLEANUP_RADIUS * CLEANUP_RADIUS) {
|
||||
// Clean up this trash: award points and remove it
|
||||
GameManager::setSharedData("gameScore", GameManager::getSharedData<int>("gameScore") + CLEANUP_SCORE_BONUS);
|
||||
GameManager::setSharedData("trashActiveCount", std::max(0, GameManager::getSharedData<int>("trashActiveCount") - 1));
|
||||
GameManager::destroyEntity(trash);
|
||||
}
|
||||
}
|
||||
|
||||
// Move friendly
|
||||
mTransform.x += mMoveSpeedX * deltaTime;
|
||||
mTransform.y += mMoveSpeedY * deltaTime;
|
||||
|
||||
// Clamp to land section
|
||||
const float halfWidth = fw / 2.f;
|
||||
const float halfHeight = fh / 2.f;
|
||||
|
||||
// Use logical world dimensions (1280×720) not actual screen size
|
||||
constexpr int w = 1280;
|
||||
constexpr int h = 720;
|
||||
const float leftEdge = -w / 2.f + 25.f;
|
||||
|
||||
if (mTransform.x - halfWidth < leftEdge) {
|
||||
mTransform.x = leftEdge + halfWidth;
|
||||
mMoveSpeedX = std::abs(mMoveSpeedX);
|
||||
}
|
||||
if (!mOnSea && mTransform.x + halfWidth > landBoundaryX - 25.f) {
|
||||
mTransform.x = landBoundaryX - 25.f - halfWidth;
|
||||
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) {
|
||||
mTransform.y = -h / 2.f + 25.f + halfHeight;
|
||||
mMoveSpeedY = std::abs(mMoveSpeedY);
|
||||
}
|
||||
if (mTransform.y + halfHeight > h / 2.f - 25.f) {
|
||||
mTransform.y = h / 2.f - 25.f - halfHeight;
|
||||
mMoveSpeedY = -std::abs(mMoveSpeedY);
|
||||
}
|
||||
}
|
||||
|
||||
void Friendly::onCollisionEnter(Object::Entity* other) {
|
||||
auto* player = dynamic_cast<Player*>(other);
|
||||
if (!player) {
|
||||
return;
|
||||
}
|
||||
|
||||
GameManager::setSharedData("friendlyActiveCount", std::max(0, GameManager::getSharedData<int>("friendlyActiveCount") - 1));
|
||||
GameManager::setSharedData("gameScore", GameManager::getSharedData<int>("gameScore") - 50);
|
||||
GameManager::destroyEntity(this);
|
||||
}
|
||||
}
|
||||
78
src/game/agame/hudtext.cpp
Normal file
78
src/game/agame/hudtext.cpp
Normal file
@@ -0,0 +1,78 @@
|
||||
#include <game/agame/hudtext.hpp>
|
||||
#include <game/gamemanager.hpp>
|
||||
#include <window/window.hpp>
|
||||
#include <sstream>
|
||||
|
||||
namespace Game::AGame {
|
||||
void HUDText::start() {
|
||||
mZIndex = 1000;
|
||||
Object::UIText::start();
|
||||
setText("Level 1 | Score 0 | Trash 0 | Polluters 0");
|
||||
}
|
||||
|
||||
void HUDText::update(float deltaTime) {
|
||||
(void)deltaTime;
|
||||
|
||||
// Use logical world dimensions (1280×720) not actual screen size
|
||||
constexpr int windowW = 1280;
|
||||
constexpr int windowH = 720;
|
||||
float camX = 0.f;
|
||||
float camY = 0.f;
|
||||
Object::Camera::getInstance().getPosition(camX, camY);
|
||||
|
||||
auto anchorTopRight = [&]() {
|
||||
if (!mTex) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float marginX = 24.f;
|
||||
const float marginY = 24.f;
|
||||
const float textWidth = mTex->getWidth() * mTransform.adjustedScaleX();
|
||||
mTransform.x = camX + windowW / 2.f - marginX - textWidth;
|
||||
mTransform.y = camY - windowH / 2.f + marginY;
|
||||
};
|
||||
|
||||
if (GameManager::getSharedData<bool>("gameLost")) {
|
||||
const std::string leaderboardText = GameManager::getSharedData<std::string>("leaderboardText");
|
||||
std::string endText = "Umrl si!";
|
||||
if (!leaderboardText.empty()) {
|
||||
endText += "\n\n" + leaderboardText;
|
||||
}
|
||||
|
||||
if (getText() != endText) {
|
||||
Window::Window::postToMainThread([this, endText]() { setText(endText); });
|
||||
}
|
||||
anchorTopRight();
|
||||
return;
|
||||
}
|
||||
|
||||
if (GameManager::getSharedData<bool>("gameWon")) {
|
||||
const std::string leaderboardText = GameManager::getSharedData<std::string>("leaderboardText");
|
||||
std::string endText = "Zmagal si!";
|
||||
if (!leaderboardText.empty()) {
|
||||
endText += "\n\n" + leaderboardText;
|
||||
}
|
||||
|
||||
if (getText() != endText) {
|
||||
Window::Window::postToMainThread([this, endText]() { setText(endText); });
|
||||
}
|
||||
anchorTopRight();
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string playerName = GameManager::getSharedData<std::string>("playerName");
|
||||
|
||||
std::stringstream stream;
|
||||
stream << "Igralec: " << (playerName.empty() ? std::string("Anonimni") : playerName)
|
||||
<< " | Level " << GameManager::getSharedData<int>("gameStage")
|
||||
<< " | Točke " << GameManager::getSharedData<int>("gameScore")
|
||||
<< " | Smeti " << GameManager::getSharedData<int>("trashActiveCount")
|
||||
<< " | Sovražniki " << GameManager::getSharedData<int>("enemyActiveCount");
|
||||
|
||||
const std::string newHudText = stream.str();
|
||||
if (getText() != newHudText) {
|
||||
Window::Window::postToMainThread([this, newHudText]() { setText(newHudText); });
|
||||
}
|
||||
anchorTopRight();
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,60 @@
|
||||
#include <cmath>
|
||||
#include <game/input.hpp>
|
||||
#include <game/gamemanager.hpp>
|
||||
#include <utils.hpp>
|
||||
|
||||
namespace Game::AGame {
|
||||
void Player::setShipTexture(std::shared_ptr<Game::Renderer::Texture> tex) {
|
||||
mShipTex = std::move(tex);
|
||||
if (mIsShipMode && mShipTex) {
|
||||
setTexture(mShipTex);
|
||||
}
|
||||
}
|
||||
|
||||
void Player::setGroundTexture(std::shared_ptr<Game::Renderer::Texture> tex) {
|
||||
mGroundTex = std::move(tex);
|
||||
if (!mIsShipMode && mGroundTex) {
|
||||
setTexture(mGroundTex);
|
||||
}
|
||||
}
|
||||
|
||||
void Player::respawnRandomSea(float landBoundaryX) {
|
||||
auto spawnTex = mShipTex ? mShipTex : mTex;
|
||||
if (!spawnTex) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use logical world dimensions (1280×720) not actual screen size
|
||||
constexpr int w = 1280;
|
||||
constexpr int h = 720;
|
||||
|
||||
const float halfWidth = spawnTex->getWidth() * mTransform.adjustedScaleX() / 2.f;
|
||||
const float halfHeight = spawnTex->getHeight() * mTransform.adjustedScaleY() / 2.f;
|
||||
|
||||
const float minCenterX = landBoundaryX + mShoreMargin + halfWidth;
|
||||
const float maxCenterX = w / 2.f - halfWidth - 10.f;
|
||||
const float minCenterY = -h / 2.f + halfHeight;
|
||||
const float maxCenterY = h / 2.f - halfHeight;
|
||||
|
||||
float centerX = minCenterX;
|
||||
if (maxCenterX > minCenterX) {
|
||||
centerX = static_cast<float>(Utils::getUtils().rirng32(static_cast<int>(minCenterX), static_cast<int>(maxCenterX)));
|
||||
}
|
||||
|
||||
float centerY = 0.f;
|
||||
if (maxCenterY > minCenterY) {
|
||||
centerY = static_cast<float>(Utils::getUtils().rirng32(static_cast<int>(minCenterY), static_cast<int>(maxCenterY)));
|
||||
}
|
||||
|
||||
mTransform.x = centerX - halfWidth;
|
||||
mTransform.y = centerY - halfHeight;
|
||||
|
||||
mIsShipMode = true;
|
||||
if (mShipTex) {
|
||||
setTexture(mShipTex);
|
||||
}
|
||||
}
|
||||
|
||||
void Player::start() {
|
||||
//mSound = Object::Sound("../resources/example.wav", Object::Format::WAV);
|
||||
//mSound.play();
|
||||
@@ -12,15 +64,41 @@ namespace Game::AGame {
|
||||
Game::GameManager::setSharedData("gameStage", 1);
|
||||
Game::GameManager::setSharedData("gameScore", 0);
|
||||
|
||||
int w, h;
|
||||
SDL_GetWindowSizeInPixels(Window::Window::getSDLWindowBackend(), &w, &h);
|
||||
mTransform.scaleX = 5.3f;
|
||||
mTransform.scaleY = 5.3f;
|
||||
|
||||
mTransform.scaleX = 8.f;
|
||||
mTransform.scaleY = 8.f;
|
||||
if (!mShipTex) {
|
||||
mShipTex = mTex;
|
||||
}
|
||||
if (!mGroundTex) {
|
||||
mGroundTex = mTex;
|
||||
}
|
||||
|
||||
// Use logical world dimensions (1280×720) not actual screen size
|
||||
constexpr int w = 1280;
|
||||
constexpr int h = 720;
|
||||
|
||||
const float halfWidth = mTex->getWidth() * mTransform.adjustedScaleX() / 2.f;
|
||||
const float halfHeight = mTex->getHeight() * mTransform.adjustedScaleY() / 2.f;
|
||||
const float minX = -w / 2.f + halfWidth;
|
||||
const float maxX = w / 2.f - halfWidth;
|
||||
const float minY = -h / 2.f + halfHeight;
|
||||
const float maxY = h / 2.f - halfHeight;
|
||||
|
||||
const float landBoundaryX = Game::GameManager::getSharedData<float>("terrainLandBoundaryX");
|
||||
mTransform.x = static_cast<float>(Utils::getUtils().rirng32(static_cast<int>(minX), static_cast<int>(maxX)));
|
||||
mTransform.y = static_cast<float>(Utils::getUtils().rirng32(static_cast<int>(minY), static_cast<int>(maxY)));
|
||||
mIsShipMode = (mTransform.x + halfWidth) >= landBoundaryX;
|
||||
|
||||
mTransform.x -= mTex->getWidth() * mTransform.adjustedScaleX() / 2.f;
|
||||
mTransform.y -= mTex->getHeight() * mTransform.adjustedScaleY() / 2.f;
|
||||
|
||||
if (mIsShipMode && mShipTex) {
|
||||
setTexture(mShipTex);
|
||||
} else if (!mIsShipMode && mGroundTex) {
|
||||
setTexture(mGroundTex);
|
||||
}
|
||||
|
||||
LOG("W: " << w << " H: " << h);
|
||||
//mSound.~Sound();
|
||||
}
|
||||
@@ -31,11 +109,68 @@ namespace Game::AGame {
|
||||
//mTransform.scaleY = 1.f + 0.5f * std::cos(RUNNING_TIME() / 0.5f); // Pulsate scale for testing
|
||||
//Object::Camera::getInstance().move(1.f, 0.f);
|
||||
|
||||
if (mStateTransitionCooldownTimer > 0.f) {
|
||||
mStateTransitionCooldownTimer -= deltaTime;
|
||||
if (mStateTransitionCooldownTimer < 0.f) {
|
||||
mStateTransitionCooldownTimer = 0.f;
|
||||
}
|
||||
}
|
||||
|
||||
const float landBoundaryX = Game::GameManager::getSharedData<float>("terrainLandBoundaryX");
|
||||
const float halfWidth = mTex->getWidth() * mTransform.adjustedScaleX() / 2.f;
|
||||
|
||||
if (Input::isKeyPressed(SDL_SCANCODE_E)) {
|
||||
const bool nearShore = std::abs((mTransform.x + halfWidth) - landBoundaryX) <= mShoreMargin;
|
||||
if (nearShore && mStateTransitionCooldownTimer <= 0.f) {
|
||||
mIsShipMode = !mIsShipMode;
|
||||
mStateTransitionCooldownTimer = mStateTransitionCooldown;
|
||||
}
|
||||
}
|
||||
|
||||
// Simple movement
|
||||
if (Input::isKeyPressed(SDL_SCANCODE_W)) { mTransform.y -= mSpeed * deltaTime; }
|
||||
if (Input::isKeyPressed(SDL_SCANCODE_S)) { mTransform.y += mSpeed * deltaTime; }
|
||||
if (Input::isKeyPressed(SDL_SCANCODE_A)) { mTransform.x -= mSpeed * deltaTime; mIsFlipped = false; }
|
||||
if (Input::isKeyPressed(SDL_SCANCODE_D)) { mTransform.x += mSpeed * deltaTime; mIsFlipped = true; }
|
||||
mSpeed = Input::isKeyPressed(SDL_SCANCODE_LSHIFT) ? 400.f : 200.f;
|
||||
|
||||
// Use logical world dimensions (1280×720) not actual screen size
|
||||
constexpr int w = 1280;
|
||||
constexpr int h = 720;
|
||||
const float entityWidth = mTex ? mTex->getWidth() * mTransform.adjustedScaleX() : 0.f;
|
||||
const float entityHeight = mTex ? mTex->getHeight() * mTransform.adjustedScaleY() : 0.f;
|
||||
const float minX = -w / 2.f;
|
||||
const float maxX = w / 2.f - entityWidth;
|
||||
const float minY = -h / 2.f;
|
||||
const float maxY = h / 2.f - entityHeight;
|
||||
|
||||
if (mTransform.x < minX) mTransform.x = minX;
|
||||
if (mTransform.x > maxX) mTransform.x = maxX;
|
||||
if (mTransform.y < minY) mTransform.y = minY;
|
||||
if (mTransform.y > maxY) mTransform.y = maxY;
|
||||
|
||||
if (mIsShipMode && (mTransform.x + halfWidth) < landBoundaryX + mShoreMargin) {
|
||||
mTransform.x = landBoundaryX + mShoreMargin - halfWidth;
|
||||
}
|
||||
if (!mIsShipMode && (mTransform.x + halfWidth) > landBoundaryX - mShoreMargin) {
|
||||
mTransform.x = landBoundaryX - mShoreMargin - halfWidth;
|
||||
}
|
||||
|
||||
if (mIsShipMode && mShipTex) {
|
||||
setTexture(mShipTex);
|
||||
} else if (!mIsShipMode && mGroundTex) {
|
||||
setTexture(mGroundTex);
|
||||
}
|
||||
|
||||
// Push replay (position and form state)
|
||||
GameManager::pushPlayerPosition(mTransform);
|
||||
GameManager::pushPlayerFormState(mIsShipMode);
|
||||
}
|
||||
|
||||
void Player::onCollisionEnter(Object::Entity* other) {
|
||||
(void)other;
|
||||
if (GameManager::getSharedData<bool>("gameLost")) {
|
||||
GameManager::destroyEntity(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,40 @@
|
||||
#include <game/agame/trash.hpp>
|
||||
#include <game/agame/player.hpp>
|
||||
#include <object/components/boxcollider.hpp>
|
||||
#include <algorithm>
|
||||
|
||||
namespace Game::AGame {
|
||||
void Trash::start() {
|
||||
mZIndex = 20;
|
||||
addComponent<Object::Components::BoxCollider>();
|
||||
}
|
||||
|
||||
void Trash::update(float deltaTime) {
|
||||
return;
|
||||
(void)deltaTime;
|
||||
/*if (mSeaOnly) {
|
||||
const float landBoundaryX = GameManager::getSharedData<float>("terrainLandBoundaryX");
|
||||
const float margin = 25.f;
|
||||
const float halfWidth = getTexture() ? getTexture()->getWidth() * mTransform.adjustedScaleX() / 2.f : 0.f;
|
||||
if (mTransform.x - halfWidth < landBoundaryX + margin) {
|
||||
mTransform.x = landBoundaryX + margin + halfWidth;
|
||||
}
|
||||
}*/
|
||||
|
||||
// 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) {
|
||||
auto* player = dynamic_cast<Player*>(other);
|
||||
if (!player || !player->isShipMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
GameManager::setSharedData("trashActiveCount", std::max(0, GameManager::getSharedData<int>("trashActiveCount") - 1));
|
||||
GameManager::setSharedData("gameScore", GameManager::getSharedData<int>("gameScore") + 25);
|
||||
GameManager::destroyEntity(this);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,14 @@
|
||||
#include <game/gamemanager.hpp>
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <game/agame/player.hpp>
|
||||
#include <state/gamestate.hpp>
|
||||
|
||||
namespace Game {
|
||||
GameStateEnum GameManager::mCurrentGameState = GameStateEnum::RUNNING;
|
||||
std::vector<GameManager::ReplayFrame> GameManager::mReplayFrames;
|
||||
size_t GameManager::mCurrentReplayFrame = 0;
|
||||
|
||||
void GameManager::run(std::stop_token stopToken) {
|
||||
using namespace std::chrono_literals;
|
||||
@@ -27,19 +33,30 @@ namespace Game {
|
||||
|
||||
try {
|
||||
Input::update(); // Update input states at the start of each frame
|
||||
auto entities = State::GameState::getInstance().getEntitiesSnapshot();
|
||||
for (auto* entity : entities) {
|
||||
if (!entity || !entity->isActive()) {
|
||||
continue;
|
||||
|
||||
// Handle REPLAY state
|
||||
if (mCurrentGameState == GameStateEnum::REPLAY) {
|
||||
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
|
||||
entity->updateComponents(seconds);
|
||||
if (!entity->isActive()) {
|
||||
continue;
|
||||
// Update components first
|
||||
entity->updateComponents(seconds);
|
||||
if (!entity->isActive()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
entity->update(seconds);
|
||||
}
|
||||
|
||||
entity->update(seconds);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
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, float> GameManager::mSharedFloats;
|
||||
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) {
|
||||
if (type == SharedDataType::STRING) {
|
||||
@@ -83,5 +102,80 @@ namespace Game {
|
||||
void GameManager::processPendingEntityRemovals() {
|
||||
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 <window/window.hpp>
|
||||
#include <object/ui/uitextbox.hpp>
|
||||
#include <state/gamestate.hpp>
|
||||
|
||||
namespace Game {
|
||||
const bool* Input::mCurrentKeyStates = nullptr;
|
||||
const bool* Input::mPreviousKeyStates = nullptr;
|
||||
std::vector<Uint8> Input::mPreviousKeyStates = {};
|
||||
int Input::mNumKeys = 0;
|
||||
int Input::mPrevNumKeys = 0;
|
||||
SDL_MouseButtonFlags Input::mCurrentMouseButtonStates = 0;
|
||||
SDL_MouseButtonFlags Input::mPreviousMouseButtonStates = 0;
|
||||
float Input::mMouseX = 0.0f;
|
||||
float Input::mMouseY = 0.0f;
|
||||
std::mutex Input::mTextMutex;
|
||||
std::vector<std::string> Input::mPendingText;
|
||||
|
||||
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);
|
||||
|
||||
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) {
|
||||
if (key < 0 || key >= mNumKeys) return false;
|
||||
return mCurrentKeyStates[key];
|
||||
return mCurrentKeyStates && mCurrentKeyStates[key];
|
||||
}
|
||||
|
||||
bool Input::isKeyJustPressed(SDL_Scancode key) {
|
||||
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) {
|
||||
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) {
|
||||
@@ -51,4 +119,14 @@ namespace Game {
|
||||
float Input::getMouseY() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
261
src/main.cpp
261
src/main.cpp
@@ -5,33 +5,274 @@
|
||||
#include <object/transform.hpp>
|
||||
#include <game/agame/player.hpp>
|
||||
#include <game/agame/background.hpp>
|
||||
#include <game/agame/hudtext.hpp>
|
||||
#include <game/gamemanager.hpp>
|
||||
#include <renderer/renderer.hpp>
|
||||
#include <renderer/texture.hpp>
|
||||
#include <renderer/font.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 <atomic>
|
||||
|
||||
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() {
|
||||
PLNIMP("Letnik3Zadnja - Licenca: LGPLv2.1-only, CC BY-SA 4.0");
|
||||
|
||||
Window::Window window = Window::Window();
|
||||
window.init(1280, 720, "Game Window");
|
||||
|
||||
State::GameState::getInstance().addEntity(std::make_unique<AGame::SampleTextBox>("Sample Text Box", std::make_shared<Game::Renderer::Font>("../resources/roboto.ttf", window.getRenderer()->getSDLRenderer(), 48, "Roboto"), Object::DEFAULT_TRANSFORM, 640.f, 360.f));
|
||||
// Make SDL renderer available to callbacks
|
||||
gSDLRenderer = window.getRenderer()->getSDLRenderer();
|
||||
|
||||
//Object::Transform t1{100.f, 100.f, 0.f, 1.f, 1.f};
|
||||
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));
|
||||
Object::Entity* 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));
|
||||
player->addComponent<Object::Components::BoxCollider>();
|
||||
//State::GameState::getInstance().addEntity(std::make_unique<AGame::Player>("Player2", std::make_shared<Game::Renderer::Font>("../resources/roboto.ttf", window.getRenderer()->getSDLRenderer(), 128, "Roboto"), t1));
|
||||
// Register a custom event used to perform menu -> game transition on the
|
||||
// window/event thread.
|
||||
gStartGameEventType = SDL_RegisterEvents(1);
|
||||
|
||||
// Sample textbox
|
||||
State::GameState::getInstance().addEntity(std::make_unique<AGame::SampleTextBox>("Sample Text Box", std::make_shared<Game::Renderer::Font>("../resources/roboto.ttf", window.getRenderer()->getSDLRenderer(), 48, "Roboto"), Object::DEFAULT_TRANSFORM, 640.f, 360.f));
|
||||
// Separate fonts for each UI element (each font owns its own texture).
|
||||
// Reusing one font object would make all UI labels share one texture.
|
||||
auto titleFont = std::make_shared<Game::Renderer::Font>("../resources/roboto.ttf", gSDLRenderer, 72, "TitleFont");
|
||||
auto nameBoxFont = std::make_shared<Game::Renderer::Font>("../resources/roboto.ttf", gSDLRenderer, 58, "NameBoxFont");
|
||||
auto startButtonFont = std::make_shared<Game::Renderer::Font>("../resources/roboto.ttf", gSDLRenderer, 72, "StartButtonFont");
|
||||
auto replayButtonFont = std::make_shared<Game::Renderer::Font>("../resources/roboto.ttf", gSDLRenderer, 60, "ReplayButtonFont");
|
||||
|
||||
// Determine display resolution and convert to centered logical coordinates for UI layout
|
||||
static constexpr float LOGICAL_WIDTH = 1280.0f;
|
||||
static constexpr float LOGICAL_HEIGHT = 720.0f;
|
||||
|
||||
float displayW = LOGICAL_WIDTH;
|
||||
float displayH = LOGICAL_HEIGHT;
|
||||
|
||||
SDL_Window* sdlWin = Window::Window::getSDLWindowBackend();
|
||||
if (sdlWin) {
|
||||
int windowPixelsW = static_cast<int>(LOGICAL_WIDTH);
|
||||
int windowPixelsH = static_cast<int>(LOGICAL_HEIGHT);
|
||||
SDL_GetWindowSizeInPixels(sdlWin, &windowPixelsW, &windowPixelsH);
|
||||
|
||||
SDL_DisplayID displayId = SDL_GetDisplayForWindow(sdlWin);
|
||||
const SDL_DisplayMode* displayMode = SDL_GetCurrentDisplayMode(displayId);
|
||||
if (displayMode && displayMode->w > 0 && displayMode->h > 0) {
|
||||
displayW = static_cast<float>(displayMode->w);
|
||||
displayH = static_cast<float>(displayMode->h);
|
||||
} else if (windowPixelsW > 0 && windowPixelsH > 0) {
|
||||
displayW = static_cast<float>(windowPixelsW);
|
||||
displayH = static_cast<float>(windowPixelsH);
|
||||
}
|
||||
}
|
||||
|
||||
const float centerDisplayX = displayW * 0.5f;
|
||||
const float centerDisplayY = displayH * 0.5f;
|
||||
const float cx = centerDisplayX * (LOGICAL_WIDTH / displayW) - (LOGICAL_WIDTH * 0.5f);
|
||||
const float cy = centerDisplayY * (LOGICAL_HEIGHT / displayH) - (LOGICAL_HEIGHT * 0.5f);
|
||||
|
||||
LOG("cx: " << cx << ", cy: " << cy);
|
||||
|
||||
// Position UI elements relative to center (cx, cy are center offsets in logical space)
|
||||
const float titleY = cy - 160.f; // Title above center
|
||||
const float textboxY = cy; // Textbox at center
|
||||
const float startButtonY = cy + 120.f; // Start button below center
|
||||
const float replayButtonY = cy + 220.f; // Replay button below start button
|
||||
|
||||
// Title text (centered)
|
||||
auto* title = dynamic_cast<Game::Object::UIText*>(State::GameState::getInstance().addEntity(std::make_unique<Game::Object::UIText>("Title", titleFont, Object::DEFAULT_TRANSFORM, cx, titleY)));
|
||||
if (title) title->setText("Dol s Plastiko!");
|
||||
|
||||
// Name input box (larger and more visible) with a light gray background
|
||||
auto textboxBg = createSolidColorTexture(gSDLRenderer, 400, 70, 180, 180, 180, 220); // Larger, slightly darker gray
|
||||
auto* nameBox = dynamic_cast<Game::Object::UITextBox*>(State::GameState::getInstance().addEntity(std::make_unique<Game::Object::UITextBox>("NameBox", textboxBg, nameBoxFont, Object::DEFAULT_TRANSFORM, cx, textboxY)));
|
||||
if (nameBox) nameBox->setPlaceholder("Vnesi ime...");
|
||||
|
||||
// Start button (below center)
|
||||
auto btnTex = std::dynamic_pointer_cast<Game::Renderer::Texture>(startButtonFont);
|
||||
auto* btnEntity = dynamic_cast<Game::Object::UIButton*>(State::GameState::getInstance().addEntity(std::make_unique<Game::Object::UIButton>("StartButton", btnTex, Object::DEFAULT_TRANSFORM, reinterpret_cast<void*>(&startGameCallback), cx, startButtonY)));
|
||||
if (btnEntity) btnEntity->setText("Začni igro");
|
||||
|
||||
// Replay button (below start button)
|
||||
auto replayBtnTex = std::dynamic_pointer_cast<Game::Renderer::Texture>(replayButtonFont);
|
||||
auto* replayBtnEntity = dynamic_cast<Game::Object::UIButton*>(State::GameState::getInstance().addEntity(std::make_unique<Game::Object::UIButton>("ReplayButton", replayBtnTex, Object::DEFAULT_TRANSFORM, reinterpret_cast<void*>(&replayGameCallback), cx, replayButtonY)));
|
||||
if (replayBtnEntity) replayBtnEntity->setText("Replay");
|
||||
|
||||
window.run();
|
||||
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
namespace Game::Object::Components {
|
||||
BoxCollider::BoxCollider(const BoxCollider& other) : Component(other) {
|
||||
LOG("Copied BoxCollider Component: " << mName);
|
||||
LOG("Kopiran BoxCollider komponenta: " << mName);
|
||||
}
|
||||
|
||||
BoxCollider& BoxCollider::operator=(const BoxCollider& other) {
|
||||
@@ -17,7 +17,7 @@ namespace Game::Object::Components {
|
||||
}
|
||||
|
||||
BoxCollider::BoxCollider(BoxCollider&& other) noexcept : Component(std::move(other)) {
|
||||
LOG("Moved BoxCollider Component: " << mName);
|
||||
LOG("Premaknjena BoxCollider komponenta: " << mName);
|
||||
}
|
||||
|
||||
BoxCollider& BoxCollider::operator=(BoxCollider&& other) noexcept {
|
||||
@@ -50,16 +50,16 @@ namespace Game::Object::Components {
|
||||
float width = 1.f;
|
||||
float height = 1.f;
|
||||
if (const auto tex = thisEntity->getTexture()) {
|
||||
width = tex->getWidth() * transform->scaleX * UNIVERSAL_SCALE_COEFFICIENT;
|
||||
height = tex->getHeight() * transform->scaleY * UNIVERSAL_SCALE_COEFFICIENT;
|
||||
width = tex->getWidth() * transform->scaleX * UNIVERSAL_SCALE_COEFFICIENT * mScale;
|
||||
height = tex->getHeight() * transform->scaleY * UNIVERSAL_SCALE_COEFFICIENT * mScale;
|
||||
} else {
|
||||
width = transform->scaleX * UNIVERSAL_SCALE_COEFFICIENT;
|
||||
height = transform->scaleY * UNIVERSAL_SCALE_COEFFICIENT;
|
||||
width = transform->scaleX * UNIVERSAL_SCALE_COEFFICIENT * mScale;
|
||||
height = transform->scaleY * UNIVERSAL_SCALE_COEFFICIENT * mScale;
|
||||
}
|
||||
width = std::max(1.f, width);
|
||||
height = std::max(1.f, height);
|
||||
|
||||
// Transform position is used as top-left in rendering, so match that convention for collision bounds.
|
||||
// Entity rendering uses top-left transform coordinates; collider must match that space.
|
||||
float left = transform->x;
|
||||
float right = transform->x + width;
|
||||
float top = transform->y;
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Game::Object {
|
||||
Entity::~Entity() = default;
|
||||
|
||||
Entity::Entity(const Entity& other) : mName(other.mName), mTex(other.mTex), mTransform(other.mTransform), mIsActive(other.mIsActive) {
|
||||
LOG("Copied Entity: " << mName);
|
||||
LOG("Kopirana entiteta: " << mName);
|
||||
}
|
||||
|
||||
Entity& Entity::operator=(const Entity& other) {
|
||||
@@ -24,7 +24,7 @@ namespace Game::Object {
|
||||
|
||||
Entity::Entity(Entity&& other) noexcept : mName(std::move(other.mName)), mTex(other.mTex), mTransform(other.mTransform), mIsActive(other.mIsActive) {
|
||||
other.mTex = nullptr;
|
||||
LOG("Moved Entity: " << mName);
|
||||
LOG("Premaknjena entiteta: " << mName);
|
||||
}
|
||||
|
||||
Entity& Entity::operator=(Entity&& other) noexcept {
|
||||
@@ -46,7 +46,7 @@ namespace Game::Object {
|
||||
SDL_GetTextureSize(mTex->getSDLTexture(), &w, &h);
|
||||
|
||||
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;
|
||||
|
||||
// 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_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;
|
||||
|
||||
// Top-left origin; Account for camera position (center the camera on the screen)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
#include <object/ui/uibutton.hpp>
|
||||
#include <window/window.hpp>
|
||||
|
||||
namespace Game::Object {
|
||||
UIButton::UIButton(const std::string& name, std::shared_ptr<Renderer::Texture> texture, const Transform& transform, void* clickFunction, float x, float y)
|
||||
: Entity(name, texture, transform), mClickFunction(clickFunction), mX(x), mY(y) { }
|
||||
|
||||
void UIButton::start() {
|
||||
// Center the button on the position
|
||||
mTransform.x -= mTex->getWidth() * mTransform.adjustedScaleX() / 2.f;
|
||||
mTransform.y -= mTex->getHeight() * mTransform.adjustedScaleY() / 2.f;
|
||||
// Center the button on the requested position
|
||||
mTransform.x = mX - mTex->getWidth() * mTransform.adjustedScaleX() / 2.f;
|
||||
mTransform.y = mY - mTex->getHeight() * mTransform.adjustedScaleY() / 2.f;
|
||||
}
|
||||
|
||||
void UIButton::update(float deltaTime) {
|
||||
@@ -19,8 +20,21 @@ namespace Game::Object {
|
||||
float textTop = mTransform.y;
|
||||
float textBottom = mTransform.y + mTex->getHeight() * mTransform.adjustedScaleY();
|
||||
|
||||
if (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
|
||||
const bool isInside = mouseX >= textLeft && mouseX <= textRight && mouseY >= textTop && mouseY <= textBottom;
|
||||
|
||||
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) {
|
||||
using ClickFnType = void(*)();
|
||||
ClickFnType clickFn = reinterpret_cast<ClickFnType>(mClickFunction);
|
||||
@@ -32,7 +46,9 @@ namespace Game::Object {
|
||||
|
||||
void UIButton::setText(const std::string& 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 {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
#include <object/ui/uitext.hpp>
|
||||
#include <window/window.hpp>
|
||||
|
||||
namespace Game::Object {
|
||||
UIText::UIText(const std::string& name, std::shared_ptr<Renderer::Font> font, const Transform& transform, float x, float y)
|
||||
: Entity(name, font, transform), mX(x), mY(y) { }
|
||||
|
||||
void UIText::start() {
|
||||
// Center the text on the position
|
||||
mTransform.x -= mTex->getWidth() * mTransform.adjustedScaleX() / 2.f;
|
||||
mTransform.y -= mTex->getHeight() * mTransform.adjustedScaleY() / 2.f;
|
||||
// Center the text on the requested position
|
||||
mTransform.x = mX - mTex->getWidth() * mTransform.adjustedScaleX() / 2.f;
|
||||
mTransform.y = mY - mTex->getHeight() * mTransform.adjustedScaleY() / 2.f;
|
||||
}
|
||||
|
||||
void UIText::update(float deltaTime) {} /* {
|
||||
@@ -31,7 +32,15 @@ namespace Game::Object {
|
||||
|
||||
void UIText::setText(const std::string& 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 {
|
||||
|
||||
@@ -1,147 +1,297 @@
|
||||
#include <object/ui/uitextbox.hpp>
|
||||
#include <renderer/renderer.hpp>
|
||||
#include <window/window.hpp>
|
||||
|
||||
namespace Game::Object {
|
||||
|
||||
UITextBox::UITextBox(const std::string& name, std::shared_ptr<Renderer::Font> font,
|
||||
const Transform& transform, float x, float y, UITextboxConfig config)
|
||||
: Entity(name, font, transform), mX(x), mY(y), mConfig(config) { }
|
||||
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)
|
||||
: Entity(name, background, transform), mBackground(background), mFont(font), mX(x), mY(y) {
|
||||
if (!mBackground) {
|
||||
// Fallback to font texture if background is not provided
|
||||
mBackground = std::dynamic_pointer_cast<Renderer::Texture>(mFont);
|
||||
setTexture(mBackground);
|
||||
}
|
||||
}
|
||||
|
||||
void UITextBox::start() {
|
||||
mTransform.x -= mTex->getWidth() * mTransform.adjustedScaleX() / 2.f;
|
||||
mTransform.y -= mTex->getHeight() * mTransform.adjustedScaleY() / 2.f;
|
||||
// Build initial text texture (placeholder if present) so textbox has a
|
||||
// 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();
|
||||
mBoxHeight = static_cast<float>(mTex->getHeight()) * mTransform.adjustedScaleY();
|
||||
// Center the textbox on the requested position
|
||||
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;
|
||||
if (mBoxHeight < mConfig.minHeight) mBoxHeight = mConfig.minHeight;
|
||||
|
||||
refreshVisualText();
|
||||
static char scancodeToChar(SDL_Scancode scancode, bool shift) {
|
||||
// Basic mapping for letters, digits and common symbols
|
||||
if (scancode >= SDL_SCANCODE_A && scancode <= SDL_SCANCODE_Z) {
|
||||
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) {
|
||||
if (!mIsActive) return;
|
||||
|
||||
if (Input::isMouseButtonJustPressed(SDL_BUTTON_LEFT)) {
|
||||
mIsFocused = isMouseInsideBox();
|
||||
mNeedsTextRefresh = true;
|
||||
// Cursor blink
|
||||
mCursorBlinkTimer += deltaTime;
|
||||
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) {
|
||||
// Cursor blink
|
||||
mCursorTimer += deltaTime;
|
||||
if (mCursorTimer >= mConfig.cursorBlinkRate) {
|
||||
mCursorTimer = 0.f;
|
||||
mCursorVisible = !mCursorVisible;
|
||||
mNeedsTextRefresh = true;
|
||||
// Handle special keys
|
||||
if (Input::isKeyJustPressed(SDL_SCANCODE_BACKSPACE)) {
|
||||
if (mCursorIndex > 0 && !mText.empty()) {
|
||||
mText.erase(mCursorIndex - 1, 1);
|
||||
mCursorIndex = std::max<size_t>(0, mCursorIndex - 1);
|
||||
}
|
||||
}
|
||||
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()) {
|
||||
mText.pop_back();
|
||||
mNeedsTextRefresh = true;
|
||||
}
|
||||
// Character insertion from scancodes
|
||||
|
||||
if (Input::isKeyJustPressed(SDL_SCANCODE_RETURN) || Input::isKeyJustPressed(SDL_SCANCODE_KP_ENTER)) {
|
||||
mIsFocused = false;
|
||||
mCursorTimer = 0.f;
|
||||
mCursorVisible = true;
|
||||
mNeedsTextRefresh = true;
|
||||
}
|
||||
bool shift = Input::isKeyPressed(SDL_SCANCODE_LSHIFT) || Input::isKeyPressed(SDL_SCANCODE_RSHIFT);
|
||||
|
||||
for (int key = 0; key < SDL_SCANCODE_COUNT; ++key) {
|
||||
SDL_Scancode scancode = static_cast<SDL_Scancode>(key);
|
||||
if (!Input::isKeyJustPressed(scancode)) continue;
|
||||
|
||||
SDL_Keycode keycode = SDL_GetKeyFromScancode(scancode, SDL_GetModState(), true);
|
||||
if (keycode >= 32 && keycode <= 126) {
|
||||
if (mConfig.maxLength == 0 || static_cast<int>(mText.size()) < mConfig.maxLength) {
|
||||
mText.push_back(static_cast<char>(keycode));
|
||||
mNeedsTextRefresh = true;
|
||||
// Letters A-Z
|
||||
for (int sc = SDL_SCANCODE_A; sc <= SDL_SCANCODE_Z; ++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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
refreshVisualText();
|
||||
// Update rendered text only when needed
|
||||
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) {
|
||||
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;
|
||||
const float by = mTransform.y - mConfig.paddingY - config.camY + config.screenH / 2.f;
|
||||
const float bw = mBoxWidth + 2.f * mConfig.paddingX;
|
||||
const float bh = mBoxHeight + 2.f * mConfig.paddingY;
|
||||
const float t = mConfig.borderThickness;
|
||||
SDL_FPoint center;
|
||||
center.x = dst.w / 2.f;
|
||||
center.y = dst.h / 2.f;
|
||||
|
||||
// Background
|
||||
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_BLEND);
|
||||
const SDL_Color& bg = mConfig.bgColor;
|
||||
SDL_SetRenderDrawColor(r, bg.r, bg.g, bg.b, bg.a);
|
||||
const SDL_FRect bgRect = {bx, by, bw, bh};
|
||||
SDL_RenderFillRect(r, &bgRect);
|
||||
SDL_RenderTextureRotated(
|
||||
renderer->getSDLRenderer(),
|
||||
mBackground->getSDLTexture(),
|
||||
nullptr,
|
||||
&dst,
|
||||
mTransform.rotation,
|
||||
¢er,
|
||||
mIsFlipped ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE
|
||||
);
|
||||
|
||||
// Border (4 filled rects for configurable thickness)
|
||||
const SDL_Color& bc = mIsFocused ? mConfig.focusedBorderColor : mConfig.borderColor;
|
||||
SDL_SetRenderDrawColor(r, bc.r, bc.g, bc.b, bc.a);
|
||||
const SDL_FRect borders[4] = {
|
||||
{bx, by, bw, t }, // top
|
||||
{bx, by + bh - t, bw, t }, // bottom
|
||||
{bx, by, t, bh}, // left
|
||||
{bx + bw - t, by, t, bh}, // right
|
||||
};
|
||||
SDL_RenderFillRects(r, borders, 4);
|
||||
if (mIsHovered) {
|
||||
Uint8 prevR = 0;
|
||||
Uint8 prevG = 0;
|
||||
Uint8 prevB = 0;
|
||||
Uint8 prevA = 0;
|
||||
SDL_BlendMode prevBlendMode = SDL_BLENDMODE_NONE;
|
||||
|
||||
// Text (or placeholder) via base render
|
||||
Entity::render(renderer, config);
|
||||
}
|
||||
SDL_GetRenderDrawColor(renderer->getSDLRenderer(), &prevR, &prevG, &prevB, &prevA);
|
||||
SDL_GetRenderDrawBlendMode(renderer->getSDLRenderer(), &prevBlendMode);
|
||||
|
||||
void UITextBox::setText(const std::string& text) {
|
||||
mText = text;
|
||||
mNeedsTextRefresh = true;
|
||||
refreshVisualText();
|
||||
}
|
||||
SDL_SetRenderDrawBlendMode(renderer->getSDLRenderer(), SDL_BLENDMODE_BLEND);
|
||||
SDL_SetRenderDrawColor(renderer->getSDLRenderer(), 0, 0, 0, 28);
|
||||
SDL_RenderFillRect(renderer->getSDLRenderer(), &dst);
|
||||
|
||||
std::string UITextBox::getText() const { return mText; }
|
||||
std::string UITextBox::getValue() const { return mText; }
|
||||
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 += "_";
|
||||
SDL_SetRenderDrawColor(renderer->getSDLRenderer(), prevR, prevG, prevB, prevA);
|
||||
SDL_SetRenderDrawBlendMode(renderer->getSDLRenderer(), prevBlendMode);
|
||||
}
|
||||
}
|
||||
|
||||
std::dynamic_pointer_cast<Renderer::Font>(mTex)->build(color, rendered);
|
||||
mNeedsTextRefresh = false;
|
||||
// Render font texture (text) on top of background
|
||||
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 <algorithm>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace Game::Renderer {
|
||||
Font::Font(const std::string& path, SDL_Renderer* renderer, int ptSize, std::string id)
|
||||
@@ -36,12 +39,110 @@ namespace Game::Renderer {
|
||||
void Font::build(SDL_Color color, std::string text) {
|
||||
if (!mFont) { return; }
|
||||
|
||||
SDL_Surface* surf = TTF_RenderText_Blended(mFont, text.c_str(), text.size(), color);
|
||||
// Store last build parameters so we can rebuild after device resets
|
||||
mLastText = text;
|
||||
mLastColor = color;
|
||||
|
||||
const int fontLineSkip = TTF_GetFontLineSkip(mFont);
|
||||
std::vector<std::string_view> lines;
|
||||
std::size_t lineStart = 0;
|
||||
while (lineStart <= text.size()) {
|
||||
const std::size_t lineEnd = text.find('\n', lineStart);
|
||||
if (lineEnd == std::string::npos) {
|
||||
lines.emplace_back(text.data() + lineStart, text.size() - lineStart);
|
||||
break;
|
||||
}
|
||||
|
||||
lines.emplace_back(text.data() + lineStart, lineEnd - lineStart);
|
||||
lineStart = lineEnd + 1;
|
||||
if (lineStart == text.size()) {
|
||||
lines.emplace_back(std::string_view{});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (lines.empty()) {
|
||||
lines.emplace_back(std::string_view{});
|
||||
}
|
||||
|
||||
struct LineSurface {
|
||||
SDL_Surface* surface = nullptr;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
std::vector<LineSurface> lineSurfaces;
|
||||
lineSurfaces.reserve(lines.size());
|
||||
|
||||
int maxWidth = 0;
|
||||
int totalHeight = 0;
|
||||
|
||||
for (const auto line : lines) {
|
||||
LineSurface lineSurface{};
|
||||
if (!line.empty()) {
|
||||
int measuredWidth = 0;
|
||||
int measuredHeight = 0;
|
||||
if (!TTF_GetStringSize(mFont, line.data(), line.size(), &measuredWidth, &measuredHeight)) {
|
||||
for (auto& storedLine : lineSurfaces) {
|
||||
if (storedLine.surface) {
|
||||
SDL_DestroySurface(storedLine.surface);
|
||||
}
|
||||
}
|
||||
ERROR("TTF_GetStringSize Error: " << SDL_GetError() << " (This object may be unusuable)");
|
||||
return;
|
||||
}
|
||||
|
||||
lineSurface.surface = TTF_RenderText_Blended(mFont, line.data(), line.size(), color);
|
||||
if (!lineSurface.surface) {
|
||||
for (auto& storedLine : lineSurfaces) {
|
||||
if (storedLine.surface) {
|
||||
SDL_DestroySurface(storedLine.surface);
|
||||
}
|
||||
}
|
||||
ERROR("TTF_RenderText_Blended Error: " << SDL_GetError() << " (This object may be unusuable)");
|
||||
return;
|
||||
}
|
||||
|
||||
lineSurface.width = measuredWidth;
|
||||
lineSurface.height = measuredHeight;
|
||||
} else {
|
||||
lineSurface.width = 0;
|
||||
lineSurface.height = fontLineSkip;
|
||||
}
|
||||
|
||||
maxWidth = std::max(maxWidth, lineSurface.width);
|
||||
totalHeight += lineSurface.height;
|
||||
lineSurfaces.push_back(lineSurface);
|
||||
}
|
||||
|
||||
if (maxWidth <= 0) {
|
||||
maxWidth = 1;
|
||||
}
|
||||
if (totalHeight <= 0) {
|
||||
totalHeight = std::max(1, fontLineSkip);
|
||||
}
|
||||
|
||||
SDL_Surface* surf = SDL_CreateSurface(maxWidth, totalHeight, SDL_PIXELFORMAT_RGBA8888);
|
||||
if (!surf) {
|
||||
ERROR("TTF_RenderText_Blended Error: " << SDL_GetError() << " (This object may be unusuable)");
|
||||
for (auto& storedLine : lineSurfaces) {
|
||||
if (storedLine.surface) {
|
||||
SDL_DestroySurface(storedLine.surface);
|
||||
}
|
||||
}
|
||||
ERROR("SDL_CreateSurface Error: " << SDL_GetError() << " (This object may be unusuable)");
|
||||
return;
|
||||
}
|
||||
|
||||
int currentY = 0;
|
||||
for (auto& lineSurface : lineSurfaces) {
|
||||
if (lineSurface.surface) {
|
||||
SDL_Rect dstRect{0, currentY, lineSurface.width, lineSurface.height};
|
||||
SDL_BlitSurface(lineSurface.surface, nullptr, surf, &dstRect);
|
||||
SDL_DestroySurface(lineSurface.surface);
|
||||
}
|
||||
currentY += lineSurface.height;
|
||||
}
|
||||
|
||||
// Convert to texture
|
||||
mTex = SDL_CreateTextureFromSurface(mRenderer, surf);
|
||||
SDL_DestroySurface(surf);
|
||||
@@ -49,9 +150,28 @@ namespace Game::Renderer {
|
||||
ERROR("SDL_CreateTextureFromSurface Error: " << SDL_GetError() << " (This object may be unusuable)");
|
||||
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.
|
||||
if (!SDL_SetTextureScaleMode(mTex, SDL_SCALEMODE_LINEAR)) {
|
||||
WARN("Failed to set texture scale mode to LINEAR for font '" << mId << "': " << SDL_GetError());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool Font::reload(SDL_Renderer* renderer) {
|
||||
mRenderer = renderer;
|
||||
if (mLastText.empty()) return false;
|
||||
// Rebuild the texture using the stored last text and color
|
||||
build(mLastColor, mLastText);
|
||||
return true;
|
||||
}
|
||||
|
||||
SDL_Texture* Font::getSDLTexture() { return mTex; }
|
||||
std::string Font::getId() { return mId; }
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <renderer/renderer.hpp>
|
||||
#include <algorithm>
|
||||
#include <utils.hpp>
|
||||
#include <state/gamestate.hpp>
|
||||
#include <object/entity.hpp>
|
||||
@@ -16,32 +17,38 @@ namespace Game::Renderer {
|
||||
if (mRenderer) {
|
||||
SDL_DestroyRenderer(mRenderer);
|
||||
mRenderer = nullptr;
|
||||
LOG("Destroyed Renderer");
|
||||
LOG("Renderer uničen");
|
||||
}
|
||||
}
|
||||
|
||||
bool Renderer::init(SDL_Window* window) {
|
||||
// Request VSync before/at renderer setup; some backends honor this hint.
|
||||
SDL_SetHint(SDL_HINT_RENDER_VSYNC, "1");
|
||||
|
||||
// Create renderer using the portable API. Some SDL3 backends may not support the old flags,
|
||||
// so fall back to a software renderer via hint if the first attempt fails.
|
||||
mRenderer = SDL_CreateRenderer(window, nullptr);
|
||||
if (!mRenderer) {
|
||||
std::string errorMsg = "Failed to create renderer: " + std::string(SDL_GetError());
|
||||
ERROR(errorMsg.c_str());
|
||||
return false;
|
||||
WARN("Renderer creation failed, attempting software renderer: " << SDL_GetError());
|
||||
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
|
||||
mRenderer = SDL_CreateRenderer(window, nullptr);
|
||||
if (!mRenderer) {
|
||||
std::string errorMsg = std::string("Neuspešno ustvarjanje rendererja: ") + std::string(SDL_GetError());
|
||||
ERROR(errorMsg.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
mVSyncEnabled = SDL_SetRenderVSync(mRenderer, 1);
|
||||
if (!mVSyncEnabled) {
|
||||
WARN("VSync could not be enabled, using software frame pacing fallback: " << SDL_GetError());
|
||||
WARN("VSync ni mogoče omogočiti, uporabljam programsko omejitev okvirjev: " << SDL_GetError());
|
||||
}
|
||||
|
||||
if (!SDL_SetRenderDrawColor(mRenderer, 0, 0, 255, 255)) {
|
||||
ERROR("Failed to set renderer draw color: " << SDL_GetError());
|
||||
ERROR("Neuspelo nastavitev barve rendererja: " << SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG("Renderer created successfully");
|
||||
|
||||
LOG("Renderer uspešno ustvarjen");
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -53,21 +60,48 @@ namespace Game::Renderer {
|
||||
Object::Camera::getInstance().getPosition(camX, camY);
|
||||
int 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 {
|
||||
auto entities = Game::State::GameState::getInstance().getEntitiesSnapshot(true);
|
||||
for (auto* entity : entities) {
|
||||
if (entity) {
|
||||
entity->render(this, config);
|
||||
Game::State::GameState::getInstance().withEntitiesLocked([&](auto& entities) {
|
||||
std::vector<Game::Object::Entity*> renderOrder;
|
||||
renderOrder.reserve(entities.size());
|
||||
for (auto& [name, entity] : entities) {
|
||||
(void)name;
|
||||
if (entity) {
|
||||
renderOrder.push_back(entity.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(renderOrder.begin(), renderOrder.end(), [](Game::Object::Entity* a, Game::Object::Entity* b) {
|
||||
return a->getZIndex() < b->getZIndex();
|
||||
});
|
||||
|
||||
for (auto* entity : renderOrder) {
|
||||
if (entity && entity->isActive()) {
|
||||
entity->render(this, config);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (const std::exception& e) {
|
||||
ERROR("Exception while rendering frame: " << e.what());
|
||||
}
|
||||
|
||||
mPresent();
|
||||
|
||||
// Reset render scale for next frame
|
||||
SDL_SetRenderScale(mRenderer, 1.0f, 1.0f);
|
||||
}
|
||||
|
||||
void Renderer::mClear() {
|
||||
|
||||
@@ -7,6 +7,9 @@ namespace Game::Renderer {
|
||||
|
||||
Texture::Texture(const std::string& path, SDL_Renderer* renderer, std::string id)
|
||||
: mTex(nullptr), mId(id) {
|
||||
mPath = path;
|
||||
mIsFromFile = true;
|
||||
|
||||
SDL_Surface* surf = IMG_Load(path.c_str());
|
||||
if (!surf) {
|
||||
ERROR("Failed to load image at " << path);
|
||||
@@ -41,7 +44,35 @@ namespace Game::Renderer {
|
||||
Texture::~Texture() {
|
||||
if (mTex)
|
||||
SDL_DestroyTexture(mTex);
|
||||
LOG("Destroyed texture '" << mId << "'")
|
||||
LOG("Tekstura '" << mId << "' uničena")
|
||||
}
|
||||
|
||||
bool Texture::reload(SDL_Renderer* renderer) {
|
||||
if (!mIsFromFile || mPath.empty()) return false;
|
||||
if (mTex) {
|
||||
SDL_DestroyTexture(mTex);
|
||||
mTex = nullptr;
|
||||
}
|
||||
|
||||
SDL_Surface* surf = IMG_Load(mPath.c_str());
|
||||
if (!surf) {
|
||||
ERROR("Failed to reload image at " << mPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
mTex = SDL_CreateTextureFromSurface(renderer, surf);
|
||||
SDL_DestroySurface(surf);
|
||||
if (!mTex) {
|
||||
ERROR("Failed to create texture from surface when reloading " << mPath << ": " << SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Restore scale mode
|
||||
if (!SDL_SetTextureScaleMode(mTex, SDL_SCALEMODE_NEAREST)) {
|
||||
WARN("Failed to set texture scale mode to NEAREST for '" << mId << "' during reload: " << SDL_GetError());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
SDL_Texture* Texture::getSDLTexture() {
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
#include <window/window.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <renderer/texture.hpp>
|
||||
#include <game/input.hpp>
|
||||
|
||||
namespace Game::Window {
|
||||
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) { }
|
||||
|
||||
@@ -14,14 +24,14 @@ namespace Game::Window {
|
||||
if (mGameThread.joinable()) {
|
||||
mGameThread.request_stop();
|
||||
mGameThread.join();
|
||||
LOG("Game thread stopped successfully");
|
||||
LOG("Nit igre uspešno ustavljena");
|
||||
}
|
||||
|
||||
if (mWindow) {
|
||||
SDL_DestroyWindow(mWindow);
|
||||
mWindow = nullptr;
|
||||
sWindowBackend = nullptr;
|
||||
LOG("Window destroyed successfully");
|
||||
LOG("Okno uspešno uničeno");
|
||||
}
|
||||
SDL_Quit();
|
||||
}
|
||||
@@ -40,7 +50,7 @@ namespace Game::Window {
|
||||
|
||||
Audio::Audio::getInstance().init();
|
||||
|
||||
mWindow = SDL_CreateWindow(title.c_str(), width, height, SDL_WINDOW_RESIZABLE);
|
||||
mWindow = SDL_CreateWindow(title.c_str(), width, height, SDL_WINDOW_FULLSCREEN);
|
||||
if (!mWindow) {
|
||||
ERROR("Failed to create window: " << SDL_GetError());
|
||||
SDL_Quit();
|
||||
@@ -50,7 +60,7 @@ namespace Game::Window {
|
||||
mLastWindowHeight = height;
|
||||
sWindowBackend = mWindow;
|
||||
|
||||
LOG("Window created successfully");
|
||||
LOG("Okno uspešno ustvarjeno");
|
||||
|
||||
if (!mRenderer.init(mWindow)) {
|
||||
SDL_DestroyWindow(mWindow);
|
||||
@@ -64,17 +74,17 @@ namespace Game::Window {
|
||||
if (mRenderer.isVSyncEnabled()) {
|
||||
const int vsyncCap = std::max(1, mTargetFPS - VSYNC_FPS_OFFSET);
|
||||
mEffectiveFrameCap = vsyncCap;
|
||||
LOG("Low-latency VSync mode enabled. Target FPS: " << mTargetFPS << ", cap: " << mEffectiveFrameCap);
|
||||
LOG("Low-latency VSync mode vključen. Target FPS: " << mTargetFPS << ", cap: " << mEffectiveFrameCap);
|
||||
} else {
|
||||
mEffectiveFrameCap = std::max(1, mTargetFPS);
|
||||
LOG("VSync unavailable, using software cap: " << mEffectiveFrameCap);
|
||||
LOG("VSync ni na voljo, uporabljam programski limit: " << mEffectiveFrameCap);
|
||||
}
|
||||
#else
|
||||
mEffectiveFrameCap = std::max(1, mTargetFPS);
|
||||
#endif
|
||||
|
||||
mGameManager.setTargetUpdatesPerSecond(TARGET_UPDATE_RATE);
|
||||
LOG("Target updates per second: " << mGameManager.getTargetUpdatesPerSecond());
|
||||
LOG("Ciljna hitrost posodobitev na sekundo: " << mGameManager.getTargetUpdatesPerSecond());
|
||||
|
||||
mGameThread = std::jthread(std::bind_front(&Game::GameManager::run, &mGameManager));
|
||||
|
||||
@@ -87,9 +97,34 @@ namespace Game::Window {
|
||||
SDL_Event event;
|
||||
while (mRunning) {
|
||||
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();
|
||||
while (SDL_PollEvent(&event)) {
|
||||
// Forward SDL text input events to the Input queue
|
||||
if (event.type == SDL_EVENT_TEXT_INPUT) {
|
||||
if (event.text.text && event.text.text[0]) {
|
||||
Game::Input::pushText(std::string(event.text.text));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Run custom callbacks on the window/event thread.
|
||||
if (event.type >= SDL_EVENT_USER && event.user.data1) {
|
||||
using UserEventFn = void(*)();
|
||||
UserEventFn fn = reinterpret_cast<UserEventFn>(event.user.data1);
|
||||
fn();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.type == SDL_EVENT_QUIT) {
|
||||
mRunning = false;
|
||||
}
|
||||
@@ -97,31 +132,56 @@ namespace Game::Window {
|
||||
// Window resize event - update the renderer's viewport
|
||||
if (event.type == SDL_EVENT_WINDOW_RESIZED) {
|
||||
std::scoped_lock lock(mMutex);
|
||||
SDL_SetWindowSize(mWindow, mLastWindowWidth, mLastWindowHeight);
|
||||
SDL_SetRenderViewport(mRenderer.getSDLRenderer(), nullptr);
|
||||
|
||||
int newWidth, newHeight;
|
||||
SDL_GetWindowSizeInPixels(mWindow, &newWidth, &newHeight);
|
||||
|
||||
const int oldWidth = mLastWindowWidth;
|
||||
const int oldHeight = mLastWindowHeight;
|
||||
const bool canScale = oldWidth > 0 && oldHeight > 0;
|
||||
const float scaleX = canScale ? static_cast<float>(newWidth) / static_cast<float>(oldWidth) : 1.f;
|
||||
const float scaleY = canScale ? static_cast<float>(newHeight) / static_cast<float>(oldHeight) : 1.f;
|
||||
|
||||
auto entities = State::GameState::getInstance().getEntitiesSnapshot();
|
||||
for (auto* entity : entities) {
|
||||
if (entity) {
|
||||
if (canScale) {
|
||||
Object::Transform* transform = entity->getTransform();
|
||||
transform->x *= scaleX;
|
||||
transform->y *= scaleY;
|
||||
State::GameState::getInstance().withEntitiesLocked([&](auto& entities) {
|
||||
for (auto& [name, entity] : entities) {
|
||||
(void)name;
|
||||
if (entity) {
|
||||
entity->onWindowResized(mLastWindowWidth, mLastWindowHeight);
|
||||
}
|
||||
entity->onWindowResized(newWidth, newHeight);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Renderer device/targets reset (GPU device removed) - attempt to re-create renderer and reload textures
|
||||
if (event.type == SDL_EVENT_RENDER_DEVICE_RESET || event.type == SDL_EVENT_RENDER_TARGETS_RESET) {
|
||||
LOG("Renderer device reset event received: attempting to reinitialize renderer and reload textures");
|
||||
|
||||
// Destroy current renderer and try to re-init
|
||||
mRenderer.destroy();
|
||||
bool reinitOk = mRenderer.init(mWindow);
|
||||
|
||||
// If reinit failed, try forcing the software renderer
|
||||
if (!reinitOk) {
|
||||
WARN("Renderer re-init failed, forcing software renderer fallback");
|
||||
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
|
||||
reinitOk = mRenderer.init(mWindow);
|
||||
if (!reinitOk) {
|
||||
ERROR("Software renderer fallback also failed: " << SDL_GetError());
|
||||
// Unable to recover; stop running to avoid crashes
|
||||
mRunning = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
mLastWindowWidth = newWidth;
|
||||
mLastWindowHeight = newHeight;
|
||||
// Rebuild GPU textures for all entities (fonts and file-based textures)
|
||||
State::GameState::getInstance().withEntitiesLocked([&](auto& entities) {
|
||||
for (auto& [name, entity] : entities) {
|
||||
(void)name;
|
||||
if (entity) {
|
||||
auto tex = entity->getTexture();
|
||||
if (tex) {
|
||||
try {
|
||||
tex->reload(mRenderer.getSDLRenderer());
|
||||
} catch (...) {
|
||||
WARN("Exception while reloading texture for entity: " << entity->getName());
|
||||
}
|
||||
}
|
||||
entity->onWindowResized(mLastWindowWidth, mLastWindowHeight); // ensure layout is correct
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user