From d55b9527caf43bbf975ac2e41d027dcfe871e60d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=D0=9F=D0=B5=D1=87=D0=B5=D0=BD=D0=B8=D0=BD=20=D0=94=D0=B0?=
 =?UTF-8?q?=D0=BD=D0=B8=D0=BB=D0=B0=20=D0=9C=D0=B8=D1=85=D0=B0=D0=B9=D0=BB?=
 =?UTF-8?q?=D0=BE=D0=B2=D0=B8=D1=87?= <dmpechenin@edu.hse.ru>
Date: Sat, 22 Mar 2025 17:15:58 +0300
Subject: [PATCH] Support continues moving of entities

---
 source/Configuration.h                        |  3 ++
 .../DynamicEntities/DynamicEntities.cpp       | 42 ++++++++++++++++---
 .../DynamicEntities/DynamicEntities.h         |  7 +++-
 source/States/GameState/Entities/IEntity.h    |  7 +++-
 .../GameState/Entities/Pacman/Pacman.cpp      | 41 ++++++++++++++++--
 .../States/GameState/Entities/Pacman/Pacman.h |  9 +++-
 .../StaticEntities/StaticEntities.cpp         |  4 ++
 .../Entities/StaticEntities/StaticEntities.h  |  2 +
 .../GameState/GameBuilder/GameBuilder.cpp     | 16 +++++--
 source/States/GameState/GameState.cpp         | 39 +++++++++++++----
 source/States/GameState/GameState.h           |  1 +
 source/States/GameState/Maze/Room/Room.cpp    |  2 +-
 source/States/SelectState/SelectState.cpp     |  2 +-
 13 files changed, 149 insertions(+), 26 deletions(-)

diff --git a/source/Configuration.h b/source/Configuration.h
index 7c04e84..f7d0808 100644
--- a/source/Configuration.h
+++ b/source/Configuration.h
@@ -21,12 +21,15 @@ namespace config {
     constexpr char EASY_GAME_TITLE[] = "Level: Easy";
     constexpr char MEDIUM_GAME_TITLE[] = "Level: Medium";
     constexpr char HARD_GAME_TITLE[] = "Level: Hard";
+    constexpr char EXTREME_GAME_TITLE[] = "Level: Extreme";
     constexpr float EASY_GAME_ENEMY_RATIO = 0.0f;
     constexpr float MEDIUM_GAME_ENEMY_RATIO = 0.03f;
     constexpr float HARD_GAME_ENEMY_RATIO = 0.07f;
     constexpr float ROOM_SIZE = 50;
     constexpr float GAME_ENEMY_SIZE = ROOM_SIZE * 0.7;
     constexpr float GAME_FOOD_SIZE = ROOM_SIZE * 0.1;
+    constexpr float ENEMY_SPEED = 80;
+    constexpr float PACMAN_SPEED = 300;
     // Пакмэн:
     constexpr float GAME_PACMAN_SIZE = ROOM_SIZE * 0.8;
     constexpr sf::Keyboard::Key KEY_LEFT = sf::Keyboard::A;
diff --git a/source/States/GameState/Entities/DynamicEntities/DynamicEntities.cpp b/source/States/GameState/Entities/DynamicEntities/DynamicEntities.cpp
index c2f0ffa..1e228fa 100644
--- a/source/States/GameState/Entities/DynamicEntities/DynamicEntities.cpp
+++ b/source/States/GameState/Entities/DynamicEntities/DynamicEntities.cpp
@@ -4,6 +4,7 @@
 Enemy::Enemy() : m_rectangle({config::GAME_ENEMY_SIZE, config::GAME_ENEMY_SIZE}) {
     m_rectangle.setFillColor(config::GAME_ENEMY_COLOR);
     m_rectangle.setOrigin({config::GAME_ENEMY_SIZE/2, config::GAME_ENEMY_SIZE/2});
+    m_distance_limit = config::ENEMY_SPEED/config::ROOM_SIZE;
 }
 
 std::unique_ptr<IDynamicEntity> Enemy::clone() const {
@@ -11,11 +12,34 @@ std::unique_ptr<IDynamicEntity> Enemy::clone() const {
 }
 
 void Enemy::action() {
-    if (const auto miliseconds = static_cast<size_t>(m_stopwatch.getElapsedTime().asMilliseconds());
-        miliseconds < m_dist_milliseconds(m_rng))
-        return;
-    const auto direction = static_cast<Room::Direction>(m_dist_direction(m_rng));
-    m_ptr_room->get_side(direction)->enter(this);
+    if (m_is_moving) {
+        const sf::Vector2f cur_pos = m_rectangle.getPosition();
+        sf::Vector2f displacement = m_estimated_position - cur_pos;
+        const float distance = std::sqrt(displacement.x * displacement.x + displacement.y * displacement.y);
+        if (distance < m_distance_limit) {
+            m_rectangle.setPosition(m_estimated_position);
+            m_is_moving = false;
+            return;
+        }
+        const float frame_duration = m_stopwatch.getElapsedTime().asSeconds();
+        const sf::Vector2f direction = config::ENEMY_SPEED * frame_duration * displacement / distance;
+        float delta_x, delta_y;
+        if (direction.x > 0)
+            delta_x = std::min(direction.x, displacement.x);
+        else
+            delta_x = std::max(direction.x, displacement.x);
+        if (direction.y > 0)
+            delta_y = std::min(direction.y, displacement.y);
+        else
+            delta_y = std::max(direction.y, displacement.y);
+        m_rectangle.move(delta_x, delta_y);
+    }
+    else {
+        if (static_cast<size_t>(m_stopwatch.getElapsedTime().asMilliseconds()) < m_dist_milliseconds(m_rng))
+            return;
+        const auto direction = static_cast<Room::Direction>(m_dist_direction(m_rng));
+        m_ptr_room->get_side(direction)->enter(this);
+    }
     m_stopwatch.restart();
 }
 
@@ -23,6 +47,12 @@ void Enemy::draw_into(sf::RenderWindow& window) const {
     window.draw(m_rectangle);
 }
 
-void Enemy::prepare_for_drawing()  {
+void Enemy::prepare_for_first_drawing() {
     m_rectangle.setPosition(m_ptr_room->get_position());
 }
+
+void Enemy::prepare_for_drawing() {
+    m_estimated_position = m_ptr_room->get_position();
+    m_is_moving = true;
+    action();
+}
diff --git a/source/States/GameState/Entities/DynamicEntities/DynamicEntities.h b/source/States/GameState/Entities/DynamicEntities/DynamicEntities.h
index d0f1d6e..ceb115f 100644
--- a/source/States/GameState/Entities/DynamicEntities/DynamicEntities.h
+++ b/source/States/GameState/Entities/DynamicEntities/DynamicEntities.h
@@ -15,12 +15,17 @@ public:
     [[nodiscard]] std::unique_ptr<IDynamicEntity> clone() const override;
     void action() override;
     void draw_into(sf::RenderWindow& window) const override;
+    void prepare_for_first_drawing() override;
     void prepare_for_drawing() override;
+    sf::FloatRect getBounds() const { return m_rectangle.getGlobalBounds(); }
     std::unique_ptr<IGameEvent> accept(IVisitor* ptr_visitor) override { return ptr_visitor->visit(this); }
 private:
     sf::RectangleShape m_rectangle;
+    sf::Vector2f m_estimated_position;
+    bool m_is_moving = false;
+    float m_distance_limit;
     sf::Clock m_stopwatch;
     std::mt19937 m_rng{std::random_device{}()};
-    std::uniform_int_distribution<size_t> m_dist_milliseconds{0, 9999};
+    std::uniform_int_distribution<size_t> m_dist_milliseconds{300, 10000};
     std::uniform_int_distribution<> m_dist_direction{0, 3};
 };
diff --git a/source/States/GameState/Entities/IEntity.h b/source/States/GameState/Entities/IEntity.h
index 6b1e5b2..0188517 100644
--- a/source/States/GameState/Entities/IEntity.h
+++ b/source/States/GameState/Entities/IEntity.h
@@ -4,7 +4,12 @@
 
 class IEntity : public IPreparable {
 public:
-    void set_location(Room* ptr_room) noexcept {
+    virtual void prepare_for_first_drawing() = 0;
+    void set_initial_location(Room* ptr_room) {
+        m_ptr_room = ptr_room;
+        prepare_for_first_drawing();
+    }
+    void set_location(Room* ptr_room) {
         m_ptr_room = ptr_room;
         prepare_for_drawing();
     }
diff --git a/source/States/GameState/Entities/Pacman/Pacman.cpp b/source/States/GameState/Entities/Pacman/Pacman.cpp
index c7b6d5f..5a4fad2 100644
--- a/source/States/GameState/Entities/Pacman/Pacman.cpp
+++ b/source/States/GameState/Entities/Pacman/Pacman.cpp
@@ -6,28 +6,61 @@
 Pacman::Pacman() : m_circle{ config::GAME_PACMAN_SIZE/2 } {
     m_circle.setFillColor(config::GAME_COLOR_PACMAN);
     m_circle.setOrigin(config::GAME_PACMAN_SIZE/2, config::GAME_PACMAN_SIZE/2);
+    m_distance_limit = config::PACMAN_SPEED/config::ROOM_SIZE;
 }
 
-void Pacman::move(const Room::Direction direction) {
+void Pacman::replace(const Room::Direction direction) {
+    if (m_is_moving) return;
     m_ptr_room->get_side(direction)->enter(this);
 }
 
+void Pacman::move() {
+    const float frame_duration = m_stopwatch.getElapsedTime().asSeconds();
+    m_stopwatch.restart();
+    if (!m_is_moving) return;
+    const sf::Vector2f cur_pos = m_circle.getPosition();
+    const sf::Vector2f displacement = m_estimated_position - cur_pos;
+    const float distance = std::sqrt(displacement.x * displacement.x + displacement.y * displacement.y);
+    if (distance < m_distance_limit) {
+        m_circle.setPosition(m_estimated_position);
+        m_is_moving = false;
+        return;
+    }
+    const sf::Vector2f direction = config::PACMAN_SPEED * frame_duration * displacement / distance;
+    float delta_x, delta_y;
+    if (direction.x > 0)
+        delta_x = std::min(direction.x, displacement.x);
+    else
+        delta_x = std::max(direction.x, displacement.x);
+    if (direction.y > 0)
+        delta_y = std::min(direction.y, displacement.y);
+    else
+        delta_y = std::max(direction.y, displacement.y);
+    m_circle.move(delta_x, delta_y);
+}
+
 void Pacman::draw_into(sf::RenderWindow& window) const {
     window.draw(m_circle);
 }
 
-void Pacman::prepare_for_drawing() {
+void Pacman::prepare_for_first_drawing() {
     m_circle.setPosition(m_ptr_room->get_position());
 }
 
+void Pacman::prepare_for_drawing() {
+    m_estimated_position = m_ptr_room->get_position();
+    m_is_moving = true;
+    move();
+}
+
 std::unique_ptr<IGameEvent> Pacman::visit(Food* ptr_food) {
-    if (ptr_food->get_location() != this->get_location())
+    if (!this->getBounds().intersects(ptr_food->getBounds()))
         return {};
     return std::make_unique<DeleteStaticEntity>(ptr_food);
 }
 
 std::unique_ptr<IGameEvent> Pacman::visit(Enemy* ptr_enemy) {
-    if (ptr_enemy->get_location() != this->get_location())
+    if (!this->getBounds().intersects(ptr_enemy->getBounds()))
         return {};
     return std::make_unique<LostGame>();
 }
diff --git a/source/States/GameState/Entities/Pacman/Pacman.h b/source/States/GameState/Entities/Pacman/Pacman.h
index c838384..2bd8a83 100644
--- a/source/States/GameState/Entities/Pacman/Pacman.h
+++ b/source/States/GameState/Entities/Pacman/Pacman.h
@@ -5,11 +5,18 @@
 class Pacman final : public IEntity, public IVisitor {
 public:
     Pacman();
-    void move(Room::Direction direction);
+    void replace(Room::Direction direction);
+    void move();
     void draw_into(sf::RenderWindow& window) const override;
+    void prepare_for_first_drawing() override;
     void prepare_for_drawing() override;
+    sf::FloatRect getBounds() const { return m_circle.getGlobalBounds(); }
     std::unique_ptr<IGameEvent> visit(Food* ptr_food) override;
     std::unique_ptr<IGameEvent> visit(Enemy* ptr_enemy) override;
 private:
     sf::CircleShape m_circle;
+    sf::Vector2f m_estimated_position;
+    float m_distance_limit;
+    bool m_is_moving = false;
+    sf::Clock m_stopwatch;
 };
\ No newline at end of file
diff --git a/source/States/GameState/Entities/StaticEntities/StaticEntities.cpp b/source/States/GameState/Entities/StaticEntities/StaticEntities.cpp
index f3e755e..bf11f5c 100644
--- a/source/States/GameState/Entities/StaticEntities/StaticEntities.cpp
+++ b/source/States/GameState/Entities/StaticEntities/StaticEntities.cpp
@@ -14,6 +14,10 @@ void Food::draw_into(sf::RenderWindow& window) const {
     window.draw(m_circle);
 }
 
+void Food::prepare_for_first_drawing() {
+    prepare_for_drawing();
+}
+
 void Food::prepare_for_drawing() {
     m_circle.setPosition(m_ptr_room->get_position());
 }
diff --git a/source/States/GameState/Entities/StaticEntities/StaticEntities.h b/source/States/GameState/Entities/StaticEntities/StaticEntities.h
index cf09a19..4c8bcfb 100644
--- a/source/States/GameState/Entities/StaticEntities/StaticEntities.h
+++ b/source/States/GameState/Entities/StaticEntities/StaticEntities.h
@@ -12,7 +12,9 @@ public:
     Food();
     [[nodiscard]] std::unique_ptr<IStaticEntity> clone() const override;
     void draw_into(sf::RenderWindow& window) const override;
+    void prepare_for_first_drawing() override;
     void prepare_for_drawing() override;
+    sf::FloatRect getBounds() const { return m_circle.getGlobalBounds(); }
     std::unique_ptr<IGameEvent> accept(IVisitor* ptr_visitor) override { return ptr_visitor->visit(this); }
 private:
     sf::CircleShape m_circle;
diff --git a/source/States/GameState/GameBuilder/GameBuilder.cpp b/source/States/GameState/GameBuilder/GameBuilder.cpp
index d572ca0..26ab0f6 100644
--- a/source/States/GameState/GameBuilder/GameBuilder.cpp
+++ b/source/States/GameState/GameBuilder/GameBuilder.cpp
@@ -18,22 +18,30 @@ void CommonBuilder::create_context(const float dynamic_objets_ratio) {
     std::uniform_int_distribution distrib(0 , static_cast<int>(buf_rooms.size()-1));
 
     auto pos = buf_rooms.begin() + distrib(gen);
-    m_context.pacman.set_location((*pos)->get());
+    const sf::Vector2f pacman_pos = (*pos)->get()->get_position();
+    m_context.pacman.set_initial_location((*pos)->get());
     buf_rooms.erase(pos);
 
     if (dynamic_objets_ratio > 1) throw std::invalid_argument("GAME_ENEMY_RATIO should be <= 1");
     const auto number_of_dynamic_entities = static_cast<size_t>(static_cast<float>(buf_rooms.size()) * dynamic_objets_ratio);
     for (size_t i = 0; i < number_of_dynamic_entities; ++i) {
-        m_context.dynamic_objects.emplace_back(std::move(std::make_unique<Enemy>()));
         distrib.param(std::uniform_int_distribution<>::param_type(0, static_cast<int>(buf_rooms.size()) - 1));
         pos = buf_rooms.begin() + distrib(gen);
-        m_context.dynamic_objects.back()->set_location((*pos)->get());
+        if (float pos_x = (*pos)->get()->get_position().x, pos_y = (*pos)->get()->get_position().y;
+            std::sqrt((pos_x-pacman_pos.x) * (pos_x-pacman_pos.x) + (pos_y-pacman_pos.y) * (pos_y-pacman_pos.y)) <= m_room_size * 2) {
+            m_context.static_objects.emplace_back(std::move(std::make_unique<Food>()));
+            m_context.static_objects.back()->set_initial_location((*pos)->get());
+        }
+        else {
+            m_context.dynamic_objects.emplace_back(std::move(std::make_unique<Enemy>()));
+            m_context.dynamic_objects.back()->set_initial_location((*pos)->get());
+        }
         buf_rooms.erase(pos);
     }
 
     for (const auto& it_room : buf_rooms) {
         m_context.static_objects.emplace_back(std::move(std::make_unique<Food>()));
-        m_context.static_objects.back()->set_location(it_room->get());
+        m_context.static_objects.back()->set_initial_location(it_room->get());
     }
 }
 
diff --git a/source/States/GameState/GameState.cpp b/source/States/GameState/GameState.cpp
index ae6fa45..601d867 100644
--- a/source/States/GameState/GameState.cpp
+++ b/source/States/GameState/GameState.cpp
@@ -1,3 +1,4 @@
+#include <BasicAbstractions/Font.h>
 #include <States/GameState/GameState.h>
 #include <States/SelectState/SelectState.h>
 
@@ -34,27 +35,51 @@ void GameState::update() {
         events.emplace_back(std::make_unique<WinGame>());
     }
 
+    m_context_manager.get_current_context().pacman.move();
+
     for (const auto& event : events)
         event->handle(m_context_manager.get_current_context());
 }
 
 void GameState::render() {
+    sf::Text text;
+    sf::RectangleShape rect;
     switch (m_context_manager.get_current_context().state) {
         case GameContext::INGAME:
             m_window.clear(config::GAME_COLOR_BACKGROUND_INGAME);
             break;
+        case GameContext::LOST:
+            text.setFont(MyFont::Instance());
+            text.setFillColor({255, 0, 0, 220});
+            text.setString("LOST!");
+            text.setCharacterSize(300);
+            text.setStyle(sf::Text::Bold);
+            text.setPosition(
+                round((m_window.getSize().x - text.getLocalBounds().width) / 2 - text.getLocalBounds().left),
+                round((m_window.getSize().y - text.getLocalBounds().height) / 2 - text.getLocalBounds().top)
+            );
+            rect.setSize(sf::Vector2f(text.getLocalBounds().width + 100, text.getLocalBounds().height + 100));
+            rect.setFillColor(sf::Color(240, 0, 0, 50));
+            rect.setOutlineThickness(5);
+            rect.setOutlineColor(sf::Color::White);
+            rect.setPosition(
+                round((m_window.getSize().x - rect.getSize().x) / 2),
+                round((m_window.getSize().y - rect.getSize().y) / 2)
+            );
+            m_window.clear(config::GAME_COLOR_BACKGROUND_INGAME);
+            break;
         case GameContext::WIN:
             m_window.clear(config::GAME_COLOR_BACKGROUND_WIN);
             break;
-        case GameContext::LOST:
-            m_window.clear(config::GAME_COLOR_BACKGROUND_LOST);
     }
     m_maze.draw_into(m_window);
     for (const auto& obj : m_context_manager.get_current_context().static_objects)
         obj->draw_into(m_window);
+    m_context_manager.get_current_context().pacman.draw_into(m_window);
     for (const auto& obj : m_context_manager.get_current_context().dynamic_objects)
         obj->draw_into(m_window);
-    m_context_manager.get_current_context().pacman.draw_into(m_window);
+    m_window.draw(rect);
+    m_window.draw(text);
     m_window.display();
 }
 
@@ -62,19 +87,19 @@ void GameState::process_key_pressed(const sf::Keyboard::Key key) {
     switch (key) {
         case config::KEY_UP:
             m_context_manager.save_context();
-            m_context_manager.get_current_context().pacman.move(Room::UP);
+            m_context_manager.get_current_context().pacman.replace(Room::UP);
             break;
         case config::KEY_DOWN:
             m_context_manager.save_context();
-            m_context_manager.get_current_context().pacman.move(Room::DOWN);
+            m_context_manager.get_current_context().pacman.replace(Room::DOWN);
             break;
         case config::KEY_LEFT:
             m_context_manager.save_context();
-            m_context_manager.get_current_context().pacman.move(Room::LEFT);
+            m_context_manager.get_current_context().pacman.replace(Room::LEFT);
             break;
         case config::KEY_RIGHT:
             m_context_manager.save_context();
-            m_context_manager.get_current_context().pacman.move(Room::RIGHT);
+            m_context_manager.get_current_context().pacman.replace(Room::RIGHT);
             break;
         default: break;
     }
diff --git a/source/States/GameState/GameState.h b/source/States/GameState/GameState.h
index 1a5df43..83cdfbf 100644
--- a/source/States/GameState/GameState.h
+++ b/source/States/GameState/GameState.h
@@ -10,6 +10,7 @@ public:
     explicit GameState(IStateManager& state_manager, const sf::VideoMode& video_mode, const sf::String& window_title) :
         IState(state_manager), IWindowKeeper(video_mode, window_title) {
         m_window.setFramerateLimit(config::FRAME_RATE_LIMIT);
+        m_window.setKeyRepeatEnabled(false);
     }
     bool do_step() override;
     void set_maze(Maze&& maze) { m_maze = std::move(maze); }
diff --git a/source/States/GameState/Maze/Room/Room.cpp b/source/States/GameState/Maze/Room/Room.cpp
index d1a7cc8..da320fe 100644
--- a/source/States/GameState/Maze/Room/Room.cpp
+++ b/source/States/GameState/Maze/Room/Room.cpp
@@ -6,7 +6,7 @@ Room::Room(float size) : m_rectangle({size, size}) {
 
 void Room::set_side(const Direction side, const std::shared_ptr<IRoomSide>& ptr_side) {
     if (side == INVALID) throw std::invalid_argument("Invalid direction");
-    m_sides[side] = std::move(ptr_side);
+    m_sides[side] = ptr_side;
     m_sides[side]->prepare_for_drawing();
 }
 
diff --git a/source/States/SelectState/SelectState.cpp b/source/States/SelectState/SelectState.cpp
index c86ff97..cd5092b 100644
--- a/source/States/SelectState/SelectState.cpp
+++ b/source/States/SelectState/SelectState.cpp
@@ -36,7 +36,7 @@ Menu::Menu(IStateManager& state_manager) {
         std::make_unique<GameBuilderDirector>(
             std::make_unique<ComplexRandomBuilder>(config::GAME_VIDEO_MODE.width, config::GAME_VIDEO_MODE.height, config::ROOM_SIZE, config::GAME_COLOR_ROOM),
             config::GAME_VIDEO_MODE,
-            config::HARD_GAME_TITLE,
+            config::EXTREME_GAME_TITLE,
             config::HARD_GAME_ENEMY_RATIO));
 
     m_buttons[0].set(sf::Vector2f{pos_left, pos_top}, config::BUTTON_SIZE, config::BUTTON_TEXT_EASY,
-- 
GitLab