diff --git a/include/game/agame/background.hpp b/include/game/agame/background.hpp index 3ac6a94..d32fbfd 100644 --- a/include/game/agame/background.hpp +++ b/include/game/agame/background.hpp @@ -14,6 +14,9 @@ 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: void spawnLevel(int stage); void spawnFriendly(int stage, int count); @@ -22,6 +25,9 @@ GAME_ENTITY(Background) float mLandBoundaryX = 0.f; bool mPendingLevelSpawn = false; int mPendingLevelStage = 0; + // Periodic friendly spawn settings + float mFriendlySpawnAvgInterval = 6.f; // average seconds between spawns + int mMaxAutoFriendlies = 7; // hard cap for total active friendlies std::shared_ptr mSeaTex; std::shared_ptr mEnemyTex; std::shared_ptr mTrashTex; diff --git a/include/game/agame/enemy.hpp b/include/game/agame/enemy.hpp index 146b226..4369637 100644 --- a/include/game/agame/enemy.hpp +++ b/include/game/agame/enemy.hpp @@ -15,5 +15,6 @@ namespace Game::AGame { float mMoveSpeedX = 0.f; float mMoveSpeedY = 0.f; float mDirectionChangeTimer = 0.f; + float mShoreSpawnCooldown = 0.f; END_GAME_ENTITY() } \ No newline at end of file diff --git a/include/game/agame/trash.hpp b/include/game/agame/trash.hpp index 7445dca..a9f743e 100644 --- a/include/game/agame/trash.hpp +++ b/include/game/agame/trash.hpp @@ -14,5 +14,8 @@ 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() } \ No newline at end of file diff --git a/include/object/components/boxcollider.hpp b/include/object/components/boxcollider.hpp index b0dca7f..78f5da8 100644 --- a/include/object/components/boxcollider.hpp +++ b/include/object/components/boxcollider.hpp @@ -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 mCollidingWith; // Track collisions per-entity so enter/stay/exit callbacks remain correct with multiple colliders }; diff --git a/src/game/agame/background.cpp b/src/game/agame/background.cpp index 01011a7..50ad140 100644 --- a/src/game/agame/background.cpp +++ b/src/game/agame/background.cpp @@ -1,10 +1,43 @@ #include #include #include +#include #include +#include #include +#include +#include +#include +#include #include +namespace { + void writeFinalScoreFile(int score) { + std::ofstream file("score.txt", std::ios::trunc); + if (!file.is_open()) { + WARN("Failed to open score.txt for writing"); + 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("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"; + } +} + namespace Game::AGame { void Background::start() { mSeaTex = std::make_shared("../resources/l3sea.png", SDL_GetRenderer(Window::Window::getSDLWindowBackend()), "seaTex"); @@ -90,6 +123,7 @@ namespace Game::AGame { 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; @@ -100,10 +134,38 @@ namespace Game::AGame { const float halfFriendlyW = mFriendlyTex->getWidth() * tS.adjustedScaleX() / 2.f; const float halfFriendlyH = mFriendlyTex->getHeight() * tS.adjustedScaleY() / 2.f; - for (int i = 0; i < count; ++i) { + + // 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(Utils::getUtils().rirng32(static_cast(viewLeft + halfFriendlyW + 25.f), static_cast(mLandBoundaryX - halfFriendlyW - 25.f))); tS.y = static_cast(Utils::getUtils().rirng32(static_cast(viewTop + halfFriendlyH + 100.f), static_cast(viewBottom - halfFriendlyH - 100.f))); - GameManager::instantiateEntity(std::make_unique("Friendly" + std::to_string(stage) + "_" + std::to_string(i + 1), mFriendlyTex, tS)); + auto* friendly = State::GameState::getInstance().addEntity(std::make_unique("Friendly" + std::to_string(stage) + "_L" + std::to_string(i + 1), mFriendlyTex, tS)); + if (friendly) { + if (auto* collider = friendly->getComponent()) { + 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(Utils::getUtils().rirng32(static_cast(mLandBoundaryX + halfFriendlyW + 25.f), static_cast(viewRight - halfFriendlyW - 25.f))); + tS.y = static_cast(Utils::getUtils().rirng32(static_cast(viewTop + halfFriendlyH + 100.f), static_cast(viewBottom - halfFriendlyH - 100.f))); + auto* friendly = State::GameState::getInstance().addEntity(std::make_unique("Friendly" + std::to_string(stage) + "_S" + std::to_string(i + 1), mFriendlyTex, tS)); + if (friendly) { + if (auto* collider = friendly->getComponent()) { + collider->setScale(0.75f); + } + } } } @@ -155,6 +217,34 @@ namespace Game::AGame { 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(name, mTrashTex, t)); + + // If requested, mark the spawned trash as sea-only + if (seaOnly) { + auto* tr = GameManager::getEntityByName(name); + if (tr) tr->setSeaOnly(true); + } + + GameManager::setSharedData("trashActiveCount", GameManager::getSharedData("trashActiveCount") + 1); + } + void Background::update(float deltaTime) { (void)deltaTime; @@ -162,6 +252,52 @@ namespace Game::AGame { const int trashCount = GameManager::getSharedData("trashActiveCount"); const int stage = GameManager::getSharedData("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("friendlyActiveCount"); + if (activeFriendlies < mMaxAutoFriendlies) { + // Compute chance = deltaTime / avgInterval + const float chance = deltaTime / std::max(0.0001f, mFriendlySpawnAvgInterval); + const int thresh = static_cast(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(seaProb * 100.f); + + Object::Transform tS; + tS.rotation = 0.f; + tS.scaleX = 6.f; + tS.scaleY = 6.f; + 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(Utils::getUtils().rirng32(static_cast(viewLeft + halfFriendlyW + 25.f), static_cast(mLandBoundaryX - halfFriendlyW - 25.f))); + } else { + tS.x = static_cast(Utils::getUtils().rirng32(static_cast(mLandBoundaryX + halfFriendlyW + 25.f), static_cast(viewRight - halfFriendlyW - 25.f))); + } + tS.y = static_cast(Utils::getUtils().rirng32(static_cast(viewTop + halfFriendlyH + 100.f), static_cast(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(name, mFriendlyTex, tS)); + if (friendly) { + if (auto* collider = friendly->getComponent()) { + collider->setScale(0.75f); + } + GameManager::setSharedData("friendlyActiveCount", GameManager::getSharedData("friendlyActiveCount") + 1); + } + } + } + } + if (mPendingLevelSpawn) { if (enemyCount <= 0 && trashCount <= 0) { GameManager::processPendingEntityRemovals(); @@ -176,6 +312,7 @@ namespace Game::AGame { mPendingLevelSpawn = true; mPendingLevelStage = stage + 1; } else if (!GameManager::getSharedData("gameWon")) { + writeFinalScoreFile(GameManager::getSharedData("gameScore")); GameManager::setSharedData("gameWon", true); LOG("All levels cleared"); } diff --git a/src/game/agame/enemy.cpp b/src/game/agame/enemy.cpp index e1e2e88..446468d 100644 --- a/src/game/agame/enemy.cpp +++ b/src/game/agame/enemy.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -31,8 +32,17 @@ namespace Game::AGame { return; } - // Enemies are always visible - mIsVisible = true; + // Enemies are visible only within a reveal radius around the player + const float revealRadius = GameManager::getSharedData("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); // Semi-random movement with periodic direction changes mDirectionChangeTimer += deltaTime; @@ -47,6 +57,9 @@ namespace Game::AGame { // 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("terrainLandBoundaryX"); @@ -67,6 +80,22 @@ namespace Game::AGame { 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("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; diff --git a/src/game/agame/hudtext.cpp b/src/game/agame/hudtext.cpp index 51321e2..62dd0e8 100644 --- a/src/game/agame/hudtext.cpp +++ b/src/game/agame/hudtext.cpp @@ -33,13 +33,13 @@ namespace Game::AGame { }; if (GameManager::getSharedData("gameLost")) { - setText("You Died!"); + setText("Umrl si!"); anchorTopRight(); return; } if (GameManager::getSharedData("gameWon")) { - setText("You Won!"); + setText("Zmagal si!"); anchorTopRight(); return; } diff --git a/src/game/agame/player.cpp b/src/game/agame/player.cpp index 4a7e14c..a42630c 100644 --- a/src/game/agame/player.cpp +++ b/src/game/agame/player.cpp @@ -126,6 +126,21 @@ namespace Game::AGame { if (Input::isKeyPressed(SDL_SCANCODE_D)) { mTransform.x += mSpeed * deltaTime; mIsFlipped = true; } mSpeed = Input::isKeyPressed(SDL_SCANCODE_LSHIFT) ? 400.f : 200.f; + int w = 0; + int h = 0; + SDL_GetWindowSizeInPixels(Window::Window::getSDLWindowBackend(), &w, &h); + 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; } diff --git a/src/game/agame/trash.cpp b/src/game/agame/trash.cpp index 49e0850..df23123 100644 --- a/src/game/agame/trash.cpp +++ b/src/game/agame/trash.cpp @@ -11,6 +11,14 @@ namespace Game::AGame { void Trash::update(float deltaTime) { (void)deltaTime; + if (mSeaOnly) { + const float landBoundaryX = GameManager::getSharedData("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; + } + } return; } diff --git a/src/main.cpp b/src/main.cpp index f6e6374..acd43bf 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +18,12 @@ using namespace Game; int main() { PLNIMP("Letnik3Zadnja - Licenca: LGPLv2.1-only, CC BY-SA 4.0"); + // Prompt for player name before initializing the window/engine + std::string playerName; + std::cout << "Vnesi ime igralca (pritisni Enter za 'Igralec'): "; + std::getline(std::cin, playerName); + if (playerName.empty()) playerName = "Igralec"; + Game::GameManager::setSharedData("playerName", playerName); Window::Window window = Window::Window(); window.init(1280, 720, "Game Window"); diff --git a/src/object/components/boxcollider.cpp b/src/object/components/boxcollider.cpp index f5bdce6..2bc9695 100644 --- a/src/object/components/boxcollider.cpp +++ b/src/object/components/boxcollider.cpp @@ -50,11 +50,11 @@ 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); diff --git a/src/object/ui/uibutton.cpp b/src/object/ui/uibutton.cpp index 98030b9..b5d4881 100644 --- a/src/object/ui/uibutton.cpp +++ b/src/object/ui/uibutton.cpp @@ -5,9 +5,9 @@ namespace Game::Object { : 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) { diff --git a/src/object/ui/uitext.cpp b/src/object/ui/uitext.cpp index ee76224..6772cba 100644 --- a/src/object/ui/uitext.cpp +++ b/src/object/ui/uitext.cpp @@ -5,9 +5,9 @@ namespace Game::Object { : 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) {} /* { diff --git a/src/object/ui/uitextbox.cpp b/src/object/ui/uitextbox.cpp index 4a1198b..d8085eb 100644 --- a/src/object/ui/uitextbox.cpp +++ b/src/object/ui/uitextbox.cpp @@ -8,8 +8,8 @@ namespace Game::Object { : Entity(name, font, transform), mX(x), mY(y), mConfig(config) { } void UITextBox::start() { - mTransform.x -= mTex->getWidth() * mTransform.adjustedScaleX() / 2.f; - mTransform.y -= mTex->getHeight() * mTransform.adjustedScaleY() / 2.f; + mTransform.x = mX - mTex->getWidth() * mTransform.adjustedScaleX() / 2.f; + mTransform.y = mY - mTex->getHeight() * mTransform.adjustedScaleY() / 2.f; mBoxWidth = static_cast(mTex->getWidth()) * mTransform.adjustedScaleX(); mBoxHeight = static_cast(mTex->getHeight()) * mTransform.adjustedScaleY(); diff --git a/src/renderer/font.cpp b/src/renderer/font.cpp index e11ae11..0f64549 100644 --- a/src/renderer/font.cpp +++ b/src/renderer/font.cpp @@ -49,6 +49,11 @@ namespace Game::Renderer { ERROR("SDL_CreateTextureFromSurface Error: " << SDL_GetError() << " (This object may be unusuable)"); return; } + + // 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()); + } } diff --git a/src/window/window.cpp b/src/window/window.cpp index fbd17f4..29c79d8 100644 --- a/src/window/window.cpp +++ b/src/window/window.cpp @@ -40,7 +40,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, 0); if (!mWindow) { ERROR("Failed to create window: " << SDL_GetError()); SDL_Quit(); @@ -94,36 +94,24 @@ namespace Game::Window { mRunning = false; } + if (event.type == SDL_EVENT_WINDOW_ENTER_FULLSCREEN) { + SDL_SetWindowFullscreen(mWindow, false); + } + // 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(newWidth) / static_cast(oldWidth) : 1.f; - const float scaleY = canScale ? static_cast(newHeight) / static_cast(oldHeight) : 1.f; - State::GameState::getInstance().withEntitiesLocked([&](auto& entities) { for (auto& [name, entity] : entities) { (void)name; if (entity) { - if (canScale) { - Object::Transform* transform = entity->getTransform(); - transform->x *= scaleX; - transform->y *= scaleY; - } - entity->onWindowResized(newWidth, newHeight); + entity->onWindowResized(mLastWindowWidth, mLastWindowHeight); } } }); - - mLastWindowWidth = newWidth; - mLastWindowHeight = newHeight; } }