From 241d8119ce23f2a97374fabf9897bbf8e8417265 Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Sun, 9 Jul 2023 21:24:50 +0200 Subject: [PATCH] Added consumption and countdown --- CMakeLists.txt | 9 ++- assets/fonts/pixel.ttf | Bin 0 -> 97456 bytes src/collectables.hpp | 7 +- src/config.h | 11 +++ src/game/camera/tracking_view.cpp | 10 ++- src/game/camera/tracking_view_options.hpp | 8 +- src/game/collectables/collectable.cpp | 29 +++++-- src/game/collectables/collectable.hpp | 7 +- src/game/collectables/collectable_config.hpp | 5 +- src/game/collectables/collectable_factory.cpp | 2 +- .../collection/collectables_collection.cpp | 37 ++++++++- .../collection/collectables_collection.hpp | 2 +- src/game/game.cpp | 7 ++ src/game/game.h | 10 ++- src/game/layer/global_layer.cpp | 39 +++++++++ src/game/layer/global_layer.hpp | 27 +++++++ src/game/level/level_config.hpp | 3 + src/game/level/level_loader.cpp | 6 ++ src/game/physics/holes/holes_simulation.cpp | 24 +++++- src/game/physics/holes/holes_simulation.hpp | 2 + src/game/player/player.cpp | 27 +++---- src/game/player/player.hpp | 4 +- src/game/player/player_collection.cpp | 19 +++++ src/game/player/player_collection.hpp | 2 + src/game/time/countdown.cpp | 70 +++++++++++++++++ src/game/time/countdown.hpp | 31 ++++++++ src/levels.hpp | 74 ++++++++++-------- src/main.cpp | 14 ++++ src/sprites/animated_sprite.cpp | 5 ++ src/sprites/animated_sprite.hpp | 2 + src/sprites/masked_sprite.cpp | 37 +++++++-- src/sprites/masked_sprite.hpp | 6 ++ src/sprites/single_sprite.cpp | 5 ++ src/sprites/single_sprite.hpp | 2 + src/sprites/sprite.hpp | 5 ++ src/sprites/versatile_sprite.cpp | 5 ++ src/sprites/versatile_sprite.hpp | 2 + src/typography/font_manager.cpp | 37 +++++++++ src/typography/font_manager.hpp | 27 +++++++ 39 files changed, 538 insertions(+), 81 deletions(-) create mode 100644 assets/fonts/pixel.ttf create mode 100644 src/game/layer/global_layer.cpp create mode 100644 src/game/layer/global_layer.hpp create mode 100644 src/game/time/countdown.cpp create mode 100644 src/game/time/countdown.hpp create mode 100644 src/typography/font_manager.cpp create mode 100644 src/typography/font_manager.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index eaae4c9..e2bb869 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -129,7 +129,14 @@ set(SOURCES src/game/physics/body_adapter.hpp src/sprites/masked_sprite.cpp src/sprites/masked_sprite.hpp - src/sprites/configs/masked_sprite_config.hpp src/sprites/masked_sprite_hole.hpp) + src/sprites/configs/masked_sprite_config.hpp + src/sprites/masked_sprite_hole.hpp + src/game/time/countdown.cpp + src/game/time/countdown.hpp + src/game/layer/global_layer.cpp + src/game/layer/global_layer.hpp + src/typography/font_manager.cpp + src/typography/font_manager.hpp) set(PHYSICS_00_SOURCES src/prototypes/physics_00.cpp) diff --git a/assets/fonts/pixel.ttf b/assets/fonts/pixel.ttf new file mode 100644 index 0000000000000000000000000000000000000000..618e5bb88efc72572877cd6a99cb2cdf06741de5 GIT binary patch literal 97456 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 P*T49z0v0{r8p3}AhSeWV literal 0 HcmV?d00001 diff --git a/src/collectables.hpp b/src/collectables.hpp index 4db4939..d742d90 100644 --- a/src/collectables.hpp +++ b/src/collectables.hpp @@ -6,7 +6,12 @@ #include "game/collectables/collectable_config.hpp" std::map const all_collectables = { - {"box", CollectableConfig("rosebush")} + {"rose", CollectableConfig("rose", 1)}, + {"rosebush", CollectableConfig("rosebush", 3)}, + {"stone", CollectableConfig("stone", 5)}, + {"bike", CollectableConfig("bike", 10)}, + {"small-tree", CollectableConfig("small-tree", 20)}, + {"tram", CollectableConfig("tram", 50)} }; #endif //HOLESOME_COLLECTABLES_HPP diff --git a/src/config.h b/src/config.h index fc6abee..f717123 100644 --- a/src/config.h +++ b/src/config.h @@ -34,6 +34,7 @@ #define MASKED_HOLE_BORDER_TRANSITION_SIZE 0.2f #define MASKED_HOLE_DARKNESS_LIMIT 0.5f #define COLLECTABLE_SCALE 5.f +#define MASKED_SPRITE_LOWER_BOUND -3.f // Tracking view defaults #define DEF_TV_IS_ABSOLUTE_FREE_MOVE_THRESHOLD false @@ -70,4 +71,14 @@ #define DB_WORLD_GRID_RENDER false #define DB_TRACKING_VIEW_CENTER false +// Fonts +#define FONT_BASE_PATH "assets/fonts/" +#define DEFAULT_FONT "pixel" +#define COUNTDOWN_FONT_SIZE 50 + +// Name => Path +const std::map all_fonts = { + {"pixel", "pixel.ttf"} +}; + #endif //HOLESOME_CONFIG_H diff --git a/src/game/camera/tracking_view.cpp b/src/game/camera/tracking_view.cpp index 4b72412..e415e82 100644 --- a/src/game/camera/tracking_view.cpp +++ b/src/game/camera/tracking_view.cpp @@ -3,7 +3,8 @@ #include "../player/player_collection.hpp" TrackingView::TrackingView(TrackingViewOptions options) : options(options), - view(new sf::View()), + view(new sf::View(options.initialCenter, + options.minViewSize)), trackables({}) { marker = new CircleObject(DB_CIRCLE_RADIUS, sf::Color::Yellow); @@ -22,8 +23,8 @@ void TrackingView::lateUpdate() if (!trackables.empty()) { followTrackables(); - adjustSizeToTrackables(); } + adjustSizeToTrackables(); } void TrackingView::setSize(sf::Vector2f newSize) @@ -143,6 +144,11 @@ void TrackingView::processTrackableStates() TrackingArea TrackingView::getTrackingArea() const { + if (trackables.empty()) + { + return TrackingArea(); + } + auto initialTrackable = trackables[0]; TrackingArea area = { initialTrackable->getTrackablePosition() - initialTrackable->getTrackableSize() / 2.f, diff --git a/src/game/camera/tracking_view_options.hpp b/src/game/camera/tracking_view_options.hpp index 98d1beb..cf067be 100644 --- a/src/game/camera/tracking_view_options.hpp +++ b/src/game/camera/tracking_view_options.hpp @@ -21,18 +21,20 @@ struct TrackingViewOptions float softResizeSpeed = DEF_TV_SOFT_RESIZE_SPEED; /** - * If setWorld to 0, view will not be limited. + * If set to 0, view will not be limited. */ sf::Vector2f minViewSize = DEF_TV_MIN_VIEW_SIZE; /** - * If setWorld to 0, view will not be limited. + * If set to 0, view will not be limited. */ sf::Vector2f maxViewSize = DEF_TV_MAX_VIEW_SIZE; + sf::Vector2f initialCenter = {0, 0}; + /** * Will be added to tracked area size twice, as padding for each side. - * If isAbsoluteViewSizePadding is setWorld to true, padding will be added to view size, is multiplied with tracking size and added. + * If isAbsoluteViewSizePadding is set to true, padding will be added to view size, is multiplied with tracking size and added. */ sf::Vector2f viewSizePadding = DEF_TV_VIEW_SIZE_PADDING; bool isAbsoluteViewSizePadding = DEF_TV_IS_ABSOLUTE_VIEW_SIZE_PADDING; diff --git a/src/game/collectables/collectable.cpp b/src/game/collectables/collectable.cpp index 6152fd6..ad3e51f 100644 --- a/src/game/collectables/collectable.cpp +++ b/src/game/collectables/collectable.cpp @@ -2,8 +2,12 @@ #include "../../sprites/versatile_sprite.hpp" #include "../../config.h" #include "../input/input_mapper.h" +#include "../physics/holes/holes_simulation.hpp" +#include "collection/collectables_collection.hpp" +#include "../player/player_collection.hpp" -Collectable::Collectable() +Collectable::Collectable(int points) + : points(points) { collectableId = collectableCount; collectableCount++; @@ -24,10 +28,12 @@ float Collectable::getDepth() const void Collectable::setSprite(const std::string &spriteName) { // Create versatile sprite - auto sprite = std::make_shared(spriteName); - size = sprite->getSize() * COLLECTABLE_SCALE; - sprite->setSize(size); - addChildScreenOffset(sprite, IsometricCoordinates(-size / 2.f)); + auto spriteObject = std::make_shared(spriteName); + size = spriteObject->getSize() * COLLECTABLE_SCALE; + spriteObject->setSize(size); + addChildScreenOffset(spriteObject, IsometricCoordinates(-size / 2.f)); + + sprite = spriteObject; // Set half size offset of coordinates coordinates->move(IsometricCoordinates(0, -size.x / 2.f, 0)); @@ -37,3 +43,16 @@ sf::Vector2f Collectable::getSize() const { return size / WORLD_TO_ISO_SCALE; } + +void Collectable::preRenderUpdate() +{ + if (!sprite->isVisible()) + { + auto closestPlayer = PlayerCollection::getInstance()->getClosestPlayer(*coordinates); + closestPlayer->consume(points); + setActive(false); + return; + } + + GameObject::preRenderUpdate(); +} diff --git a/src/game/collectables/collectable.hpp b/src/game/collectables/collectable.hpp index 70251d2..7215948 100644 --- a/src/game/collectables/collectable.hpp +++ b/src/game/collectables/collectable.hpp @@ -4,11 +4,12 @@ #include "../game_object.h" #include "../player/player.hpp" +#include "../../sprites/masked_sprite.hpp" class Collectable : public GameObject { public: - Collectable(); + Collectable(int points); void setSprite(const std::string &spriteName); @@ -23,11 +24,15 @@ public: return collectableId; } + void preRenderUpdate() override; + private: int collectableId = 0; static inline int collectableCount = 0; + std::shared_ptr sprite; sf::Vector2f size; + int points = 0; std::shared_ptr consumedBy = nullptr; }; diff --git a/src/game/collectables/collectable_config.hpp b/src/game/collectables/collectable_config.hpp index 34fcbae..ffb9717 100644 --- a/src/game/collectables/collectable_config.hpp +++ b/src/game/collectables/collectable_config.hpp @@ -6,9 +6,10 @@ struct CollectableConfig { std::string spriteName; + int points = 0; - explicit CollectableConfig(std::string spriteName) - : spriteName(std::move(spriteName)) + explicit CollectableConfig(std::string spriteName, int points) + : spriteName(std::move(spriteName)), points(points) {} CollectableConfig() = default; diff --git a/src/game/collectables/collectable_factory.cpp b/src/game/collectables/collectable_factory.cpp index 8fcca64..d46d1fd 100644 --- a/src/game/collectables/collectable_factory.cpp +++ b/src/game/collectables/collectable_factory.cpp @@ -3,7 +3,7 @@ std::shared_ptr CollectableFactory::createFromInLevelConfig(const CollectableInLevel &config) { auto collectableConfig = config.collectableConfig; - auto collectable = std::make_shared(); + auto collectable = std::make_shared(config.collectableConfig.points); collectable->coordinates->setGrid(config.position); collectable->setSprite(collectableConfig.spriteName); diff --git a/src/game/collectables/collection/collectables_collection.cpp b/src/game/collectables/collection/collectables_collection.cpp index fd52503..267e105 100644 --- a/src/game/collectables/collection/collectables_collection.cpp +++ b/src/game/collectables/collection/collectables_collection.cpp @@ -4,6 +4,7 @@ #include "collectables_depth_collection.hpp" #include "../../../logging/easylogging++.h" #include "../../../config.h" +#include "../../physics/holes/holes_simulation.hpp" std::shared_ptr CollectablesCollection::getInstance() { @@ -28,8 +29,26 @@ void CollectablesCollection::createEmpty(int maxDepth) } } -void CollectablesCollection::remove(const std::shared_ptr &collectable) +void CollectablesCollection::remove(int collectableId) { + HolesSimulation::getInstance()->removeCollectable(collectableId); + + std::shared_ptr collectable = nullptr; + for (auto &child: getChildren()) + { + collectable = std::dynamic_pointer_cast(child); + if (collectable->getId() == collectableId) + { + break; + } + } + + if (collectable == nullptr) + { + LOG(ERROR) << "No collectables left to remove."; + return; + } + depthCollections[collectable->getDepth()]->remove(collectable); removeChild(collectable); } @@ -44,7 +63,21 @@ void CollectablesCollection::update() { GameObject::update(); - if (!CONSIDER_COLLECTABLE_DEPTH_MOVEMENT) { + // Remove inactive collectables + auto collectables = getChildren(); + for (auto &child: collectables) + { + auto collectable = std::dynamic_pointer_cast(child); + if (collectable->getActive()) + { + continue; + } + + remove(collectable->getId()); + } + + if (!CONSIDER_COLLECTABLE_DEPTH_MOVEMENT) + { return; } diff --git a/src/game/collectables/collection/collectables_collection.hpp b/src/game/collectables/collection/collectables_collection.hpp index a01bf5b..0f64297 100644 --- a/src/game/collectables/collection/collectables_collection.hpp +++ b/src/game/collectables/collection/collectables_collection.hpp @@ -21,7 +21,7 @@ public: void draw(sf::RenderWindow *window) override; void add(const std::shared_ptr& collectable); - void remove(const std::shared_ptr& collectable); + void remove(int collectableId); private: static inline std::shared_ptr singletonInstance = nullptr; diff --git a/src/game/game.cpp b/src/game/game.cpp index e74cc52..b588bb8 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -6,6 +6,7 @@ #include "level/level_loader.hpp" #include "physics/map/map_simulation.hpp" #include "../logging/easylogging++.h" +#include "layer/global_layer.hpp" Game::Game(std::shared_ptr window) : window(std::move(window)) { @@ -145,3 +146,9 @@ bool Game::isLevelLoaded() const { return loadedLevelConfig.isValid(); } + +void Game::startCountdown(int durationInSeconds) +{ + countdown = std::make_shared(durationInSeconds); + GlobalLayer::getInstance()->add(countdown); +} diff --git a/src/game/game.h b/src/game/game.h index fbbf1c7..6cb17d6 100644 --- a/src/game/game.h +++ b/src/game/game.h @@ -9,6 +9,7 @@ #include "camera/tracking_view.h" #include "level/level_config.hpp" #include "frame_counter.hpp" +#include "time/countdown.hpp" class TrackingView; @@ -16,8 +17,10 @@ class TrackingView; class Game { public: - static std::shared_ptr constructInstance(const std::shared_ptr& window); + static std::shared_ptr constructInstance(const std::shared_ptr &window); + static std::shared_ptr getInstance(); + explicit Game(std::shared_ptr window); void run(); @@ -28,9 +31,11 @@ public: void setLevel(LevelConfig levelConfig); + void startCountdown(int durationInSeconds); + [[nodiscard]] bool isLevelLoaded() const; - void addGameObject(const std::shared_ptr& gameObject); + void addGameObject(const std::shared_ptr &gameObject); std::shared_ptr window; private: @@ -41,6 +46,7 @@ private: LevelConfig loadedLevelConfig = {}; std::shared_ptr frameCounter = std::make_shared(); + std::shared_ptr countdown = std::make_shared(); void drawFrame(); diff --git a/src/game/layer/global_layer.cpp b/src/game/layer/global_layer.cpp new file mode 100644 index 0000000..a4cf365 --- /dev/null +++ b/src/game/layer/global_layer.cpp @@ -0,0 +1,39 @@ +#include "global_layer.hpp" + +void GlobalLayer::clear() +{ + clearChildren(); + addDetachedChild(view); +} + +std::shared_ptr GlobalLayer::getInstance() +{ + if (singletonInstance == nullptr) + { + singletonInstance = std::make_shared(); + } + return singletonInstance; +} + +void GlobalLayer::draw(sf::RenderWindow *window) +{ + view->setViewForWindow(); + GameObject::draw(window); +} + +GlobalLayer::GlobalLayer() +{ + // Reference screen size of 1920x1080 + TrackingViewOptions options = { + .minViewSize = sf::Vector2f{10, 1080}, + .initialCenter = sf::Vector2f{1920, 1080} / 2.0f, + }; + + view = std::make_shared(options); + addDetachedChild(view); +} + +void GlobalLayer::add(const std::shared_ptr& gameObject) +{ + addDetachedChild(gameObject); +} diff --git a/src/game/layer/global_layer.hpp b/src/game/layer/global_layer.hpp new file mode 100644 index 0000000..22483dc --- /dev/null +++ b/src/game/layer/global_layer.hpp @@ -0,0 +1,27 @@ +#ifndef HOLESOME_GLOBAL_LAYER_HPP +#define HOLESOME_GLOBAL_LAYER_HPP + + +#include "../game_object.h" +#include "../camera/tracking_view.h" + +class GlobalLayer : public GameObject +{ +public: + GlobalLayer(); + + static std::shared_ptr getInstance(); + + void clear(); + + void draw(sf::RenderWindow *window) override; + + void add(const std::shared_ptr& gameObject); + +private: + static inline std::shared_ptr singletonInstance = nullptr; + std::shared_ptr view; +}; + + +#endif //HOLESOME_GLOBAL_LAYER_HPP diff --git a/src/game/level/level_config.hpp b/src/game/level/level_config.hpp index c31a428..0a00445 100644 --- a/src/game/level/level_config.hpp +++ b/src/game/level/level_config.hpp @@ -13,6 +13,7 @@ struct LevelConfig { std::string name; + int durationInSeconds = 0; sf::Vector2i worldMapSize = {}; TileMapConfig tileMapConfig = {}; std::vector playerSpawnPoints = {}; @@ -20,11 +21,13 @@ struct LevelConfig std::vector skyColors = {}; LevelConfig(std::string name, + int durationInSeconds, const std::vector &playerSpawnPoints, const std::vector &collectables, std::vector skyColors, const TileMapConfig &tileMapConfig) : name(std::move(name)), + durationInSeconds(durationInSeconds), playerSpawnPoints(playerSpawnPoints), tileMapConfig(tileMapConfig), skyColors(std::move(skyColors)) diff --git a/src/game/level/level_loader.cpp b/src/game/level/level_loader.cpp index 8d5e00e..e93a725 100644 --- a/src/game/level/level_loader.cpp +++ b/src/game/level/level_loader.cpp @@ -11,6 +11,7 @@ #include "../camera/multiplayer_view.hpp" #include "../physics/holes/holes_simulation.hpp" #include "../physics/holes/layouts/hole_layout.hpp" +#include "../layer/global_layer.hpp" void LevelLoader::loadLevel(const LevelConfig &levelConfig) { @@ -23,6 +24,7 @@ void LevelLoader::loadLevel(const LevelConfig &levelConfig) MapSimulation::getInstance()->resetMap(levelConfig.worldMapSize); HolesSimulation::getInstance()->clear(); PlayerCollection::getInstance()->clear(); + GlobalLayer::getInstance()->clear(); HoleLayout::getInstance()->clear(); // Add views @@ -60,6 +62,10 @@ void LevelLoader::loadLevel(const LevelConfig &levelConfig) spawnCollectable(collectableInfo); } + game->startCountdown(levelConfig.durationInSeconds); + + // Must be last + game->addGameObject(GlobalLayer::getInstance()); LOG(INFO) << "Finished loading level '" << levelConfig.name << "'."; } diff --git a/src/game/physics/holes/holes_simulation.cpp b/src/game/physics/holes/holes_simulation.cpp index b1499db..2ed92a3 100644 --- a/src/game/physics/holes/holes_simulation.cpp +++ b/src/game/physics/holes/holes_simulation.cpp @@ -2,7 +2,8 @@ std::shared_ptr HolesSimulation::getInstance() { - if (singletonInstance == nullptr) { + if (singletonInstance == nullptr) + { singletonInstance = std::make_shared(); } @@ -13,7 +14,8 @@ std::vector> HolesSimulation::getCollecta { std::vector> collectableSimulations{}; - for (auto &child : getChildren()) { + for (auto &child: getChildren()) + { auto sim = std::dynamic_pointer_cast(child); collectableSimulations.push_back(sim); } @@ -35,8 +37,10 @@ void HolesSimulation::addCollectable(const std::shared_ptr &collect void HolesSimulation::lateUpdate() { // Remove disabled collectables - for (const auto& sim : getCollectableSimulations()) { - if (sim->getCollectable()->getActive()) { + for (const auto &sim: getCollectableSimulations()) + { + if (sim->getCollectable()->getActive()) + { continue; } @@ -45,3 +49,15 @@ void HolesSimulation::lateUpdate() GameObject::lateUpdate(); } + +void HolesSimulation::removeCollectable(int collectableId) +{ + for (const auto &sim: getCollectableSimulations()) + { + if (sim->getCollectable()->getId() == collectableId) + { + removeChild(sim); + return; + } + } +} diff --git a/src/game/physics/holes/holes_simulation.hpp b/src/game/physics/holes/holes_simulation.hpp index 74552cd..c2b2cca 100644 --- a/src/game/physics/holes/holes_simulation.hpp +++ b/src/game/physics/holes/holes_simulation.hpp @@ -13,6 +13,8 @@ public: void addCollectable(const std::shared_ptr &collectable); + void removeCollectable(int collectableId); + void lateUpdate() override; void clear(); diff --git a/src/game/player/player.cpp b/src/game/player/player.cpp index c5bb9e7..d6b6b12 100644 --- a/src/game/player/player.cpp +++ b/src/game/player/player.cpp @@ -14,7 +14,7 @@ Player::Player(std::shared_ptr assignedInput, const std::string & skinSprite = std::make_shared(skinRessourceName, getIsoSize()); addChildScreenOffset(skinSprite, IsometricCoordinates(-getIsoSize() / 2.f)); - updateRadiusBasedOnLevel(); + updateRadiusBasedOnPoints(); LOG(INFO) << "Player " << playerId << " created."; } @@ -41,19 +41,6 @@ void Player::update() auto moveDelta = moveDirection * speed * FRAME_TIME.asSeconds(); coordinates->move(moveDelta); - if (input->isPerformingAction(GameAction::GROW)) - { - points = (points + 1) * (1 + 1 * FRAME_TIME.asSeconds()); - } else if (input->isPerformingAction(GameAction::SHRINK)) - { - points *= 1 - 1 * FRAME_TIME.asSeconds(); - if (points < 0) { - points = 0; - } - } - - updateRadiusBasedOnLevel(); - GameObject::update(); } @@ -101,10 +88,18 @@ long Player::getPoints() const return points; } -void Player::updateRadiusBasedOnLevel() +void Player::updateRadiusBasedOnPoints() { long points = getPoints(); - float newWorldRadius = PLAYER_MIN_RADIUS + PLAYER_RADIUS_PER_LEVEL * pow(points / 100.f, 2); + float newWorldRadius = PLAYER_MIN_RADIUS + PLAYER_RADIUS_PER_LEVEL * points / 10.f; setWorldRadius(newWorldRadius); } + +void Player::consume(int points) +{ + this->points += points; + LOG(INFO) << "Player " << playerId << " consumed " << points << " points. Total: " << this->points; + + updateRadiusBasedOnPoints(); +} diff --git a/src/game/player/player.hpp b/src/game/player/player.hpp index 90cef17..331f060 100644 --- a/src/game/player/player.hpp +++ b/src/game/player/player.hpp @@ -31,6 +31,8 @@ public: [[nodiscard]] long getPoints() const; + void consume(int points); + TranslatedCoordinates spawnPosition; private: std::shared_ptr input; @@ -44,7 +46,7 @@ private: void setWorldRadius(float newWorldRadius); - void updateRadiusBasedOnLevel(); + void updateRadiusBasedOnPoints(); }; diff --git a/src/game/player/player_collection.cpp b/src/game/player/player_collection.cpp index 00aa517..08d87e8 100644 --- a/src/game/player/player_collection.cpp +++ b/src/game/player/player_collection.cpp @@ -144,3 +144,22 @@ void PlayerCollection::updateInputIdentityAllowance() const { InputMapper::getInstance()->allowNewInputIdentities = getPlayers().size() < maxPlayerCount; } + +std::shared_ptr PlayerCollection::getClosestPlayer(const TranslatedCoordinates& point) const +{ + std::shared_ptr closestPlayer = nullptr; + float closestDistance = INFINITY; + for (auto &player: getPlayers()) + { + auto playerCenterGround = player->coordinates->world().toGroundCoordinates(); + auto pointCenterGround = point.world().toGroundCoordinates(); + // Normalize distance by player radius to get a value below 1 for something inside the player + float distance = length(playerCenterGround - pointCenterGround) / player->getWorldRadius(); + if (distance < closestDistance) + { + closestPlayer = player; + closestDistance = distance; + } + } + return closestPlayer; +} diff --git a/src/game/player/player_collection.hpp b/src/game/player/player_collection.hpp index ac3aaaf..04aed42 100644 --- a/src/game/player/player_collection.hpp +++ b/src/game/player/player_collection.hpp @@ -18,6 +18,8 @@ public: void removePlayer(const std::shared_ptr& player); + std::shared_ptr getClosestPlayer(const TranslatedCoordinates& point) const; + [[nodiscard]] std::vector> getPlayers() const; [[nodiscard]] std::shared_ptr getPlayerById(int playerId) const; diff --git a/src/game/time/countdown.cpp b/src/game/time/countdown.cpp new file mode 100644 index 0000000..ab1f818 --- /dev/null +++ b/src/game/time/countdown.cpp @@ -0,0 +1,70 @@ +#include +#include +#include "countdown.hpp" +#include "../../typography/font_manager.hpp" +#include "../../config.h" + +void Countdown::restart(int durationInSeconds) +{ + this->durationInSeconds = durationInSeconds; + timeElapsed = sf::Time::Zero; + clock.restart(); + stopped = false; +} + +Countdown::Countdown(int durationInSeconds) +{ + restart(durationInSeconds); +} + +void Countdown::stop() +{ + stopped = true; +} + +bool Countdown::isFinished() const +{ + return timeElapsed.asSeconds() >= durationInSeconds; +} + +void Countdown::update() +{ + GameObject::update(); + + if (stopped) + { + return; + } + + timeElapsed += clock.restart(); +} + +void Countdown::draw(sf::RenderWindow *window) +{ + GameObject::draw(window); + + // Draw the countdown + int timeLeft = durationInSeconds - timeElapsed.asSeconds(); + if (timeLeft <= 0) + { + timeLeft = 0; + } + + int minutes = timeLeft / 60; + int seconds = timeLeft % 60; + std::stringstream ss; + ss << std::setfill('0') << std::setw(2) << minutes << ":" << std::setw(2) << seconds; + auto timeLeftString = ss.str(); + + + auto font = FontManager::getInstance()->getDefaultFont(); + auto text = sf::Text(timeLeftString, *font, COUNTDOWN_FONT_SIZE); + text.setFillColor(sf::Color::White); + text.setPosition(1920 / 2.f, 5); + + // Align top center + sf::FloatRect textRect = text.getLocalBounds(); + text.setOrigin(textRect.left + textRect.width / 2.0f, + textRect.top); + window->draw(text); +} diff --git a/src/game/time/countdown.hpp b/src/game/time/countdown.hpp new file mode 100644 index 0000000..1b4c319 --- /dev/null +++ b/src/game/time/countdown.hpp @@ -0,0 +1,31 @@ +#ifndef HOLESOME_COUNTDOWN_HPP +#define HOLESOME_COUNTDOWN_HPP + + +#include +#include "../game_object.h" + +class Countdown : public GameObject +{ +public: + explicit Countdown(int durationInSeconds = 0); + + void update() override; + + void restart(int durationInSeconds); + + void stop(); + + [[nodiscard]] bool isFinished() const; + + void draw(sf::RenderWindow *window) override; + +private: + sf::Clock clock; + int durationInSeconds = 0; + sf::Time timeElapsed = sf::Time::Zero; + bool stopped = false; +}; + + +#endif //HOLESOME_COUNTDOWN_HPP diff --git a/src/levels.hpp b/src/levels.hpp index 717fada..a4c97ab 100644 --- a/src/levels.hpp +++ b/src/levels.hpp @@ -8,21 +8,25 @@ #define INITIAL_LEVEL "default" std::map const all_levels = { - {"default", LevelConfig("Default", {{0, 0}, - {18, 18}, - {0, 18}, - {18, 0}}, { - CollectableInLevel("box", {3, 5}), - CollectableInLevel("box", {4, 5}), - CollectableInLevel("box", {10, 6}), - CollectableInLevel("box", {2, 8}), - CollectableInLevel("box", {1, 2}), - CollectableInLevel("box", {4, 3}), - CollectableInLevel("box", {8, 3}), - CollectableInLevel("box", {6, 7}), - CollectableInLevel("box", {5, 5}), - CollectableInLevel("box", {9, 5}), - CollectableInLevel("box", {0, 1}) + {"default", LevelConfig("Default", + 30, + { + {0, 0}, + {18, 18}, + {0, 18}, + {18, 0} + }, { + CollectableInLevel("rose", {3, 5}), + CollectableInLevel("rosebush", {4, 5}), + CollectableInLevel("stone", {10, 6}), + CollectableInLevel("bike", {2, 8}), + CollectableInLevel("rose", {1, 2}), + CollectableInLevel("small-tree", {4, 3}), + CollectableInLevel("rose", {8, 3}), + CollectableInLevel("rose", {6, 7}), + CollectableInLevel("rose", {5, 5}), + CollectableInLevel("tram", {9, 5}), + CollectableInLevel("rose", {0, 1}) }, { // Blues @@ -40,25 +44,27 @@ std::map const all_levels = { sf::Color(20, 18, 11), sf::Color::Black }, - TileMapConfig("iso-tiles", {{4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, - {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, - {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, - {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, - {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, - {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, - {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, - {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, - {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, - {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, - {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, - {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, - {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, - {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, - {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, - {4, 0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, - {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, - {4, 1, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, - {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}}) + TileMapConfig("iso-tiles", { + {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, + {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, + {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, + {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, + {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, + {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, + {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, + {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, + {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, + {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, + {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, + {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, + {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, + {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, + {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, + {4, 0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, + {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, + {4, 1, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, + {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4} + }) )} }; diff --git a/src/main.cpp b/src/main.cpp index 462e8b5..607399e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,11 +4,14 @@ #include "texture_config.h" #include "game/level/level_loader.hpp" #include "levels.hpp" +#include "typography/font_manager.hpp" void loadAllTextures(); void runGame(); +void loadAllFonts(); + INITIALIZE_EASYLOGGINGPP int main(int argc, char *argv[]) @@ -16,9 +19,20 @@ int main(int argc, char *argv[]) START_EASYLOGGINGPP(argc, argv); loadAllTextures(); + loadAllFonts(); runGame(); } +void loadAllFonts() +{ + LOG(INFO) << "Loading fonts..."; + for (auto const &[key, path]: all_fonts) + { + FontManager::getInstance()->loadFont(key, path); + } + LOG(INFO) << "Finished loading fonts."; +} + void runGame() { LOG(INFO) << "Starting game ..."; diff --git a/src/sprites/animated_sprite.cpp b/src/sprites/animated_sprite.cpp index 597d708..78e871e 100644 --- a/src/sprites/animated_sprite.cpp +++ b/src/sprites/animated_sprite.cpp @@ -77,3 +77,8 @@ void AnimatedSprite::preRenderUpdate() renderPosition = calculateRotatedCornerPosition(position, angle); } + +bool AnimatedSprite::isVisible() const +{ + return true; +} diff --git a/src/sprites/animated_sprite.hpp b/src/sprites/animated_sprite.hpp index 5baf4aa..bedaf27 100644 --- a/src/sprites/animated_sprite.hpp +++ b/src/sprites/animated_sprite.hpp @@ -25,6 +25,8 @@ public: void preRenderUpdate() override; + bool isVisible() const override; + void setRotation(float angle) override; private: diff --git a/src/sprites/masked_sprite.cpp b/src/sprites/masked_sprite.cpp index d47ba7a..f3a407a 100644 --- a/src/sprites/masked_sprite.cpp +++ b/src/sprites/masked_sprite.cpp @@ -24,7 +24,15 @@ void MaskedSprite::preRenderUpdate() { GameObject::preRenderUpdate(); - // TODO: Did anything change? Is update of masked sprite necessary? + if (isHidden) + { + return; + } + + if (previousAngle == angle && previousRenderPosition == renderPosition) + { + return; + } if (angle == 0) { @@ -38,6 +46,14 @@ void MaskedSprite::preRenderUpdate() sprite->setRotation(angle); updateFreshMaskedSprite(); + + previousAngle = angle; + previousRenderPosition = renderPosition; + + if (isHidden) + { + LOG(INFO) << "Masked sprite has been permanently hidden"; + } } void MaskedSprite::draw(sf::RenderWindow *window) @@ -49,14 +65,12 @@ void MaskedSprite::draw(sf::RenderWindow *window) void MaskedSprite::updateFreshMaskedSprite() { - // TODO: Calculate depth per pixel auto maskedImage = std::make_shared(); maskedImage->create(textureRect.width, textureRect.height, sf::Color::Transparent); - -// todo: or use sf::RenderTexture? - maskedImage->copy(*image, 0, 0); + isHidden = true; + // Calculate world coordinates for each pixel auto coordinatesTopLeftCorner = TranslatedCoordinates( IsometricCoordinates(renderPosition.x, renderPosition.y, coordinates->isometric().depth)); @@ -77,6 +91,7 @@ void MaskedSprite::updateFreshMaskedSprite() topLeftCorner + xAxis * (xOffset * xFactorPerPixel) + yAxis * (yOffset * yFactorPerPixel); if (pixelPosition.y >= 0) { + isHidden = false; continue; } @@ -124,6 +139,7 @@ sf::Color MaskedSprite::calculateNewPixelColor(sf::Color currentColor, sf::Vecto if (position.y >= 0) { // Pixel is above ground + isHidden = false; return currentColor; } @@ -138,6 +154,7 @@ sf::Color MaskedSprite::calculateNewPixelColor(sf::Color currentColor, sf::Vecto auto players = PlayerCollection::getInstance()->getPlayers(); if (players.empty()) { + isHidden = false; return currentColor; } @@ -153,6 +170,16 @@ sf::Color MaskedSprite::calculateNewPixelColor(sf::Color currentColor, sf::Vecto } float biggestHoleAlphaFactor = *std::max_element(holeAlphaFactors.begin(), holeAlphaFactors.end()); + if (biggestHoleAlphaFactor > 0) + { + isHidden = false; + } + currentColor.a *= biggestHoleAlphaFactor; return currentColor; } + +bool MaskedSprite::isVisible() const +{ + return !isHidden; +} diff --git a/src/sprites/masked_sprite.hpp b/src/sprites/masked_sprite.hpp index fa0b649..bd31d36 100644 --- a/src/sprites/masked_sprite.hpp +++ b/src/sprites/masked_sprite.hpp @@ -27,6 +27,8 @@ public: void setRotation(float angle) override; + bool isVisible() const override; + private: std::shared_ptr sprite; std::shared_ptr maskedTexture; @@ -34,7 +36,11 @@ private: std::shared_ptr image; sf::IntRect textureRect; float angle = 0; + float previousAngle = -10; sf::Vector2f renderPosition; + sf::Vector2f previousRenderPosition = sf::Vector2f(-10, 0); + + bool isHidden = false; void updateFreshMaskedSprite(); diff --git a/src/sprites/single_sprite.cpp b/src/sprites/single_sprite.cpp index 4f70e29..b29130c 100644 --- a/src/sprites/single_sprite.cpp +++ b/src/sprites/single_sprite.cpp @@ -61,3 +61,8 @@ void SingleSprite::preRenderUpdate() renderPosition = calculateRotatedCornerPosition(position, angle); } + +bool SingleSprite::isVisible() const +{ + return true; +} diff --git a/src/sprites/single_sprite.hpp b/src/sprites/single_sprite.hpp index 41a69db..b874a2f 100644 --- a/src/sprites/single_sprite.hpp +++ b/src/sprites/single_sprite.hpp @@ -25,6 +25,8 @@ public: void preRenderUpdate() override; + bool isVisible() const override; + private: sf::Sprite sprite; float angle = 0; diff --git a/src/sprites/sprite.hpp b/src/sprites/sprite.hpp index 67ffa1c..3a2629e 100644 --- a/src/sprites/sprite.hpp +++ b/src/sprites/sprite.hpp @@ -25,6 +25,11 @@ public: throw std::runtime_error("Sprite::getSprite() not implemented"); } + [[nodiscard]] virtual bool isVisible() const + { + throw std::runtime_error("Sprite::isVisible() not implemented"); + } + /** * Rotates a sprite around its center. * @param angle [0, 360] diff --git a/src/sprites/versatile_sprite.cpp b/src/sprites/versatile_sprite.cpp index df7a276..c542002 100644 --- a/src/sprites/versatile_sprite.cpp +++ b/src/sprites/versatile_sprite.cpp @@ -46,3 +46,8 @@ void VersatileSprite::setRotation(float angle) { sprite->setRotation(angle); } + +bool VersatileSprite::isVisible() const +{ + return sprite->isVisible(); +} diff --git a/src/sprites/versatile_sprite.hpp b/src/sprites/versatile_sprite.hpp index f25ea81..1c57447 100644 --- a/src/sprites/versatile_sprite.hpp +++ b/src/sprites/versatile_sprite.hpp @@ -22,6 +22,8 @@ public: void setRotation(float angle) override; + bool isVisible() const override; + private: std::shared_ptr sprite = nullptr; }; diff --git a/src/typography/font_manager.cpp b/src/typography/font_manager.cpp new file mode 100644 index 0000000..247b2c5 --- /dev/null +++ b/src/typography/font_manager.cpp @@ -0,0 +1,37 @@ +#include "font_manager.hpp" +#include "../config.h" + +std::shared_ptr FontManager::getInstance() +{ + if (singletonInstance == nullptr) + { + singletonInstance = std::make_shared(); + } + return singletonInstance; +} + +void FontManager::loadFont(const std::string &name, const std::string &fontPath) +{ + auto font = std::make_shared(); + auto fullPath = FONT_BASE_PATH + fontPath; + if (!font->loadFromFile(fullPath)) + { + throw std::runtime_error("Failed to load font: " + fullPath); + } + + fonts[name] = font; +} + +std::shared_ptr FontManager::getFont(const std::string &name) +{ + if (fonts.find(name) == fonts.end()) + { + throw std::runtime_error("Font not found: " + name); + } + return fonts[name]; +} + +std::shared_ptr FontManager::getDefaultFont() +{ + return getFont(DEFAULT_FONT); +} diff --git a/src/typography/font_manager.hpp b/src/typography/font_manager.hpp new file mode 100644 index 0000000..9233b1f --- /dev/null +++ b/src/typography/font_manager.hpp @@ -0,0 +1,27 @@ +#ifndef HOLESOME_FONT_MANAGER_HPP +#define HOLESOME_FONT_MANAGER_HPP + + +#include +#include +#include + +class FontManager +{ +public: + static std::shared_ptr getInstance(); + + void loadFont(const std::string &name, const std::string &fontPath); + + std::shared_ptr getFont(const std::string &name); + + std::shared_ptr getDefaultFont(); + +private: + static inline std::shared_ptr singletonInstance = nullptr; + + std::map> fonts; +}; + + +#endif //HOLESOME_FONT_MANAGER_HPP