diff --git a/include/game/agame/background.hpp b/include/game/agame/background.hpp index 8b809a6..3ac6a94 100644 --- a/include/game/agame/background.hpp +++ b/include/game/agame/background.hpp @@ -7,16 +7,24 @@ #include #include #include +#include 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; 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; + std::shared_ptr mSeaTex; std::shared_ptr mEnemyTex; std::shared_ptr mTrashTex; + std::shared_ptr mFriendlyTex; END_GAME_ENTITY() } \ No newline at end of file diff --git a/include/game/agame/enemy.hpp b/include/game/agame/enemy.hpp index 9c43da6..146b226 100644 --- a/include/game/agame/enemy.hpp +++ b/include/game/agame/enemy.hpp @@ -10,5 +10,10 @@ 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; END_GAME_ENTITY() } \ No newline at end of file diff --git a/include/game/agame/friendly.hpp b/include/game/agame/friendly.hpp new file mode 100644 index 0000000..fb82fbc --- /dev/null +++ b/include/game/agame/friendly.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include +#include +#include + +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; + END_GAME_ENTITY() +} diff --git a/include/game/agame/hudtext.hpp b/include/game/agame/hudtext.hpp new file mode 100644 index 0000000..c5ac593 --- /dev/null +++ b/include/game/agame/hudtext.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace Game::AGame { + class HUDText : public Object::UIText { + using Object::UIText::UIText; + + public: + ~HUDText() override = default; + void start() override; + void update(float deltaTime) override; + }; +} diff --git a/include/game/agame/player.hpp b/include/game/agame/player.hpp index 524b0ea..eee6760 100644 --- a/include/game/agame/player.hpp +++ b/include/game/agame/player.hpp @@ -7,9 +7,19 @@ 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 tex); + void setGroundTexture(std::shared_ptr tex); + void respawnRandomSea(float landBoundaryX); + bool isShipMode() const { return mIsShipMode; } + 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 mShipTex; + std::shared_ptr mGroundTex; + bool mIsShipMode = true; + float mShoreMargin = 40.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 7c143c7..7445dca 100644 --- a/include/game/agame/trash.hpp +++ b/include/game/agame/trash.hpp @@ -6,7 +6,13 @@ #include #include +namespace Game::AGame { + class Player; +} + namespace Game::AGame { GAME_ENTITY(Trash) + public: + void onCollisionEnter(Object::Entity* other) override; END_GAME_ENTITY() } \ No newline at end of file diff --git a/resources/l3friendly.png b/resources/l3friendly.png new file mode 100644 index 0000000..d09b906 Binary files /dev/null and b/resources/l3friendly.png differ diff --git a/resources/l3player.png b/resources/l3player.png new file mode 100644 index 0000000..5e40f94 Binary files /dev/null and b/resources/l3player.png differ diff --git a/resources/l3sea.png b/resources/l3sea.png new file mode 100644 index 0000000..619ad28 Binary files /dev/null and b/resources/l3sea.png differ diff --git a/src/game/agame/background.cpp b/src/game/agame/background.cpp index a83ae7c..01011a7 100644 --- a/src/game/agame/background.cpp +++ b/src/game/agame/background.cpp @@ -1,19 +1,37 @@ #include +#include #include #include #include +#include namespace Game::AGame { void Background::start() { + mSeaTex = std::make_shared("../resources/l3sea.png", SDL_GetRenderer(Window::Window::getSDLWindowBackend()), "seaTex"); mEnemyTex = std::make_shared("../resources/l3enemy.png", SDL_GetRenderer(Window::Window::getSDLWindowBackend()), "enemyTex"); mTrashTex = std::make_shared("../resources/l3trash.png", SDL_GetRenderer(Window::Window::getSDLWindowBackend()), "trashTex"); + mFriendlyTex = std::make_shared("../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); 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); + // Land boundary: left 1/3 of map in centered coordinates + // For 1280px window: -640 (left) + 426.67 (1/3) = -213.33 + mLandBoundaryX = -static_cast(mW) / 2.f + static_cast(mW) / 3.f; + GameManager::setSharedData("terrainLandBoundaryX", mLandBoundaryX); + GameManager::setSharedData("enemyRevealRadius", 260.f); + mTransform.scaleX *= 10.f; mTransform.scaleY *= 10.f; @@ -21,72 +39,147 @@ namespace Game::AGame { mTransform.y -= mTex->getHeight() * mTransform.adjustedScaleY() / 2.f; LOG("W: " << mW << " H: " << mH); + 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(mW) / 2.f; + const float worldRight = static_cast(mW) / 2.f; + const float worldTop = -static_cast(mH) / 2.f; + const float worldBottom = static_cast(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& 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 viewTop = -mH / 2.f; + const float viewBottom = mH / 2.f; + + Object::Transform tS; + tS.rotation = 0.f; + tS.scaleX = 6.f; + tS.scaleY = 6.f; + + 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) { + 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)); + } + } + + 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"); + if (player) { + player->respawnRandomSea(mLandBoundaryX); + } + } + + Object::Transform tS; + tS.rotation = 0.f; + + tS.scaleX = 7.f; + tS.scaleY = 7.f; + 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(Utils::getUtils().rirng32(static_cast(viewLeft + halfEnemyW + 25.f), static_cast(mLandBoundaryX - halfEnemyW - 25.f))); + tS.y = static_cast(Utils::getUtils().rirng32(static_cast(viewTop + halfEnemyH + 100.f), static_cast(viewBottom - halfEnemyH - 100.f))); + GameManager::instantiateEntity(std::make_unique("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(Utils::getUtils().rirng32(static_cast(mLandBoundaryX + halfTrashW + 25.f), static_cast(viewRight - halfTrashW - 25.f))); + tS.y = static_cast(Utils::getUtils().rirng32(static_cast(viewTop + halfTrashH + 100.f), static_cast(viewBottom - halfTrashH - 100.f))); + GameManager::instantiateEntity(std::make_unique("Trash" + std::to_string(stage) + "_" + std::to_string(i + 1), mTrashTex, tS)); + } + + spawnFriendly(stage, friendlyCount); } void Background::update(float deltaTime) { - mEnemySpawnTimer += deltaTime; + (void)deltaTime; - int cnt = GameManager::getSharedData("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; + const int enemyCount = GameManager::getSharedData("enemyActiveCount"); + const int trashCount = GameManager::getSharedData("trashActiveCount"); + const int stage = GameManager::getSharedData("gameStage"); - 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(viewLeft + (2.f * mW / 3.f) + halfEnemyW); - int spawnMaxX = static_cast(viewRight - halfEnemyW - 25.f); - int spawnMinY = static_cast(viewTop + halfEnemyH + 100.f); - int spawnMaxY = static_cast(viewBottom - halfEnemyH - 100.f); - - // Safety for tiny windows / huge sprites. - if (spawnMinX > spawnMaxX) spawnMinX = spawnMaxX = static_cast(camX); - if (spawnMinY > spawnMaxY) spawnMinY = spawnMaxY = static_cast(camY); - - tS.x = static_cast(Utils::getUtils().rirng32(spawnMinX, spawnMaxX)); - tS.y = static_cast(Utils::getUtils().rirng32(spawnMinY, spawnMaxY)); - GameManager::instantiateEntity(std::make_unique("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(Utils::getUtils().rirng32(spawnMinY, spawnMaxY)); - GameManager::instantiateEntity(std::make_unique("Trash" + std::to_string(cnt + 1), mTrashTex, tS)); + if (mPendingLevelSpawn) { + if (enemyCount <= 0 && trashCount <= 0) { + GameManager::processPendingEntityRemovals(); + mPendingLevelSpawn = false; + spawnLevel(mPendingLevelStage); + } + return; } - /*const bool* state = SDL_GetKeyboardState(nullptr); - if (state[SDL_SCANCODE_P]) { - mTransform.scaleX *= 2.f; - mTransform.scaleY *= 2.f; + if (enemyCount <= 0 && trashCount <= 0) { + if (stage < mMaxLevels) { + mPendingLevelSpawn = true; + mPendingLevelStage = stage + 1; + } else if (!GameManager::getSharedData("gameWon")) { + GameManager::setSharedData("gameWon", true); + LOG("All levels cleared"); + } } - 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); } void Background::onWindowResized(int newWidth, int newHeight) { diff --git a/src/game/agame/enemy.cpp b/src/game/agame/enemy.cpp index e6eb593..e1e2e88 100644 --- a/src/game/agame/enemy.cpp +++ b/src/game/agame/enemy.cpp @@ -1,22 +1,129 @@ #include #include +#include +#include +#include +#include +#include +#include +#include namespace Game::AGame { void Enemy::start() { mZIndex = 20; addComponent(); LOG("Enemy started: " << getName()); + + // Initialize random movement + const float angle = static_cast(Utils::getUtils().rirng32(0, 360)) * 3.14159f / 180.f; + const float speed = 20.f + static_cast(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"); + if (!player) { + mIsVisible = false; + return; + } + + // Enemies are always visible + mIsVisible = true; + + // Semi-random movement with periodic direction changes + mDirectionChangeTimer += deltaTime; + if (mDirectionChangeTimer > 2.0f) { + const float angle = static_cast(Utils::getUtils().rirng32(0, 360)) * 3.14159f / 180.f; + const float speed = 20.f + static_cast(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; + + // Clamp to land section + const float landBoundaryX = GameManager::getSharedData("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; + + // Get window dimensions for boundary calculations + int w = 0, h = 0; + SDL_GetWindowSizeInPixels(Window::Window::getSDLWindowBackend(), &w, &h); + 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); + } + 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("Dummy"); + if (!entities) { + auto snapshot = State::GameState::getInstance().getEntitiesSnapshot(); + for (auto* other : snapshot) { + if (!other || other == this || !dynamic_cast(other)) continue; + auto* otherEnemy = dynamic_cast(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("enemyActiveCount") - 1); - - // Find in state + auto* player = dynamic_cast(other); + if (!player || !mIsVisible) { + return; + } + + if (hasAdjacentEnemy()) { + LOG("Player collided with a strong group of polluters; game over!"); + GameManager::setSharedData("gameLost", true); + GameManager::destroyEntity(player); + return; + } + + LOG("Enemy '" << getName() << "' collided with player; removing polluter and awarding points"); + GameManager::setSharedData("enemyActiveCount", std::max(0, GameManager::getSharedData("enemyActiveCount") - 1)); + GameManager::setSharedData("gameScore", GameManager::getSharedData("gameScore") + 100); GameManager::destroyEntity(this); } } \ No newline at end of file diff --git a/src/game/agame/friendly.cpp b/src/game/agame/friendly.cpp new file mode 100644 index 0000000..459e211 --- /dev/null +++ b/src/game/agame/friendly.cpp @@ -0,0 +1,81 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Game::AGame { + void Friendly::start() { + mZIndex = 20; + addComponent(); + LOG("Friendly started: " << getName()); + + // Initialize random movement + const float angle = static_cast(Utils::getUtils().rirng32(0, 360)) * 3.14159f / 180.f; + const float speed = 20.f + static_cast(Utils::getUtils().rirng32(0, 30)); + mMoveSpeedX = std::cos(angle) * speed; + mMoveSpeedY = std::sin(angle) * speed; + mDirectionChangeTimer = 0.f; + } + + void Friendly::update(float deltaTime) { + (void)deltaTime; + + // Semi-random movement with periodic direction changes + mDirectionChangeTimer += deltaTime; + if (mDirectionChangeTimer > 2.0f) { + const float angle = static_cast(Utils::getUtils().rirng32(0, 360)) * 3.14159f / 180.f; + const float speed = 20.f + static_cast(Utils::getUtils().rirng32(0, 30)); + mMoveSpeedX = std::cos(angle) * speed; + mMoveSpeedY = std::sin(angle) * speed; + mDirectionChangeTimer = 0.f; + } + + // Move friendly + mTransform.x += mMoveSpeedX * deltaTime; + mTransform.y += mMoveSpeedY * deltaTime; + + // Clamp to land section + const float landBoundaryX = GameManager::getSharedData("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; + + // Get window dimensions for boundary calculations + int w = 0, h = 0; + SDL_GetWindowSizeInPixels(Window::Window::getSDLWindowBackend(), &w, &h); + 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); + } + 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(other); + if (!player) { + return; + } + + GameManager::setSharedData("friendlyActiveCount", std::max(0, GameManager::getSharedData("friendlyActiveCount") - 1)); + GameManager::setSharedData("gameScore", GameManager::getSharedData("gameScore") - 50); + GameManager::destroyEntity(this); + } +} diff --git a/src/game/agame/hudtext.cpp b/src/game/agame/hudtext.cpp new file mode 100644 index 0000000..51321e2 --- /dev/null +++ b/src/game/agame/hudtext.cpp @@ -0,0 +1,56 @@ +#include +#include +#include +#include + +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; + + int windowW = 0; + int windowH = 0; + SDL_GetWindowSizeInPixels(Window::Window::getSDLWindowBackend(), &windowW, &windowH); + 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("gameLost")) { + setText("You Died!"); + anchorTopRight(); + return; + } + + if (GameManager::getSharedData("gameWon")) { + setText("You Won!"); + anchorTopRight(); + return; + } + + std::stringstream stream; + stream << "Level " << GameManager::getSharedData("gameStage") + << " | Točke " << GameManager::getSharedData("gameScore") + << " | Smeti " << GameManager::getSharedData("trashActiveCount") + << " | Sovražniki " << GameManager::getSharedData("enemyActiveCount"); + + setText(stream.str()); + anchorTopRight(); + } +} diff --git a/src/game/agame/player.cpp b/src/game/agame/player.cpp index d26bcad..4a7e14c 100644 --- a/src/game/agame/player.cpp +++ b/src/game/agame/player.cpp @@ -3,8 +3,60 @@ #include #include #include +#include namespace Game::AGame { + void Player::setShipTexture(std::shared_ptr tex) { + mShipTex = std::move(tex); + if (mIsShipMode && mShipTex) { + setTexture(mShipTex); + } + } + + void Player::setGroundTexture(std::shared_ptr tex) { + mGroundTex = std::move(tex); + if (!mIsShipMode && mGroundTex) { + setTexture(mGroundTex); + } + } + + void Player::respawnRandomSea(float landBoundaryX) { + auto spawnTex = mShipTex ? mShipTex : mTex; + if (!spawnTex) { + return; + } + + int w = 0; + int h = 0; + SDL_GetWindowSizeInPixels(Window::Window::getSDLWindowBackend(), &w, &h); + + 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(Utils::getUtils().rirng32(static_cast(minCenterX), static_cast(maxCenterX))); + } + + float centerY = 0.f; + if (maxCenterY > minCenterY) { + centerY = static_cast(Utils::getUtils().rirng32(static_cast(minCenterY), static_cast(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 = 8.f; mTransform.scaleY = 8.f; + if (!mShipTex) { + mShipTex = mTex; + } + if (!mGroundTex) { + mGroundTex = mTex; + } + + int w = 0; + int h = 0; + SDL_GetWindowSizeInPixels(Window::Window::getSDLWindowBackend(), &w, &h); + + 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("terrainLandBoundaryX"); + mTransform.x = static_cast(Utils::getUtils().rirng32(static_cast(minX), static_cast(maxX))); + mTransform.y = static_cast(Utils::getUtils().rirng32(static_cast(minY), static_cast(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,41 @@ 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); + const float landBoundaryX = Game::GameManager::getSharedData("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) { + mIsShipMode = !mIsShipMode; + } + } + // 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; + + 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); + } + } + + void Player::onCollisionEnter(Object::Entity* other) { + (void)other; + if (GameManager::getSharedData("gameLost")) { + GameManager::destroyEntity(this); + } } } \ No newline at end of file diff --git a/src/game/agame/trash.cpp b/src/game/agame/trash.cpp index 1ce1273..49e0850 100644 --- a/src/game/agame/trash.cpp +++ b/src/game/agame/trash.cpp @@ -1,11 +1,27 @@ #include +#include +#include +#include namespace Game::AGame { void Trash::start() { mZIndex = 20; + addComponent(); } void Trash::update(float deltaTime) { + (void)deltaTime; return; } + + void Trash::onCollisionEnter(Object::Entity* other) { + auto* player = dynamic_cast(other); + if (!player || !player->isShipMode()) { + return; + } + + GameManager::setSharedData("trashActiveCount", std::max(0, GameManager::getSharedData("trashActiveCount") - 1)); + GameManager::setSharedData("gameScore", GameManager::getSharedData("gameScore") + 25); + GameManager::destroyEntity(this); + } } \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 22f4773..f6e6374 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -20,16 +21,16 @@ int main() { Window::Window window = Window::Window(); window.init(1280, 720, "Game Window"); - State::GameState::getInstance().addEntity(std::make_unique("Sample Text Box", std::make_shared("../resources/roboto.ttf", window.getRenderer()->getSDLRenderer(), 48, "Roboto"), Object::DEFAULT_TRANSFORM, 640.f, 360.f)); - - //Object::Transform t1{100.f, 100.f, 0.f, 1.f, 1.f}; State::GameState::getInstance().addEntity(std::make_unique("BG", std::make_shared("../resources/bgtest.png", window.getRenderer()->getSDLRenderer()), Object::DEFAULT_TRANSFORM)); - Object::Entity* player = State::GameState::getInstance().addEntity(std::make_unique("Player", std::make_shared("../resources/l3ladja.png", window.getRenderer()->getSDLRenderer()), Object::DEFAULT_TRANSFORM)); - player->addComponent(); - //State::GameState::getInstance().addEntity(std::make_unique("Player2", std::make_shared("../resources/roboto.ttf", window.getRenderer()->getSDLRenderer(), 128, "Roboto"), t1)); - // Sample textbox - State::GameState::getInstance().addEntity(std::make_unique("Sample Text Box", std::make_shared("../resources/roboto.ttf", window.getRenderer()->getSDLRenderer(), 48, "Roboto"), Object::DEFAULT_TRANSFORM, 640.f, 360.f)); + auto* player = dynamic_cast(State::GameState::getInstance().addEntity(std::make_unique("Player", std::make_shared("../resources/l3ladja.png", window.getRenderer()->getSDLRenderer()), Object::DEFAULT_TRANSFORM))); + if (player) { + player->addComponent(); + player->setShipTexture(std::make_shared("../resources/l3ladja.png", window.getRenderer()->getSDLRenderer())); + player->setGroundTexture(std::make_shared("../resources/l3player.png", window.getRenderer()->getSDLRenderer())); + } + + State::GameState::getInstance().addEntity(std::make_unique("HUD", std::make_shared("../resources/roboto.ttf", window.getRenderer()->getSDLRenderer(), 72, "HUDFont"), Object::Transform{0.f, 0.f, 0.f, 1.f, 1.f}, 320.f, 40.f)); window.run(); diff --git a/src/object/components/boxcollider.cpp b/src/object/components/boxcollider.cpp index c060a92..f5bdce6 100644 --- a/src/object/components/boxcollider.cpp +++ b/src/object/components/boxcollider.cpp @@ -59,7 +59,7 @@ namespace Game::Object::Components { 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; diff --git a/src/renderer/renderer.cpp b/src/renderer/renderer.cpp index 7004921..5f00007 100644 --- a/src/renderer/renderer.cpp +++ b/src/renderer/renderer.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -57,12 +58,26 @@ namespace Game::Renderer { RendererConfig config{ camX, camY, screenW, screenH }; 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 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()); } diff --git a/src/window/window.cpp b/src/window/window.cpp index 713a89f..fbd17f4 100644 --- a/src/window/window.cpp +++ b/src/window/window.cpp @@ -108,17 +108,19 @@ namespace Game::Window { const float scaleX = canScale ? static_cast(newWidth) / static_cast(oldWidth) : 1.f; const float scaleY = canScale ? static_cast(newHeight) / static_cast(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) { + if (canScale) { + Object::Transform* transform = entity->getTransform(); + transform->x *= scaleX; + transform->y *= scaleY; + } + entity->onWindowResized(newWidth, newHeight); } - entity->onWindowResized(newWidth, newHeight); } - } + }); mLastWindowWidth = newWidth; mLastWindowHeight = newHeight;