diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a7f5bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +cmake-build-debug/ +CMakeCache.txt +cmake_install.cmake +Makefile +TanksGame + +*.o + +/build/ \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..7367234 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +TanksGame \ No newline at end of file diff --git a/.idea/TankiGame.iml b/.idea/TankiGame.iml new file mode 100644 index 0000000..f08604b --- /dev/null +++ b/.idea/TankiGame.iml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/.idea/editor.xml b/.idea/editor.xml new file mode 100644 index 0000000..ddf3db3 --- /dev/null +++ b/.idea/editor.xml @@ -0,0 +1,345 @@ + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..0b76fe5 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d965816 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "C_Cpp.default.compileCommands": "${workspaceFolder}/cmake-build-debug/compile_commands.json", + "C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools" +} diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..8dd7b48 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,41 @@ +cmake_minimum_required(VERSION 3.16) +project(TanksGame LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +find_package(Qt5 REQUIRED COMPONENTS Core Widgets Multimedia) + +add_executable(TanksGame + src/main.cpp + src/GameScene.cpp + src/GameModel.cpp + src/InputController.cpp + include/GameScene.h + include/GameModel.h + include/InputController.h + src/Bonus.cpp + src/Tank.cpp + src/TankView.cpp + src/EnemyTank.cpp + src/KamikazeEnemy.cpp + src/LightEnemy.cpp + src/HeavyEnemy.cpp + src/TwinShooterEnemy.cpp + src/Wall.cpp + src/BrickWall.cpp + src/Bullet.cpp + include/TankView.h +) + +target_link_libraries(TanksGame PRIVATE + Qt5::Core + Qt5::Widgets +) + +target_link_libraries(TanksGame PRIVATE Qt5::Multimedia) +target_include_directories(TanksGame PUBLIC ${CMAKE_SOURCE_DIR}/include) \ No newline at end of file diff --git a/README.md b/README.md index 8b13789..496bcac 100644 --- a/README.md +++ b/README.md @@ -1 +1,25 @@ +# Tanks + + игра «ТанкиNSU», написанная с использованием Qt5 + +## Структура проекта +- `include/` — все заголовочные файлы (.h) +- `src/` — реализации (.cpp) +- `CMakeLists.txt` — конфигурация сборки CMake + + +## Краткая инструкция по сборке +Требования: +- C++ компилятор с поддержкой C++20 +- CMake >= 3.16 +- Qt5 + +Сборка из корня проекта : + +```bash +mkdir -p cmake-build-debug +cd cmake-build-debug +cmake .. +cmake --build . --config Debug -- -j 4 +``` diff --git a/include/Bonus.h b/include/Bonus.h new file mode 100644 index 0000000..f263c59 --- /dev/null +++ b/include/Bonus.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +class GameScene; + +class Bonus : public QGraphicsEllipseItem { + public: + explicit Bonus(qreal x, qreal y, qreal w = 16.0, qreal h = 16.0); + ~Bonus() override = default; + + virtual void Apply(GameScene* scene) = 0; +}; + +class HealthBonus : public Bonus { + public: + explicit HealthBonus(qreal x, qreal y); + void Apply(GameScene* scene) override; +}; + +class MineBonus : public Bonus { + public: + explicit MineBonus(qreal x, qreal y); + void Apply(GameScene* scene) override; +}; + +class ScoreBonus : public Bonus { + public: + explicit ScoreBonus(qreal x, qreal y); + void Apply(GameScene* scene) override; +}; diff --git a/include/BrickWall.h b/include/BrickWall.h new file mode 100644 index 0000000..9b64ea0 --- /dev/null +++ b/include/BrickWall.h @@ -0,0 +1,9 @@ +#pragma once +#include + +class BrickWall : public QGraphicsPixmapItem { +public: + BrickWall(qreal x, qreal y); + QRectF GetRect() const; + static constexpr int kDurability = 3; +}; diff --git a/include/Bullet.h b/include/Bullet.h new file mode 100644 index 0000000..f83719c --- /dev/null +++ b/include/Bullet.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +enum class Direction; +enum class BulletOwner { + Player, + Enemy, +}; + +class Bullet final : public QGraphicsPixmapItem { +public: + explicit Bullet(qreal x, qreal y, Direction direction, BulletOwner owner); + + void Move(); + Direction GetDirection() const; + BulletOwner GetOwner() const; + QRectF GetBoundingRect() const; + +private: + Direction m_direction; + BulletOwner m_owner; + + static constexpr qreal kSpeed = 5.0; + static constexpr qreal kSize = 6.0; +}; diff --git a/include/CloneFactory.hpp b/include/CloneFactory.hpp new file mode 100644 index 0000000..fd37b5f --- /dev/null +++ b/include/CloneFactory.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include +#include +#include "Factory.hpp" + +template < + class AbstractProduct, + class ProductCreator = std::function(const AbstractProduct*)>, + template class FactoryErrorPolicy = DefaultFactoryError +> +class CloneFactory { +public: + using CreatorType = ProductCreator; + using TypeKey = std::type_index; + + bool Register(const std::type_info& ti, CreatorType creator) { + TypeKey key(ti); + auto res = associations_.emplace(key, std::move(creator)); + return res.second; + } + + bool Unregister(const std::type_info& ti) { + TypeKey key(ti); + return associations_.erase(key) > 0; + } + + std::unique_ptr CreateObject(const AbstractProduct* model) { + if (!model) { + return FactoryErrorPolicy::OnUnknownType(TypeKey(typeid(void))); + } + TypeKey key(typeid(*model)); + auto it = associations_.find(key); + if (it != associations_.end()) { + return (it->second)(model); + } + return FactoryErrorPolicy::OnUnknownType(key); + } + +private: + std::map associations_; +}; diff --git a/include/EnemyTank.h b/include/EnemyTank.h new file mode 100644 index 0000000..c2edfd0 --- /dev/null +++ b/include/EnemyTank.h @@ -0,0 +1,53 @@ +#pragma once +#include +#include +#include +#include + +class QPainter; +#include "Tank.h" + +class Bullet; + + +class EnemyTank : public QGraphicsPixmapItem { +public: + explicit EnemyTank(qreal x, qreal y); + + virtual void Update(); + virtual QRectF GetFutureRect(Direction direction) const; + Direction GetDirection() const { return m_direction; } + + void SetDirection(Direction dir); + void ResetMoveTimer(int frames) { moveCooldown = frames; } + void ResetFireTimer(int frames) { fireCooldown = frames; } + int GetMoveCooldown() const { return moveCooldown; } + int GetFireCooldown() const { return fireCooldown; } + + virtual qreal GetSpeed() const { return Speed; } + virtual qreal GetWidth() const { return Width; } + virtual qreal GetHeight() const { return Height; } + + + int GetHP() const { return hp; } + void TakeDamage(int dmg); + bool IsDead() const { return hp <= 0; } + + + virtual std::vector> Fire(Direction dir); + +private: + static constexpr qreal Speed = 30.0; + static constexpr qreal Width = 32.0; + static constexpr qreal Height = 32.0; + +protected: + virtual void UpdatePixmap(); + void DrawGun(QPainter& painter, int center, int gunLength) const; + + Direction m_direction = Direction::Down; + int moveCooldown = 0; + int fireCooldown = 0; + int hp = 1; +}; + diff --git a/include/Factory.hpp b/include/Factory.hpp new file mode 100644 index 0000000..34eb894 --- /dev/null +++ b/include/Factory.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include +#include + +template +class DefaultFactoryError { +public: + class Exception : public std::exception { + public: + Exception(const IdentifierType& unknownId) : unknownId_(unknownId) {} + virtual const char* what() const noexcept override { + return "unknown type ident"; + } + const IdentifierType& GetId() const { return unknownId_; } + private: + IdentifierType unknownId_; + }; + +protected: + static std::unique_ptr OnUnknownType(const IdentifierType& id) { + throw Exception(id); + } +}; + +template < + class AbstractProduct, + class IdentifierType, + class ProductCreator = std::function()>, + template class FactoryErrorPolicy = DefaultFactoryError +> +class Factory : public FactoryErrorPolicy { +public: + using CreatorType = ProductCreator; + + bool Register(const IdentifierType& id, CreatorType creator) { + auto res = associations_.emplace(id, std::move(creator)); + return res.second; + } + + bool Unregister(const IdentifierType& id) { + return associations_.erase(id) > 0; + } + + std::unique_ptr CreateObject(const IdentifierType& id) { + auto it = associations_.find(id); + if (it != associations_.end()) { + return (it->second)(); + } + return FactoryErrorPolicy::OnUnknownType(id); + } + +private: + std::map associations_; +}; diff --git a/include/GameModel.h b/include/GameModel.h new file mode 100644 index 0000000..cbb6786 --- /dev/null +++ b/include/GameModel.h @@ -0,0 +1,99 @@ +#pragma once + +#include +#include +#include + +class Tank; +class Wall; +class BrickWall; +class Bullet; +class EnemyTank; +class Bonus; + +class GameModel : public QObject { + Q_OBJECT +public: + explicit GameModel(QObject* parent = nullptr); + ~GameModel(); + + int GetLives() const { return m_lives; } + int GetKills() const { return m_kills; } + bool IsGameOver() const { return m_gameOver; } + bool IsGameWon() const { return m_gameWon; } + +public slots: + void ModifyPlayerLives(int delta); + void ModifyScore(int delta); + void SetGameOver(bool v = true); + void SetGameWon(bool v = true); + void SetLives(int lives); + void SetKills(int kills); + +signals: + void livesChanged(int); + void scoreChanged(int); + void gameOver(); + void gameWon(); + +private: + int m_lives = 3; + int m_kills = 0; + bool m_gameOver = false; + bool m_gameWon = false; + + public: + void SetPlayer(std::unique_ptr p); + Tank* GetPlayer() const; + void ClearPlayer(); + + Wall* AddWall(std::unique_ptr w); + std::vector GetWalls() const; + void ClearWalls(); + size_t GetWallsCount() const; + Wall* GetWallAt(size_t i) const; + + BrickWall* AddBrick(std::unique_ptr b); + std::vector GetBricks() const; + void RemoveBrick(BrickWall* b); + void ClearBricks(); + size_t GetBricksCount() const; + BrickWall* GetBrickAt(size_t i) const; + + Bullet* AddBullet(std::unique_ptr b); + std::vector GetBullets() const; + void RemoveBullet(Bullet* b); + void ClearBullets(); + size_t GetBulletsCount() const; + Bullet* GetBulletAt(size_t i) const; + + Bullet* AddEnemyBullet(std::unique_ptr b); + std::vector GetEnemyBullets() const; + void RemoveEnemyBullet(Bullet* b); + void ClearEnemyBullets(); + size_t GetEnemyBulletsCount() const; + Bullet* GetEnemyBulletAt(size_t i) const; + + EnemyTank* AddEnemyTank(std::unique_ptr e); + std::vector GetEnemyTanks() const; + void RemoveEnemyTank(EnemyTank* e); + void ClearEnemyTanks(); + size_t GetEnemyTanksCount() const; + EnemyTank* GetEnemyTankAt(size_t i) const; + + Bonus* AddBonus(std::unique_ptr b); + std::vector GetBonuses() const; + void RemoveBonus(Bonus* b); + void ClearBonuses(); + size_t GetBonusesCount() const; + Bonus* GetBonusAt(size_t i) const; + + private: + std::unique_ptr player; + std::vector> walls; + std::vector> bricks; + std::vector> bullets; + std::vector> enemyBullets; + std::vector> enemyTanks; + std::vector> bonuses; +}; diff --git a/include/GameScene.h b/include/GameScene.h new file mode 100644 index 0000000..00ee567 --- /dev/null +++ b/include/GameScene.h @@ -0,0 +1,94 @@ +#pragma once + +#include +#include +#include +#include +#include +#include "GameModel.h" +#include "Bullet.h" +#include "Tank.h" +#include "Wall.h" +#include "BrickWall.h" +#include +#include "Factory.hpp" +#include +#include +#include +#include + +class Bullet; +class EnemyTank; +class Bonus; +class InputController; +class TankView; + +class GameScene final : public QGraphicsScene { + Q_OBJECT + + public: + explicit GameScene(QObject* parent = nullptr); + ~GameScene(); + void ModifyPlayerLives(int delta); + void ModifyScore(int delta); + + private slots: + void Update(); + + + void SetLivesTextStyle(); + private: + + void keyPressEvent(QKeyEvent* event) override; + void keyReleaseEvent(QKeyEvent* event) override; + + void HandlePlayerInputAndMovement(); + void SpawnEnemiesIfNeeded(); + void UpdateEnemies(); + void UpdatePlayerBullets(); + void UpdateEnemyBullets(); + + + void FireBullet(); + void RestartGame(); + void InitializeLevel(); + void UpdateBonuses(); + + bool IsCollidingWithSolidWall(const QRectF& rect) const; + BrickWall* FindCollidingBrickWall(const QRectF& rect) const; + bool IsCollidingWithAnyWall(const QRectF& rect) const; + + void UpdateLivesDisplay(); + void UpdateScoreDisplay(); + void ShowGameOver(); + void ShowWinScreen(); + + GameModel* m_model = nullptr; + Factory m_enemyFactory; + bool m_canFire = true; + int m_enemySpawnCooldown = 0; + + + QTimer m_gameTimer; + InputController* m_inputController = nullptr; + + QList m_freeSpawnPoints; + + + QGraphicsTextItem* m_livesText = nullptr; + QGraphicsTextItem* m_scoreText = nullptr; + + + QGraphicsRectItem* m_overlay = nullptr; + QGraphicsRectItem* m_gameOverBox = nullptr; + QGraphicsTextItem* m_gameOverText = nullptr; + QGraphicsTextItem* m_restartText = nullptr; + + TankView* m_playerView = nullptr; + + + QGraphicsRectItem* m_winOverlay = nullptr; + QGraphicsRectItem* m_winBox = nullptr; + QGraphicsTextItem* m_winText = nullptr; + QGraphicsTextItem* m_winRestartText = nullptr; +}; diff --git a/include/GlobalConstants.h b/include/GlobalConstants.h new file mode 100644 index 0000000..30b089a --- /dev/null +++ b/include/GlobalConstants.h @@ -0,0 +1,80 @@ +#pragma once + +// Игровые константы: размеры сцены, карта, лимиты и условия победы +constexpr int kSceneWidth = 640; +constexpr int kSceneHeight = 640; +constexpr int kTileSize = 32; +constexpr int kMapRows = 20; +constexpr int kMapCols = 20; +constexpr int kMaxEnemies = 4; +constexpr int kSpawnCooldown = 60; +constexpr int kWinConditionKills = 10; +constexpr int kInitialPlayerLives = 3; + +// Параметры танка: размеры спрайта, скорость и компоненты для отрисовки +constexpr int kTankSize = 32; +constexpr int kTankSpeed = 2; +constexpr int kTankBodyX = 4; +constexpr int kTankBodyY = 8; +constexpr int kTankBodyWidth = 24; +constexpr int kTankBodyHeight = 16; +constexpr int kTankTrackWidth = 4; +constexpr int kTankTrackHeight = 12; +constexpr int kTankTrackLeftX = 2; +constexpr int kTankTrackRightX = 26; +constexpr int kTankTurretX = 12; +constexpr int kTankTurretY = 12; +constexpr int kTankTurretSize = 8; +constexpr int kTankGunCenter = 16; +constexpr int kTankGunLength = 12; + +// Параметры вражеского танка +constexpr int kEnemyTankSize = kTankSize; +constexpr int kEnemyTankSpeed = 2; +constexpr int kEnemyTankBodyX = kTankBodyX; +constexpr int kEnemyTankBodyY = kTankBodyY; +constexpr int kEnemyTankBodyWidth = kTankBodyWidth; +constexpr int kEnemyTankBodyHeight = kTankBodyHeight; +constexpr int kEnemyTankTrackWidth = kTankTrackWidth; +constexpr int kEnemyTankTrackHeight = kTankTrackHeight; +constexpr int kEnemyTankTurretX = kTankTurretX; +constexpr int kEnemyTankTurretY = kTankTurretY; +constexpr int kEnemyTankTurretSize = kTankTurretSize; +constexpr int kEnemyTankGunCenter = kTankGunCenter; +constexpr int kEnemyTankGunLength = kTankGunLength; + +// Пуля: размер и скорость +constexpr qreal kBulletSize = 6.0; +constexpr qreal kBulletSpeed = 4.0; + +// Кирпичная стена: параметры рисунка +constexpr int kBrickWallSize = 32; +constexpr int kBrickWidth = 14; +constexpr int kBrickHeight = 14; +constexpr int kBrickGap = 2; +constexpr int kBrickRows = 2; +constexpr int kBrickCols = 2; + +// Стена: параметры панелей и заклепок +constexpr int kWallSize = 32; +constexpr int kPanelSize = 12; +constexpr int kPanelSpacing = 4; +constexpr int kRivetRadius = 1; + +// Бонусы: размер и смещение +constexpr qreal kBonusSize = 16.0; +constexpr qreal kBonusOffset = 8.0; + +// Коллизии +constexpr int kCollisionInset = 4; + +// Задержка между выстрелами +constexpr int kPlayerFireCooldownFrames = 18; + +// Цвета +#include +inline const QColor kSceneBackgroundColor = QColor(20, 40, 20); +inline const QColor kTankBodyColor = QColor(50, 205, 50); +inline const QColor kTankTrackColor = QColor(0, 100, 0); +inline const QColor kTankTurretColor = QColor(34, 139, 34); + diff --git a/include/HeavyEnemy.h b/include/HeavyEnemy.h new file mode 100644 index 0000000..d9975e3 --- /dev/null +++ b/include/HeavyEnemy.h @@ -0,0 +1,10 @@ +#pragma once +#include "EnemyTank.h" + +class HeavyEnemy : public EnemyTank { +public: + explicit HeavyEnemy(qreal x, qreal y); + qreal GetSpeed() const override { return 1; } +protected: + void UpdatePixmap() override; +}; diff --git a/include/InputController.h b/include/InputController.h new file mode 100644 index 0000000..98f6111 --- /dev/null +++ b/include/InputController.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include + +class GameModel; + +class InputController : public QObject { + Q_OBJECT +public: + explicit InputController(GameModel* model, QObject* parent = nullptr); + + void HandleKeyPress(QKeyEvent* event); + void HandleKeyRelease(QKeyEvent* event); + + bool IsKeyPressed(Qt::Key key) const; + +signals: + void fireRequested(); + void restartRequested(); + +private: + QSet m_pressedKeys; + GameModel* m_model = nullptr; +}; diff --git a/include/KamikazeEnemy.h b/include/KamikazeEnemy.h new file mode 100644 index 0000000..fbe769e --- /dev/null +++ b/include/KamikazeEnemy.h @@ -0,0 +1,15 @@ +#pragma once + +#include "EnemyTank.h" + +class KamikazeEnemy : public EnemyTank { +public: + explicit KamikazeEnemy(qreal x, qreal y); + + void Update() override; + qreal GetSpeed() const override { return 3.0; } + qreal GetWidth() const override { return 28.0; } + qreal GetHeight() const override { return 28.0; } + + std::vector> Fire(Direction dir) override; +}; diff --git a/include/LightEnemy.h b/include/LightEnemy.h new file mode 100644 index 0000000..14dd78a --- /dev/null +++ b/include/LightEnemy.h @@ -0,0 +1,10 @@ +#pragma once +#include "EnemyTank.h" + +class LightEnemy : public EnemyTank { +public: + explicit LightEnemy(qreal x, qreal y); + qreal GetSpeed() const override { return 1.5; } +protected: + void UpdatePixmap() override; +}; diff --git a/include/Tank.h b/include/Tank.h new file mode 100644 index 0000000..137d117 --- /dev/null +++ b/include/Tank.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include + +#include "TankModel.h" + +enum class Direction +{ + Up, + Down, + Left, + Right +}; + +class Tank final +{ +public: + explicit Tank(qreal x, qreal y); + + TankModel* GetModel() const { return m_model.get(); } + QPointF GetPosition() const { return m_pos; } + void SetPosition(const QPointF& p) { m_pos = p; } + + void SetDirection(Direction direction); + void Update(); + void Move(); + auto GetFutureRect(Direction dir) const -> QRectF; + auto GetDirection() const -> Direction; + + +private: + Direction m_direction { Direction::Up }; + static constexpr qreal kSpeed = 2.0; + + QPointF m_pos{0,0}; + std::unique_ptr m_model; +}; diff --git a/include/TankModel.h b/include/TankModel.h new file mode 100644 index 0000000..1a898ff --- /dev/null +++ b/include/TankModel.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include + +class TankModel { +public: + TankModel() = default; + TankModel(qreal speed, int hp, int damage, int armor) + : m_speed(speed), m_hp(hp), m_damage(damage), m_armor(armor) {} + + qreal GetSpeed() const { return m_speed; } + int GetHP() const { return m_hp; } + int GetDamage() const { return m_damage; } + int GetArmor() const { return m_armor; } + + void SetSpeed(qreal v) { m_speed = v; } + void SetHP(int v) { m_hp = v; } + void SetDamage(int v) { m_damage = v; } + void SetArmor(int v) { m_armor = v; } + + void TakeDamage(int dmg) { + int effective = dmg - m_armor; + if (effective < 0) effective = 0; + m_hp -= effective; + m_damageLog.push_back(std::to_string(dmg)); + } + + bool IsDead() const { return m_hp <= 0; } + + const std::vector& GetDamageLog() const { return m_damageLog; } + +private: + qreal m_speed{2.0}; + int m_hp{1}; + int m_damage{1}; + int m_armor{0}; + std::vector m_damageLog; +}; diff --git a/include/TankView.h b/include/TankView.h new file mode 100644 index 0000000..94aab2c --- /dev/null +++ b/include/TankView.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include +#include "Tank.h" + +class TankView : public QGraphicsPixmapItem { +public: + explicit TankView(Tank* model); + void UpdateView(); + +private: + void UpdatePixmap(); + void DrawGun(QPainter& painter, int center, int gunLength) const; + + Tank* m_model = nullptr; +}; diff --git a/include/TwinShooterEnemy.h b/include/TwinShooterEnemy.h new file mode 100644 index 0000000..68443c1 --- /dev/null +++ b/include/TwinShooterEnemy.h @@ -0,0 +1,11 @@ +#pragma once +#include "EnemyTank.h" + +class TwinShooterEnemy : public EnemyTank { +public: + explicit TwinShooterEnemy(qreal x, qreal y); + qreal GetSpeed() const override { return 0.8; } +protected: + void UpdatePixmap() override; + std::vector> Fire(Direction dir) override; +}; diff --git a/include/Wall.h b/include/Wall.h new file mode 100644 index 0000000..89c93ed --- /dev/null +++ b/include/Wall.h @@ -0,0 +1,13 @@ +#pragma once +#include + +class QPainter; + +class Wall final : public QGraphicsPixmapItem +{ +public: + explicit Wall(qreal x, qreal y); + +private: + void InitRivets(QPainter& painter); +}; diff --git a/src/Bonus.cpp b/src/Bonus.cpp new file mode 100644 index 0000000..2c66731 --- /dev/null +++ b/src/Bonus.cpp @@ -0,0 +1,39 @@ +#include "Bonus.h" +#include "GameScene.h" +#include "GlobalConstants.h" + +#include +#include + +Bonus::Bonus(qreal x, qreal y, qreal w, qreal h) : QGraphicsEllipseItem(0, 0, w, h) { + setPos(x + kBonusOffset, y + kBonusOffset); + setPen(Qt::NoPen); + setZValue(5); +} + +HealthBonus::HealthBonus(qreal x, qreal y) : Bonus(x, y, kBonusSize, kBonusSize) { + setBrush(QBrush(Qt::green)); +} + +void HealthBonus::Apply(GameScene* scene) { + if (!scene) return; + scene->ModifyPlayerLives(+1); +} + +MineBonus::MineBonus(qreal x, qreal y) : Bonus(x, y, kBonusSize, kBonusSize) { + setBrush(QBrush(Qt::red)); +} + +void MineBonus::Apply(GameScene* scene) { + if (!scene) return; + scene->ModifyPlayerLives(-1); +} + +ScoreBonus::ScoreBonus(qreal x, qreal y) : Bonus(x, y, kBonusSize, kBonusSize) { + setBrush(QBrush(Qt::cyan)); +} + +void ScoreBonus::Apply(GameScene* scene) { + if (!scene) return; + scene->ModifyScore(5); +} diff --git a/src/BrickWall.cpp b/src/BrickWall.cpp new file mode 100644 index 0000000..1dd6350 --- /dev/null +++ b/src/BrickWall.cpp @@ -0,0 +1,36 @@ + +#include "BrickWall.h" + +#include +#include + +#include "GlobalConstants.h" + +BrickWall::BrickWall(qreal x, qreal y) { + setPos(x, y); + setZValue(0); + + QPixmap pixmap(kBrickWallSize, kBrickWallSize); + pixmap.fill(Qt::transparent); + + QPainter painter(&pixmap); + painter.setRenderHint(QPainter::Antialiasing, false); + + painter.fillRect(0, 0, kBrickWallSize, kBrickWallSize, + QColor(200, 200, 200)); + + for (int row = 0; row < kBrickRows; ++row) { + for (int col = 0; col < kBrickCols; ++col) { + int bx = col * (kBrickWidth + kBrickGap) + kBrickGap; + int by = row * (kBrickHeight + kBrickGap) + kBrickGap; + + painter.fillRect(bx, by, kBrickWidth, kBrickHeight, + QColor(178, 34, 34)); + painter.setPen(Qt::white); + painter.drawRect(bx, by, kBrickWidth, kBrickHeight); + } + } + + painter.end(); + setPixmap(pixmap); +} diff --git a/src/Bullet.cpp b/src/Bullet.cpp new file mode 100644 index 0000000..4c724be --- /dev/null +++ b/src/Bullet.cpp @@ -0,0 +1,50 @@ +#include "Bullet.h" +#include "Tank.h" +#include +#include + +#include "GlobalConstants.h" + +Bullet::Bullet(qreal x, qreal y, Direction direction, BulletOwner owner) + : m_direction(direction), m_owner(owner) { + setPos(x, y); + setZValue(2); + + QPixmap pixmap(static_cast(kBulletSize), static_cast(kBulletSize)); + if (owner == BulletOwner::Player) { + pixmap.fill(Qt::yellow); + } else { + pixmap.fill(QColor(255, 140, 0)); + } + setPixmap(pixmap); +} + +void Bullet::Move() { + const QPointF current_pos = pos(); + switch (m_direction) { + case Direction::Up: + setPos(current_pos.x(), current_pos.y() - kBulletSpeed); + break; + case Direction::Down: + setPos(current_pos.x(), current_pos.y() + kBulletSpeed); + break; + case Direction::Left: + setPos(current_pos.x() - kBulletSpeed, current_pos.y()); + break; + case Direction::Right: + setPos(current_pos.x() + kBulletSpeed, current_pos.y()); + break; + } +} + +Direction Bullet::GetDirection() const { + return m_direction; +} + +BulletOwner Bullet::GetOwner() const { + return m_owner; +} + +QRectF Bullet::GetBoundingRect() const { + return QRectF(pos().x(), pos().y(), kBulletSize, kBulletSize); +} diff --git a/src/EnemyTank.cpp b/src/EnemyTank.cpp new file mode 100644 index 0000000..4f66238 --- /dev/null +++ b/src/EnemyTank.cpp @@ -0,0 +1,131 @@ +#include "EnemyTank.h" + +#include +#include +#include + +#include "GlobalConstants.h" +#include "Bullet.h" + +#include +#include + +EnemyTank::EnemyTank(qreal x, qreal y) { + setPos(x, y); + setZValue(1); + + m_direction = Direction::Down; + UpdatePixmap(); + + moveCooldown = QRandomGenerator::global()->bounded(18, 48); + fireCooldown = QRandomGenerator::global()->bounded(10, 50); + hp = 1; +} + +void EnemyTank::Update() { + if (moveCooldown > 0) --moveCooldown; + if (fireCooldown > 0) --fireCooldown; +} + +QRectF EnemyTank::GetFutureRect(Direction direction) const { + qreal new_x = pos().x(); + qreal new_y = pos().y(); + + switch (direction) { + case Direction::Up: + new_y -= GetSpeed(); + break; + case Direction::Down: + new_y += GetSpeed(); + break; + case Direction::Left: + new_x -= GetSpeed(); + break; + case Direction::Right: + new_x += GetSpeed(); + break; + } + + qreal collisionW = GetWidth() - 2 * kCollisionInset; + qreal collisionH = GetHeight() - 2 * kCollisionInset; + return QRectF(new_x + kCollisionInset, new_y + kCollisionInset, collisionW, collisionH); +} + +void EnemyTank::UpdatePixmap() { + QPixmap pixmap(GetWidth(), GetHeight()); + pixmap.fill(Qt::transparent); + + QPainter painter(&pixmap); + painter.setRenderHint(QPainter::Antialiasing, false); + + painter.fillRect(kEnemyTankBodyX, kEnemyTankBodyY, kEnemyTankBodyWidth, kEnemyTankBodyHeight, + QColor(139, 69, 19)); + + painter.fillRect(kTankTrackLeftX, 10, kEnemyTankTrackWidth, kEnemyTankTrackHeight, + QColor(101, 67, 33)); + painter.fillRect(kTankTrackRightX, 10, kEnemyTankTrackWidth, kEnemyTankTrackHeight, + QColor(101, 67, 33)); + + painter.fillRect(kEnemyTankTurretX, kEnemyTankTurretY, kEnemyTankTurretSize, kEnemyTankTurretSize, + QColor(120, 40, 10)); + + painter.setPen(QPen(Qt::black, 2)); + const int center = kEnemyTankGunCenter; + DrawGun(painter, center, kEnemyTankGunLength); + + painter.end(); + setPixmap(pixmap); +} + +void EnemyTank::DrawGun(QPainter& painter, int center, int gunLength) const { + using Handler = std::function; + static const std::unordered_map handlers = { + {Direction::Up, [](QPainter& p, int c, int g) { p.drawLine(c, c, c, c - g); }}, + {Direction::Down, [](QPainter& p, int c, int g) { p.drawLine(c, c, c, c + g); }}, + {Direction::Left, [](QPainter& p, int c, int g) { p.drawLine(c, c, c - g, c); }}, + {Direction::Right, [](QPainter& p, int c, int g) { p.drawLine(c, c, c + g, c); }} + }; + + auto it = handlers.find(m_direction); + if (it != handlers.end()) { + it->second(painter, center, gunLength); + } +} + +void EnemyTank::SetDirection(Direction direction) { + if (m_direction == direction) { + return; + } + m_direction = direction; + UpdatePixmap(); +} + +void EnemyTank::TakeDamage(int dmg) { + hp -= dmg; +} + +std::vector> EnemyTank::Fire(Direction dir) { + std::vector> list; + QPointF pos = this->pos(); + qreal bulletX = pos.x() + GetWidth() / 2 - 3; + qreal bulletY = pos.y() + GetHeight() / 2 - 3; + + switch (dir) { + case Direction::Up: + bulletY = pos.y() - 3; + break; + case Direction::Down: + bulletY = pos.y() + GetHeight() - 3; + break; + case Direction::Left: + bulletX = pos.x() - 3; + break; + case Direction::Right: + bulletX = pos.x() + GetWidth() - 3; + break; + } + + list.push_back(std::make_unique(bulletX, bulletY, dir, BulletOwner::Enemy)); + return list; +} + diff --git a/src/GameModel.cpp b/src/GameModel.cpp new file mode 100644 index 0000000..ad493d3 --- /dev/null +++ b/src/GameModel.cpp @@ -0,0 +1,291 @@ +#include "GameModel.h" + +#include "Tank.h" +#include "Wall.h" +#include "BrickWall.h" +#include "Bullet.h" +#include "EnemyTank.h" +#include "Bonus.h" + +#include + +GameModel::~GameModel() = default; + + +void GameModel::SetPlayer(std::unique_ptr p) { + player = std::move(p); +} + +Tank* GameModel::GetPlayer() const { + return player.get(); +} + +void GameModel::ClearPlayer() { + player.reset(); +} + +Wall* GameModel::AddWall(std::unique_ptr w) { + walls.push_back(std::move(w)); + return walls.back().get(); +} + +std::vector GameModel::GetWalls() const { + std::vector out; + out.reserve(walls.size()); + for (const auto& p : walls) { + out.push_back(p.get()); + } + return out; +} + +void GameModel::ClearWalls() { + walls.clear(); +} + +size_t GameModel::GetWallsCount() const { + return walls.size(); +} + +Wall* GameModel::GetWallAt(size_t i) const { + return walls[i].get(); +} + +BrickWall* GameModel::AddBrick(std::unique_ptr b) { + bricks.push_back(std::move(b)); + return bricks.back().get(); +} + +std::vector GameModel::GetBricks() const { + std::vector out; + out.reserve(bricks.size()); + for (const auto& p : bricks) { + out.push_back(p.get()); + } + return out; +} + +void GameModel::RemoveBrick(BrickWall* b) { + auto it = std::find_if(bricks.begin(), bricks.end(), [b](const std::unique_ptr& p) { + return p.get() == b; + }); + if (it != bricks.end()) { + bricks.erase(it); + } +} + +void GameModel::ClearBricks() { + bricks.clear(); +} + +size_t GameModel::GetBricksCount() const { + return bricks.size(); +} + +BrickWall* GameModel::GetBrickAt(size_t i) const { + return bricks[i].get(); +} + +Bullet* GameModel::AddBullet(std::unique_ptr b) { + bullets.push_back(std::move(b)); + return bullets.back().get(); +} + +std::vector GameModel::GetBullets() const { + std::vector out; + out.reserve(bullets.size()); + for (const auto& p : bullets) { + out.push_back(p.get()); + } + return out; +} + +void GameModel::RemoveBullet(Bullet* b) { + auto it = std::find_if(bullets.begin(), bullets.end(), [b](const std::unique_ptr& p) { + return p.get() == b; + }); + if (it != bullets.end()) { + bullets.erase(it); + } +} + +void GameModel::ClearBullets() { + bullets.clear(); +} + +size_t GameModel::GetBulletsCount() const { + return bullets.size(); +} + +Bullet* GameModel::GetBulletAt(size_t i) const { + return bullets[i].get(); +} + +Bullet* GameModel::AddEnemyBullet(std::unique_ptr b) { + enemyBullets.push_back(std::move(b)); + return enemyBullets.back().get(); +} + +std::vector GameModel::GetEnemyBullets() const { + std::vector out; + out.reserve(enemyBullets.size()); + for (const auto& p : enemyBullets) { + out.push_back(p.get()); + } + return out; +} + +void GameModel::RemoveEnemyBullet(Bullet* b) { + auto it = std::find_if(enemyBullets.begin(), enemyBullets.end(), [b](const std::unique_ptr& p) { + return p.get() == b; + }); + if (it != enemyBullets.end()) { + enemyBullets.erase(it); + } +} + +void GameModel::ClearEnemyBullets() { + enemyBullets.clear(); +} + +size_t GameModel::GetEnemyBulletsCount() const { + return enemyBullets.size(); +} + +Bullet* GameModel::GetEnemyBulletAt(size_t i) const { + return enemyBullets[i].get(); +} + +EnemyTank* GameModel::AddEnemyTank(std::unique_ptr e) { + enemyTanks.push_back(std::move(e)); + return enemyTanks.back().get(); +} + +std::vector GameModel::GetEnemyTanks() const { + std::vector out; + out.reserve(enemyTanks.size()); + for (const auto& p : enemyTanks) { + out.push_back(p.get()); + } + return out; +} + +void GameModel::RemoveEnemyTank(EnemyTank* e) { + auto it = std::find_if(enemyTanks.begin(), enemyTanks.end(), [e](const std::unique_ptr& p) { + return p.get() == e; + }); + if (it != enemyTanks.end()) { + enemyTanks.erase(it); + } +} + +void GameModel::ClearEnemyTanks() { + enemyTanks.clear(); +} + +size_t GameModel::GetEnemyTanksCount() const { + return enemyTanks.size(); +} + +EnemyTank* GameModel::GetEnemyTankAt(size_t i) const { + return enemyTanks[i].get(); +} + +Bonus* GameModel::AddBonus(std::unique_ptr b) { + bonuses.push_back(std::move(b)); + return bonuses.back().get(); +} + +std::vector GameModel::GetBonuses() const { + std::vector out; + out.reserve(bonuses.size()); + for (const auto& p : bonuses) { + out.push_back(p.get()); + } + return out; +} + +void GameModel::RemoveBonus(Bonus* b) { + auto it = std::find_if(bonuses.begin(), bonuses.end(), [b](const std::unique_ptr& p) { + return p.get() == b; + }); + if (it != bonuses.end()) { + bonuses.erase(it); + } +} + +void GameModel::ClearBonuses() { + bonuses.clear(); +} + +size_t GameModel::GetBonusesCount() const { + return bonuses.size(); +} + +Bonus* GameModel::GetBonusAt(size_t i) const { + return bonuses[i].get(); +} + +GameModel::GameModel(QObject* parent) : QObject(parent) {} + +void GameModel::ModifyPlayerLives(int delta) { + if (m_gameOver) { + return; + } + + m_lives += delta; + + if (m_lives < 0) { + m_lives = 0; + } + + emit livesChanged(m_lives); + + if (m_lives == 0) { + m_gameOver = true; + emit gameOver(); + } +} + +void GameModel::ModifyScore(int delta) { + if (m_gameOver) { + return; + } + + SetKills(m_kills + delta); +} + +void GameModel::SetGameOver(bool v) { + m_gameOver = v; + + if (v) { + emit gameOver(); + } +} + +void GameModel::SetGameWon(bool v) { + m_gameWon = v; + + if (v) { + emit gameWon(); + } +} + +void GameModel::SetLives(int lives) { + m_lives = lives; + + if (m_lives < 0) { + m_lives = 0; + } + + emit livesChanged(m_lives); +} + +void GameModel::SetKills(int kills) { + m_kills = kills; + + if (m_kills < 0) { + m_kills = 0; + } + + emit scoreChanged(m_kills); +} diff --git a/src/GameScene.cpp b/src/GameScene.cpp new file mode 100644 index 0000000..a0ece17 --- /dev/null +++ b/src/GameScene.cpp @@ -0,0 +1,731 @@ +#include + +enum class EnemyType : int { + Light = 0, + Heavy = 1, + Twin = 2, + Kamikaze = 3 +}; +#include "GameScene.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "BrickWall.h" +#include "Bullet.h" +#include "EnemyTank.h" +#include "LightEnemy.h" +#include "HeavyEnemy.h" +#include "TwinShooterEnemy.h" +#include "Tank.h" +#include "KamikazeEnemy.h" +#include "TankView.h" +#include "Wall.h" +#include "Bonus.h" + +#include "GameModel.h" +#include "InputController.h" + +#include "GlobalConstants.h" + +static int s_playerFireCooldown = 0; + +GameScene::GameScene(QObject* parent) + : QGraphicsScene(parent), + m_canFire(true), + m_enemySpawnCooldown(kSpawnCooldown) { + setSceneRect(0, 0, kSceneWidth, kSceneHeight); + setBackgroundBrush(QColor(20, 40, 20)); + + m_model = new GameModel(this); + m_model->SetLives(kInitialPlayerLives); + m_model->SetKills(0); + m_model->SetPlayer(std::make_unique(kSceneWidth / 2, kSceneHeight / 2)); + m_playerView = new TankView(m_model->GetPlayer()); + addItem(m_playerView); + + m_livesText = new QGraphicsTextItem(); + m_livesText->setZValue(10); + addItem(m_livesText); + + m_scoreText = new QGraphicsTextItem(); + m_scoreText->setZValue(10); + addItem(m_scoreText); + + InitializeLevel(); + + m_gameTimer.setInterval(1000 / 60); + connect(&m_gameTimer, &QTimer::timeout, this, &GameScene::Update); + m_gameTimer.start(); + + connect(m_model, &GameModel::livesChanged, this, &GameScene::UpdateLivesDisplay); + connect(m_model, &GameModel::scoreChanged, this, &GameScene::UpdateScoreDisplay); + connect(m_model, &GameModel::gameOver, this, &GameScene::ShowGameOver); + connect(m_model, &GameModel::gameWon, this, &GameScene::ShowWinScreen); + UpdateLivesDisplay(); + UpdateScoreDisplay(); + + m_inputController = new InputController(m_model, this); + connect(m_inputController, &InputController::restartRequested, this, &GameScene::RestartGame); + connect(m_inputController, &InputController::fireRequested, this, [this]() { + if (m_canFire && !m_model->IsGameOver()) { + FireBullet(); + m_canFire = false; + s_playerFireCooldown = kPlayerFireCooldownFrames; + } + }); + + m_enemyFactory.Register("light", []() -> std::unique_ptr { return std::make_unique(0.0, 0.0); }); + m_enemyFactory.Register("heavy", []() -> std::unique_ptr { return std::make_unique(0.0, 0.0); }); + m_enemyFactory.Register("twin", []() -> std::unique_ptr { return std::make_unique(0.0, 0.0); }); + m_enemyFactory.Register("kamikaze", []() -> std::unique_ptr { return std::make_unique(0.0, 0.0); }); +} + +GameScene::~GameScene() { + if (m_model) { + for (auto* b : m_model->GetBullets()) if (b) removeItem(b); + for (auto* b : m_model->GetEnemyBullets()) if (b) removeItem(b); + for (auto* w : m_model->GetWalls()) if (w) removeItem(w); + for (auto* br : m_model->GetBricks()) if (br) removeItem(br); + if (m_playerView) removeItem(m_playerView); + + for (auto* e : m_model->GetEnemyTanks()) if (e) removeItem(e); + for (auto* bo : m_model->GetBonuses()) if (bo) removeItem(bo); + + m_model->ClearBullets(); + m_model->ClearEnemyBullets(); + m_model->ClearWalls(); + m_model->ClearBricks(); + m_model->ClearEnemyTanks(); + m_model->ClearBonuses(); + m_model->ClearPlayer(); + } + + auto cleanupOptional = [this](auto*& ptr) { + if (ptr) { + removeItem(ptr); + delete ptr; + ptr = nullptr; + } + }; + cleanupOptional(m_overlay); + cleanupOptional(m_gameOverBox); + cleanupOptional(m_gameOverText); + cleanupOptional(m_restartText); + cleanupOptional(m_winOverlay); + cleanupOptional(m_winBox); + cleanupOptional(m_winText); + cleanupOptional(m_winRestartText); + cleanupOptional(m_livesText); + cleanupOptional(m_scoreText); + if (m_playerView) { removeItem(m_playerView); delete m_playerView; m_playerView = nullptr; } + delete m_model; +} + +void GameScene::InitializeLevel() { + const char* levelMap[kMapRows] = { + "WWWWWWWWWWWWWWWWWWWW", + "W W", + "W BBBBBBBBBBBBBBBB W", + "W B W B W", + "W B W B W", + "W B B B W", + "W B WBBBBBBBW B W", + "W B W B W B W", + "W B W B W B W", + "W B W W B W", + "W BBBB BBBBB W", + "W B W B W B W", + "W B W B W B W", + "W B WBBBBBBBW B W", + "W B B B W", + "W B W B W", + "W B W B W", + "W B W B W", + "W BBBBBBBBBBBBBBBB W", + "WWWWWWWWWWWWWWWWWWWW" + }; + + for (int y = 0; y < kMapRows; ++y) { + for (int x = 0; x < kMapCols; ++x) { + char cell = levelMap[y][x]; + qreal posX = x * kTileSize; + qreal posY = y * kTileSize; + + if (cell == 'W') { + Wall* w = m_model->AddWall(std::make_unique(posX, posY)); + addItem(w); + } else if (cell == 'B') { + BrickWall* br = m_model->AddBrick(std::make_unique(posX, posY)); + addItem(br); + } else if (cell == ' ') { + m_freeSpawnPoints.append(QPointF(posX, posY)); + } + } + } + + const int kBonusesToSpawn = 4; + for (int i = 0; i < kBonusesToSpawn && !m_freeSpawnPoints.isEmpty(); ++i) { + int idx = QRandomGenerator::global()->bounded(m_freeSpawnPoints.size()); + QPointF spawn = m_freeSpawnPoints.takeAt(idx); + + int r = QRandomGenerator::global()->bounded(0, 3); + if (r == 0) { + Bonus* bonus = m_model->AddBonus(std::make_unique(spawn.x(), spawn.y())); + addItem(bonus); + } else if (r == 1) { + Bonus* bonus = m_model->AddBonus(std::make_unique(spawn.x(), spawn.y())); + addItem(bonus); + } else { + Bonus* bonus = m_model->AddBonus(std::make_unique(spawn.x(), spawn.y())); + addItem(bonus); + } + } +} + +void GameScene::Update() { + if (m_model->IsGameOver() || m_model->IsGameWon()) { + return; + } + + HandlePlayerInputAndMovement(); + SpawnEnemiesIfNeeded(); + UpdateEnemies(); + UpdatePlayerBullets(); + UpdateBonuses(); + UpdateEnemyBullets(); + + if (s_playerFireCooldown > 0) --s_playerFireCooldown; + m_canFire = (s_playerFireCooldown <= 0); +} + +void GameScene::HandlePlayerInputAndMovement() { + auto* player = m_model->GetPlayer(); + Direction desiredDirection = player->GetDirection(); + bool shouldMove = false; + + if (m_inputController && m_inputController->IsKeyPressed(Qt::Key_Left)) { + desiredDirection = Direction::Left; + shouldMove = true; + } else if (m_inputController && m_inputController->IsKeyPressed(Qt::Key_Right)) { + desiredDirection = Direction::Right; + shouldMove = true; + } else if (m_inputController && m_inputController->IsKeyPressed(Qt::Key_Up)) { + desiredDirection = Direction::Up; + shouldMove = true; + } else if (m_inputController && m_inputController->IsKeyPressed(Qt::Key_Down)) { + desiredDirection = Direction::Down; + shouldMove = true; + } + + player->SetDirection(desiredDirection); + + if (shouldMove) { + QRectF futureRect = player->GetFutureRect(desiredDirection); + if (!IsCollidingWithAnyWall(futureRect)) { + player->Move(); + if (m_playerView) m_playerView->UpdateView(); + } + } + else { + if (m_playerView) m_playerView->UpdateView(); + } + + player->Update(); +} + +void GameScene::SpawnEnemiesIfNeeded() { + if (m_enemySpawnCooldown > 0) { + --m_enemySpawnCooldown; + return; + } + + if (m_freeSpawnPoints.isEmpty() || m_model->GetEnemyTanksCount() >= kMaxEnemies) { + return; + } + + int idx = QRandomGenerator::global()->bounded(m_freeSpawnPoints.size()); + QPointF spawnPos = m_freeSpawnPoints[idx]; + EnemyType type = static_cast(QRandomGenerator::global()->bounded(0, 4)); + std::unique_ptr uptr; + switch (type) { + case EnemyType::Light: + uptr = m_enemyFactory.CreateObject("light"); + break; + case EnemyType::Heavy: + uptr = m_enemyFactory.CreateObject("heavy"); + break; + case EnemyType::Twin: + uptr = m_enemyFactory.CreateObject("twin"); + break; + case EnemyType::Kamikaze: + default: + uptr = m_enemyFactory.CreateObject("kamikaze"); + break; + } + + if (uptr) { + uptr->setPos(spawnPos); + EnemyTank* enemy = m_model->AddEnemyTank(std::move(uptr)); + addItem(enemy); + m_enemySpawnCooldown = kSpawnCooldown; + } +} + +void GameScene::UpdateEnemies() { + auto enemiesCopy = m_model->GetEnemyTanks(); + for (auto* enemy : enemiesCopy) { + enemy->Update(); + if (enemy->GetMoveCooldown() <= 0) { + int r = QRandomGenerator::global()->bounded(0, 6); + Direction newDir; + if (r < 3) { + newDir = Direction::Down; + } else if (r == 3) { + newDir = Direction::Left; + } else if (r == 4) { + newDir = Direction::Right; + } else { + newDir = Direction::Up; + } + enemy->SetDirection(newDir); + enemy->ResetMoveTimer(QRandomGenerator::global()->bounded(18, 48)); + } + + Direction dir = enemy->GetDirection(); + QRectF futureRect = enemy->GetFutureRect(dir); + if (!IsCollidingWithAnyWall(futureRect)) { + QPointF pos = enemy->pos(); + switch (dir) { + case Direction::Up: + enemy->setPos(pos.x(), pos.y() - enemy->GetSpeed()); + break; + case Direction::Down: + enemy->setPos(pos.x(), pos.y() + enemy->GetSpeed()); + break; + case Direction::Left: + enemy->setPos(pos.x() - enemy->GetSpeed(), pos.y()); + break; + case Direction::Right: + enemy->setPos(pos.x() + enemy->GetSpeed(), pos.y()); + break; + } + } else { + bool moved = false; + for (int t = 0; t < 4 && !moved; ++t) { + int r = QRandomGenerator::global()->bounded(0, 4); + Direction alt = static_cast(r); + QRectF altRect = enemy->GetFutureRect(alt); + if (!IsCollidingWithAnyWall(altRect)) { + enemy->SetDirection(alt); + QPointF pos = enemy->pos(); + switch (alt) { + case Direction::Up: + enemy->setPos(pos.x(), pos.y() - enemy->GetSpeed()); + break; + case Direction::Down: + enemy->setPos(pos.x(), pos.y() + enemy->GetSpeed()); + break; + case Direction::Left: + enemy->setPos(pos.x() - enemy->GetSpeed(), pos.y()); + break; + case Direction::Right: + enemy->setPos(pos.x() + enemy->GetSpeed(), pos.y()); + break; + } + moved = true; + } + } + } + + if (m_playerView) { + auto* k = dynamic_cast(enemy); + if (k) { + QRectF playerRect = m_playerView->boundingRect().translated(m_playerView->pos()); + QRectF enemyRect = enemy->boundingRect().translated(enemy->pos()); + if (playerRect.intersects(enemyRect)) { + removeItem(enemy); + m_model->RemoveEnemyTank(enemy); + m_model->ModifyPlayerLives(-1); + continue; + } + } + } + + if (enemy->GetFireCooldown() <= 0) { + int dirIdx = QRandomGenerator::global()->bounded(0, 4); + Direction fireDir = static_cast(dirIdx); + + auto bullets = enemy->Fire(fireDir); + for (auto& b : bullets) { + addItem(b.get()); + m_model->AddEnemyBullet(std::move(b)); + } + enemy->ResetFireTimer(QRandomGenerator::global()->bounded(120, 300)); + } + } +} + +void GameScene::UpdatePlayerBullets() { + if (!m_model) return; + for (int i = (int)m_model->GetBulletsCount() - 1; i >= 0; --i) { + Bullet* bullet = m_model->GetBulletAt((size_t)i); + bullet->Move(); + QRectF bulletRect = bullet->GetBoundingRect(); + + if (!sceneRect().intersects(bulletRect)) { + removeItem(bullet); + m_model->RemoveBullet(bullet); + continue; + } + + bool hitEnemy = false; + for (int j = (int)m_model->GetEnemyTanksCount() - 1; j >= 0; --j) { + auto* enemy = m_model->GetEnemyTankAt((size_t)j); + QRectF enemyRect = enemy->boundingRect().translated(enemy->pos()); + if (bulletRect.intersects(enemyRect)) { + enemy->TakeDamage(1); + removeItem(bullet); + m_model->RemoveBullet(bullet); + + if (enemy->IsDead()) { + removeItem(enemy); + m_model->RemoveEnemyTank(enemy); + + m_model->ModifyScore(1); + if (m_model->GetKills() >= kWinConditionKills) { + m_model->SetGameWon(true); + } + } + + hitEnemy = true; + break; + } + } + if (hitEnemy) { + continue; + } + + if (IsCollidingWithSolidWall(bulletRect)) { + removeItem(bullet); + m_model->RemoveBullet(bullet); + continue; + } + + if (auto* brick = FindCollidingBrickWall(bulletRect)) { + removeItem(brick); + m_model->RemoveBrick(brick); + removeItem(bullet); + m_model->RemoveBullet(bullet); + continue; + } + } +} + +void GameScene::UpdateEnemyBullets() { + if (!m_model) return; + for (int i = (int)m_model->GetEnemyBulletsCount() - 1; i >= 0; --i) { + Bullet* bullet = m_model->GetEnemyBulletAt((size_t)i); + bullet->Move(); + QRectF bulletRect = bullet->GetBoundingRect(); + + if (!sceneRect().intersects(bulletRect)) { + removeItem(bullet); + m_model->RemoveEnemyBullet(bullet); + continue; + } + + if (IsCollidingWithSolidWall(bulletRect)) { + removeItem(bullet); + m_model->RemoveEnemyBullet(bullet); + continue; + } + + if (auto* brick = FindCollidingBrickWall(bulletRect)) { + removeItem(brick); + m_model->RemoveBrick(brick); + removeItem(bullet); + m_model->RemoveEnemyBullet(bullet); + continue; + } + + if (!m_model->IsGameOver()) { + if (m_playerView) { + QRectF playerRect = m_playerView->boundingRect().translated(m_playerView->pos()); + if (bulletRect.intersects(playerRect)) { + removeItem(bullet); + m_model->RemoveEnemyBullet(bullet); + m_model->ModifyPlayerLives(-1); + continue; + } + } + } + } +} + +void GameScene::UpdateBonuses() { + if (!m_model || !m_model->GetPlayer()) return; + + if (!m_playerView) return; + QRectF playerRect = m_playerView->boundingRect().translated(m_playerView->pos()); + + for (int i = (int)m_model->GetBonusesCount() - 1; i >= 0; --i) { + auto* bonus = m_model->GetBonusAt((size_t)i); + if (!bonus) continue; + + QRectF bonusRect = bonus->boundingRect().translated(bonus->pos()); + if (playerRect.intersects(bonusRect)) { + bonus->Apply(this); + removeItem(bonus); + m_model->RemoveBonus(bonus); + } + } +} + +void GameScene::ModifyPlayerLives(int delta) { + if (m_model) m_model->ModifyPlayerLives(delta); +} + +void GameScene::ModifyScore(int delta) { + if (m_model) m_model->ModifyScore(delta); +} + +void GameScene::keyPressEvent(QKeyEvent* event) { + if (m_inputController) m_inputController->HandleKeyPress(event); + QGraphicsScene::keyPressEvent(event); +} + +void GameScene::keyReleaseEvent(QKeyEvent* event) { + if (m_inputController) m_inputController->HandleKeyRelease(event); + QGraphicsScene::keyReleaseEvent(event); +} + +bool GameScene::IsCollidingWithSolidWall(const QRectF& rect) const { + if (!m_model) return false; + for (size_t i = 0; i < m_model->GetWallsCount(); ++i) { + const auto* wall = m_model->GetWallAt(i); + if (wall->boundingRect().translated(wall->pos()).intersects(rect)) { + return true; + } + } + return false; +} + +BrickWall* GameScene::FindCollidingBrickWall(const QRectF& rect) const { + if (!m_model) return nullptr; + for (size_t i = 0; i < m_model->GetBricksCount(); ++i) { + BrickWall* brick = m_model->GetBrickAt(i); + if (brick->boundingRect().translated(brick->pos()).intersects(rect)) { + return brick; + } + } + return nullptr; +} + +bool GameScene::IsCollidingWithAnyWall(const QRectF& rect) const { + return IsCollidingWithSolidWall(rect) || (FindCollidingBrickWall(rect) != nullptr); +} + +void GameScene::FireBullet() { + auto* player = m_model->GetPlayer(); + QPointF tankPos = player->GetPosition(); + Direction direction = player->GetDirection(); + + constexpr qreal kTankHalf = 16.0; + constexpr qreal kBulletHalf = 3.0; + + qreal offsetX = 0.0; + qreal offsetY = 0.0; + + switch (direction) { + case Direction::Up: + offsetX = kTankHalf - kBulletHalf; + offsetY = -kBulletHalf; + break; + case Direction::Down: + offsetX = kTankHalf - kBulletHalf; + offsetY = 32.0; + break; + case Direction::Left: + offsetX = -kBulletHalf; + offsetY = kTankHalf - kBulletHalf; + break; + case Direction::Right: + offsetX = 32.0; + offsetY = kTankHalf - kBulletHalf; + break; + } + + auto bullet = std::make_unique(tankPos.x() + offsetX, tankPos.y() + offsetY, + direction, BulletOwner::Player); + addItem(bullet.get()); + m_model->AddBullet(std::move(bullet)); +} + +void GameScene::ShowGameOver() { + m_overlay = new QGraphicsRectItem(0, 0, kSceneWidth, kSceneHeight); + m_overlay->setBrush(QColor(0, 0, 0, 180)); + m_overlay->setPen(Qt::NoPen); + m_overlay->setZValue(999); + addItem(m_overlay); + + m_gameOverBox = new QGraphicsRectItem(0, 0, 300, 160); + m_gameOverBox->setPos(170, 220); + m_gameOverBox->setBrush(QColor(30, 30, 40)); + m_gameOverBox->setPen(QPen(Qt::red, 3)); + m_gameOverBox->setZValue(1000); + addItem(m_gameOverBox); + + m_gameOverText = new QGraphicsTextItem("GAME OVER"); + m_gameOverText->setFont(QFont("Arial", 28, QFont::Bold)); + m_gameOverText->setDefaultTextColor(Qt::red); + m_gameOverText->setPos(210, 230); + m_gameOverText->setZValue(1001); + addItem(m_gameOverText); + + m_restartText = new QGraphicsTextItem("Enter - Restart"); + m_restartText->setFont(QFont("Arial", 16)); + m_restartText->setDefaultTextColor(Qt::white); + m_restartText->setPos(240, 280); + m_restartText->setZValue(1001); + addItem(m_restartText); + + m_gameTimer.stop(); + for (auto* v : views()) if (v) v->setFocus(); +} + +void GameScene::ShowWinScreen() { + m_winOverlay = new QGraphicsRectItem(0, 0, kSceneWidth, kSceneHeight); + m_winOverlay->setBrush(QColor(0, 0, 0, 180)); + m_winOverlay->setPen(Qt::NoPen); + m_winOverlay->setZValue(999); + addItem(m_winOverlay); + + m_winBox = new QGraphicsRectItem(0, 0, 300, 160); + m_winBox->setPos(170, 220); + m_winBox->setBrush(QColor(20, 30, 20)); + m_winBox->setPen(QPen(Qt::green, 3)); + m_winBox->setZValue(1000); + addItem(m_winBox); + + m_winText = new QGraphicsTextItem("YOU WIN!"); + m_winText->setFont(QFont("Arial", 28, QFont::Bold)); + m_winText->setDefaultTextColor(Qt::green); + m_winText->setPos(230, 230); + m_winText->setZValue(1001); + addItem(m_winText); + + m_winRestartText = new QGraphicsTextItem("Enter - Restart"); + m_winRestartText->setFont(QFont("Arial", 16)); + m_winRestartText->setDefaultTextColor(Qt::white); + m_winRestartText->setPos(240, 280); + m_winRestartText->setZValue(1001); + addItem(m_winRestartText); + + m_gameTimer.stop(); + for (auto* v : views()) if (v) v->setFocus(); +} + +void GameScene::SetLivesTextStyle() { + m_livesText->setDefaultTextColor(Qt::white); + m_livesText->setFont(QFont("Arial", 16, QFont::Bold)); + m_livesText->setPos(5, 5); +} + +void GameScene::UpdateLivesDisplay() { + int lives = m_model ? m_model->GetLives() : 0; + m_livesText->setPlainText(QString("Lives: %1").arg(lives)); + SetLivesTextStyle(); +} + +void GameScene::UpdateScoreDisplay() { + int kills = m_model ? m_model->GetKills() : 0; + m_scoreText->setPlainText(QString("Kills: %1").arg(kills)); + m_scoreText->setDefaultTextColor(Qt::yellow); + m_scoreText->setFont(QFont("Arial", 16, QFont::Bold)); + m_scoreText->setPos(513, 5); +} + +void GameScene::RestartGame() { + auto cleanupUnique = [this](auto& container) { + for (auto& uptr : container) { + if (uptr) removeItem(uptr.get()); + } + container.clear(); + }; + + if (m_model) { + for (auto* b : m_model->GetBullets()) if (b) removeItem(b); + for (auto* b : m_model->GetEnemyBullets()) if (b) removeItem(b); + for (auto* w : m_model->GetWalls()) if (w) removeItem(w); + for (auto* br : m_model->GetBricks()) if (br) removeItem(br); + if (m_playerView) { removeItem(m_playerView); delete m_playerView; m_playerView = nullptr; } + for (auto* e : m_model->GetEnemyTanks()) if (e) removeItem(e); + for (auto* bo : m_model->GetBonuses()) if (bo) removeItem(bo); + + m_model->ClearBullets(); + m_model->ClearEnemyBullets(); + m_model->ClearWalls(); + m_model->ClearBricks(); + m_model->ClearEnemyTanks(); + m_model->ClearBonuses(); + m_model->ClearPlayer(); + } + + + auto cleanupOptional = [this](auto& ptr) { + if (ptr) { + removeItem(ptr); + delete ptr; + ptr = nullptr; + } + }; + + cleanupOptional(m_overlay); + cleanupOptional(m_gameOverBox); + cleanupOptional(m_gameOverText); + cleanupOptional(m_restartText); + cleanupOptional(m_winOverlay); + cleanupOptional(m_winBox); + cleanupOptional(m_winText); + cleanupOptional(m_winRestartText); + cleanupOptional(m_livesText); + cleanupOptional(m_scoreText); + + m_freeSpawnPoints.clear(); + m_canFire = true; + m_enemySpawnCooldown = kSpawnCooldown; + + m_model->SetPlayer(std::make_unique(kSceneWidth / 2, kSceneHeight / 2)); + m_playerView = new TankView(m_model->GetPlayer()); + addItem(m_playerView); + + InitializeLevel(); + + m_livesText = new QGraphicsTextItem(); + m_livesText->setZValue(10); + addItem(m_livesText); + + m_scoreText = new QGraphicsTextItem(); + m_scoreText->setZValue(10); + addItem(m_scoreText); + + if (m_model) { + m_model->SetLives(kInitialPlayerLives); + m_model->SetKills(0); + m_model->SetGameOver(false); + m_model->SetGameWon(false); + } + + UpdateLivesDisplay(); + UpdateScoreDisplay(); + + m_gameTimer.start(); + for (auto* v : views()) if (v) v->setFocus(); +} diff --git a/src/HeavyEnemy.cpp b/src/HeavyEnemy.cpp new file mode 100644 index 0000000..d897bc5 --- /dev/null +++ b/src/HeavyEnemy.cpp @@ -0,0 +1,36 @@ +#include "HeavyEnemy.h" + +#include +#include + +#include "GlobalConstants.h" + +HeavyEnemy::HeavyEnemy(qreal x, qreal y) : EnemyTank(x, y) { + hp = 2; + UpdatePixmap(); +} + +void HeavyEnemy::UpdatePixmap() { + QPixmap pixmap(GetWidth(), GetHeight()); + pixmap.fill(Qt::transparent); + QPainter painter(&pixmap); + painter.setRenderHint(QPainter::Antialiasing, false); + + painter.fillRect(kEnemyTankBodyX, kEnemyTankBodyY, kEnemyTankBodyWidth, kEnemyTankBodyHeight, + QColor(139, 0, 0)); + + painter.fillRect(kTankTrackLeftX, 10, kEnemyTankTrackWidth, kEnemyTankTrackHeight, + QColor(101, 67, 33)); + painter.fillRect(kTankTrackRightX, 10, kEnemyTankTrackWidth, kEnemyTankTrackHeight, + QColor(101, 67, 33)); + + painter.fillRect(kEnemyTankTurretX, kEnemyTankTurretY, kEnemyTankTurretSize, kEnemyTankTurretSize, + QColor(128, 0, 0)); + + painter.setPen(QPen(Qt::black, 2)); + const int center = kEnemyTankGunCenter; + DrawGun(painter, center, kEnemyTankGunLength); + + painter.end(); + setPixmap(pixmap); +} diff --git a/src/InputController.cpp b/src/InputController.cpp new file mode 100644 index 0000000..1de410b --- /dev/null +++ b/src/InputController.cpp @@ -0,0 +1,30 @@ +#include "InputController.h" +#include "GameModel.h" + +InputController::InputController(GameModel* model, QObject* parent) + : QObject(parent), m_model(model) {} + +void InputController::HandleKeyPress(QKeyEvent* event) { + if (event->isAutoRepeat()) return; + + if ((m_model && (m_model->IsGameOver() || m_model->IsGameWon())) && + (event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return)) { + emit restartRequested(); + return; + } + + m_pressedKeys.insert(static_cast(event->key())); + + if (event->key() == Qt::Key_Space && m_model && !m_model->IsGameOver()) { + emit fireRequested(); + } +} + +void InputController::HandleKeyRelease(QKeyEvent* event) { + if (event->isAutoRepeat()) return; + m_pressedKeys.remove(static_cast(event->key())); +} + +bool InputController::IsKeyPressed(Qt::Key key) const { + return m_pressedKeys.contains(key); +} diff --git a/src/KamikazeEnemy.cpp b/src/KamikazeEnemy.cpp new file mode 100644 index 0000000..b33d6b3 --- /dev/null +++ b/src/KamikazeEnemy.cpp @@ -0,0 +1,18 @@ +#include "KamikazeEnemy.h" + +#include +#include "GlobalConstants.h" +#include "Bullet.h" + +KamikazeEnemy::KamikazeEnemy(qreal x, qreal y) : EnemyTank(x, y) { + UpdatePixmap(); + hp = 1; +} + +void KamikazeEnemy::Update() { + EnemyTank::Update(); +} + +std::vector> KamikazeEnemy::Fire(Direction dir) { + return {}; +} diff --git a/src/LightEnemy.cpp b/src/LightEnemy.cpp new file mode 100644 index 0000000..78ba13c --- /dev/null +++ b/src/LightEnemy.cpp @@ -0,0 +1,34 @@ +#include "LightEnemy.h" + +#include +#include + +#include "GlobalConstants.h" + +LightEnemy::LightEnemy(qreal x, qreal y) : EnemyTank(x, y) { + hp = 1; + UpdatePixmap(); +} + +void LightEnemy::UpdatePixmap() { + QPixmap pixmap(GetWidth(), GetHeight()); + pixmap.fill(Qt::transparent); + QPainter painter(&pixmap); + painter.setRenderHint(QPainter::Antialiasing, false); + + painter.fillRect(kEnemyTankBodyX, kEnemyTankBodyY, kEnemyTankBodyWidth, kEnemyTankBodyHeight, + QColor(30, 144, 255)); + + painter.fillRect(kTankTrackLeftX, 10, kEnemyTankTrackWidth, kEnemyTankTrackHeight, QColor(10, 50, 120)); + painter.fillRect(kTankTrackRightX, 10, kEnemyTankTrackWidth, kEnemyTankTrackHeight, QColor(10, 50, 120)); + + painter.fillRect(kEnemyTankTurretX, kEnemyTankTurretY, kEnemyTankTurretSize, kEnemyTankTurretSize, + QColor(0, 100, 200)); + + painter.setPen(QPen(Qt::black, 2)); + const int center = kEnemyTankGunCenter; + DrawGun(painter, center, kEnemyTankGunLength); + + painter.end(); + setPixmap(pixmap); +} diff --git a/src/Tank.cpp b/src/Tank.cpp new file mode 100644 index 0000000..fb5f4fb --- /dev/null +++ b/src/Tank.cpp @@ -0,0 +1,66 @@ +#include "Tank.h" + +#include "GlobalConstants.h" + +#include + +Tank::Tank(qreal x, qreal y) + : m_direction(Direction::Up), m_model(std::make_unique()) { + m_pos = QPointF(x, y); +} + +void Tank::SetDirection(Direction direction) { + if (m_direction == direction) return; + m_direction = direction; +} + +Direction Tank::GetDirection() const { + return m_direction; +} + +void Tank::Move() { + QPointF current_pos = m_pos; + qreal speed = m_model ? m_model->GetSpeed() : kSpeed; + switch (m_direction) { + case Direction::Up: + m_pos.setY(current_pos.y() - speed); + break; + case Direction::Down: + m_pos.setY(current_pos.y() + speed); + break; + case Direction::Left: + m_pos.setX(current_pos.x() - speed); + break; + case Direction::Right: + m_pos.setX(current_pos.x() + speed); + break; + } +} + +QRectF Tank::GetFutureRect(Direction dir) const { + qreal new_x = m_pos.x(); + qreal new_y = m_pos.y(); + + qreal speed = m_model ? m_model->GetSpeed() : kSpeed; + switch (dir) { + case Direction::Up: + new_y -= speed; + break; + case Direction::Down: + new_y += speed; + break; + case Direction::Left: + new_x -= speed; + break; + case Direction::Right: + new_x += speed; + break; + } + + const qreal collisionSize = static_cast(kTankSize - 2 * kCollisionInset); + return QRectF(new_x + kCollisionInset, new_y + kCollisionInset, collisionSize, collisionSize); +} + +void Tank::Update() { +} + diff --git a/src/TankView.cpp b/src/TankView.cpp new file mode 100644 index 0000000..5554fc1 --- /dev/null +++ b/src/TankView.cpp @@ -0,0 +1,57 @@ +#include "TankView.h" + +#include "GlobalConstants.h" + +#include +#include + +TankView::TankView(Tank* model) : m_model(model) { + setZValue(1); + UpdateView(); +} + +void TankView::UpdateView() { + if (!m_model) return; + setPos(m_model->GetPosition()); + UpdatePixmap(); +} + +void TankView::DrawGun(QPainter& painter, int center, int gunLength) const { + using Handler = std::function; + static const std::unordered_map handlers = { + {Direction::Up, [](QPainter& p, int c, int g) { p.drawLine(c, c, c, c - g); }}, + {Direction::Down, [](QPainter& p, int c, int g) { p.drawLine(c, c, c, c + g); }}, + {Direction::Left, [](QPainter& p, int c, int g) { p.drawLine(c, c, c - g, c); }}, + {Direction::Right, [](QPainter& p, int c, int g) { p.drawLine(c, c, c + g, c); }} + }; + + if (!m_model) return; + auto it = handlers.find(m_model->GetDirection()); + if (it != handlers.end()) { + it->second(const_cast(painter), center, gunLength); + } +} + +void TankView::UpdatePixmap() { + QPixmap pixmap(kTankSize, kTankSize); + pixmap.fill(Qt::transparent); + + QPainter painter(&pixmap); + painter.setRenderHint(QPainter::Antialiasing, false); + + painter.fillRect(kTankBodyX, kTankBodyY, kTankBodyWidth, kTankBodyHeight, + kTankBodyColor); + + painter.fillRect(kTankTrackLeftX, 10, kTankTrackWidth, kTankTrackHeight, kTankTrackColor); + painter.fillRect(kTankTrackRightX, 10, kTankTrackWidth, kTankTrackHeight, kTankTrackColor); + + painter.fillRect(kTankTurretX, kTankTurretY, kTankTurretSize, kTankTurretSize, + kTankTurretColor); + + painter.setPen(QPen(Qt::black, 2)); + const int center = kTankGunCenter; + DrawGun(painter, center, kTankGunLength); + + painter.end(); + setPixmap(pixmap); +} diff --git a/src/TwinShooterEnemy.cpp b/src/TwinShooterEnemy.cpp new file mode 100644 index 0000000..c5ede88 --- /dev/null +++ b/src/TwinShooterEnemy.cpp @@ -0,0 +1,62 @@ +#include "TwinShooterEnemy.h" + +#include +#include + +#include "GlobalConstants.h" +#include "Bullet.h" + +TwinShooterEnemy::TwinShooterEnemy(qreal x, qreal y) : EnemyTank(x, y) { + hp = 3; + UpdatePixmap(); +} + +void TwinShooterEnemy::UpdatePixmap() { + QPixmap pixmap(GetWidth(), GetHeight()); + pixmap.fill(Qt::transparent); + QPainter painter(&pixmap); + painter.setRenderHint(QPainter::Antialiasing, false); + + painter.fillRect(kEnemyTankBodyX, kEnemyTankBodyY, kEnemyTankBodyWidth, kEnemyTankBodyHeight, + QColor(255, 165, 0)); + + painter.fillRect(kTankTrackLeftX, 10, kEnemyTankTrackWidth, kEnemyTankTrackHeight, + QColor(140, 90, 40)); + painter.fillRect(kTankTrackRightX, 10, kEnemyTankTrackWidth, kEnemyTankTrackHeight, + QColor(140, 90, 40)); + + painter.fillRect(kEnemyTankTurretX, kEnemyTankTurretY, kEnemyTankTurretSize, kEnemyTankTurretSize, + QColor(200, 100, 0)); + + painter.setPen(QPen(Qt::black, 2)); + const int center = kEnemyTankGunCenter; + DrawGun(painter, center, kEnemyTankGunLength); + + painter.end(); + setPixmap(pixmap); +} + +std::vector> TwinShooterEnemy::Fire(Direction dir) { + std::vector> bullets; + QPointF pos = this->pos(); + + // Если танк едет вверх или вниз рассчитываются две позиции для выстрелов + // bx1 и bx2 это смещенные относительно центра танка координаты X для двух пуль + // by координата Y если вверх, то чуть выше танка, если вниз то чуть ниже + // создаются две пули, летящие параллельно друг другу + if (dir == Direction::Up || dir == Direction::Down) { + qreal bx1 = pos.x() + GetWidth() / 2 - 8; + qreal bx2 = pos.x() + GetWidth() / 2 + 8; + qreal by = (dir == Direction::Up) ? pos.y() - 3 : pos.y() + GetHeight() - 3; + bullets.push_back(std::make_unique(bx1, by, dir, BulletOwner::Enemy)); + bullets.push_back(std::make_unique(bx2, by, dir, BulletOwner::Enemy)); + } else { + qreal by1 = pos.y() + GetHeight() / 2 - 8; + qreal by2 = pos.y() + GetHeight() / 2 + 8; + qreal bx = (dir == Direction::Left) ? pos.x() - 3 : pos.x() + GetWidth() - 3; + bullets.push_back(std::make_unique(bx, by1, dir, BulletOwner::Enemy)); + bullets.push_back(std::make_unique(bx, by2, dir, BulletOwner::Enemy)); + } + + return bullets; +} diff --git a/src/Wall.cpp b/src/Wall.cpp new file mode 100644 index 0000000..99c7935 --- /dev/null +++ b/src/Wall.cpp @@ -0,0 +1,49 @@ +#include "Wall.h" + +#include +#include + +#include "GlobalConstants.h" + +Wall::Wall(qreal x, qreal y) { + setPos(x, y); + setZValue(0); + + QPixmap pixmap(kWallSize, kWallSize); + pixmap.fill(Qt::transparent); + + QPainter painter(&pixmap); + painter.setRenderHint(QPainter::Antialiasing, false); + + painter.fillRect(0, 0, kWallSize, kWallSize, QColor(60, 60, 60)); + + const QColor panelColor(90, 90, 90); + painter.fillRect(2, 2, kPanelSize, kPanelSize, panelColor); + painter.fillRect(18, 2, kPanelSize, kPanelSize, panelColor); + painter.fillRect(2, 18, kPanelSize, kPanelSize, panelColor); + painter.fillRect(18, 18, kPanelSize, kPanelSize, panelColor); + + InitRivets(painter); + + painter.setPen(QPen(QColor(30, 30, 30), 2)); + painter.drawRect(0, 0, kWallSize - 1, kWallSize - 1); + + painter.end(); + setPixmap(pixmap); +} + +void Wall::InitRivets(QPainter& painter) { + painter.setPen(Qt::NoPen); + painter.setBrush(Qt::black); + constexpr std::array,16> rivets = {{ + {4, 4}, {12, 4}, {4, 12}, {12, 12}, + {20, 4}, {28, 4}, {20, 12}, {28, 12}, + {4, 20}, {12, 20}, {4, 28}, {12, 28}, + {20, 20},{28, 20},{20, 28}, {28, 28} + }}; + + for (const auto& r : rivets) { + painter.drawEllipse(r.first - kRivetRadius, r.second - kRivetRadius, + 2 * kRivetRadius, 2 * kRivetRadius); + } +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..e15384f --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,19 @@ +#include +#include +#include "GameScene.h" +#include "GlobalConstants.h" + +int main(int argc, char* argv[]) { + QApplication app(argc, argv); + + GameScene* scene = new GameScene(); + QGraphicsView* view = new QGraphicsView(scene); + + view->setFixedSize(kSceneWidth, kSceneHeight); + view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + view->setWindowTitle("ТанкиNSU"); + view->show(); + + return app.exec(); +}