From b5e691108fdcbc4bbeef1e2c4d70b958757b1efd Mon Sep 17 00:00:00 2001 From: Sam-Si <13261099+Sam-Si@users.noreply.github.com> Date: Sun, 8 Mar 2026 01:40:49 +0530 Subject: [PATCH] Implement component-based architecture for Snake game: add AI, Buff, Health, Render, and Combat components --- src/ai.cpp | 38 +- src/ai.h | 11 + src/component.cpp | 197 +++++++ src/component.h | 204 +++++++ src/game.cpp | 1368 ++++++++++++++++++++++++++++++++++++--------- src/game.h | 260 ++++++++- src/player.cpp | 42 +- src/player.h | 40 +- src/render.cpp | 34 +- src/sprite.cpp | 98 ++-- src/sprite.h | 56 +- src/ui.cpp | 501 ++++++++++------- src/ui.h | 13 +- src/weapon.cpp | 200 ++++++- src/weapon.h | 41 ++ 15 files changed, 2532 insertions(+), 571 deletions(-) create mode 100644 src/component.cpp create mode 100644 src/component.h diff --git a/src/ai.cpp b/src/ai.cpp index be9ef52..ce41b22 100644 --- a/src/ai.cpp +++ b/src/ai.cpp @@ -1,5 +1,6 @@ #include "ai.h" +#include "game.h" #include "helper.h" #include "map.h" #include "res.h" @@ -13,13 +14,10 @@ extern std::array, MAP_SIZE> itemMap; extern bool hasMap[MAP_SIZE][MAP_SIZE]; extern std::array, MAP_SIZE> hasEnemy; extern int spikeDamage; -extern int playersCount; extern const int n, m; extern const int SCALE_FACTOR; -// Sprite -extern std::shared_ptr spriteSnake[SPRITES_MAX_NUM]; -extern int spritesCount; +// Use GameContext for entity access double AI_LOCK_LIMIT; int trapVerdict(const std::shared_ptr& sprite) { @@ -47,14 +45,18 @@ int trapVerdict(const std::shared_ptr& sprite) { } int getPowerfulPlayer() { + GameContext& ctx = getGameContext(); int maxNum = 0; int mxCount = 0; int id = -1; - for (int i = 0; i < playersCount; i++) { - if (!spriteSnake[i]) { + const int playerCount = ctx.entityManager.playerCount(); + const int snakeCount = ctx.entityManager.snakeCount(); + for (int i = 0; i < playerCount; i++) { + const auto& snake = ctx.entityManager.getSnake(i); + if (!snake) { continue; } - const int num = spriteSnake[i]->num(); + const int num = snake->num(); if (num > maxNum) { maxNum = num; mxCount = 1; @@ -63,17 +65,20 @@ int getPowerfulPlayer() { mxCount++; } } - if (id == -1 || mxCount != 1 || !spriteSnake[id]) { + if (id == -1 || mxCount != 1) { return -1; } - return spriteSnake[id]->num() >= AI_LOCK_LIMIT ? id : -1; + const auto& snake = ctx.entityManager.getSnake(id); + return (snake && snake->num() >= AI_LOCK_LIMIT) ? id : -1; } int balanceVerdict(const std::shared_ptr& sprite, int id) { - if (id == -1 || !spriteSnake[id] || spriteSnake[id]->sprites().empty()) { + GameContext& ctx = getGameContext(); + const auto& playerSnake = ctx.entityManager.getSnake(id); + if (id == -1 || !playerSnake || playerSnake->sprites().empty()) { return 0; } - const auto player = spriteSnake[id]->sprites().front(); + const auto player = playerSnake->sprites().front(); if (!player) { return 0; } @@ -127,7 +132,8 @@ int compareChoiceByValue(const void* x, const void* y) { return b->value - a->value; } -void AiInput(const std::shared_ptr& snake) { +void DefaultAIBehavior::updateInput( + const std::shared_ptr& snake) const { if (!snake || snake->sprites().empty()) { return; } @@ -175,3 +181,11 @@ void AiInput(const std::shared_ptr& snake) { } } } + +void AiInput(const std::shared_ptr& snake) { + GameContext& ctx = getGameContext(); + if (!ctx.aiBehavior) { + ctx.aiBehavior = std::make_shared(); + } + ctx.aiBehavior->updateInput(snake); +} diff --git a/src/ai.h b/src/ai.h index f8c7fa2..b920a56 100644 --- a/src/ai.h +++ b/src/ai.h @@ -15,6 +15,17 @@ struct Choice { Direction direction = Direction::Right; }; +class AIBehavior { + public: + virtual ~AIBehavior() = default; + virtual void updateInput(const std::shared_ptr& snake) const = 0; +}; + +class DefaultAIBehavior final : public AIBehavior { + public: + void updateInput(const std::shared_ptr& snake) const override; +}; + void AiInput(const std::shared_ptr& snake); int getPowerfulPlayer(); diff --git a/src/component.cpp b/src/component.cpp new file mode 100644 index 0000000..8375b78 --- /dev/null +++ b/src/component.cpp @@ -0,0 +1,197 @@ +#include "component.h" + +#include "sprite.h" + +// ============================================================================ +// TransformComponent Implementation +// ============================================================================ + +TransformComponent::TransformComponent(int x, int y, Direction direction) + : x_(x), y_(y), direction_(direction), face_(direction) {} + +int TransformComponent::x() const noexcept { return x_; } +int TransformComponent::y() const noexcept { return y_; } +Direction TransformComponent::face() const noexcept { return face_; } +Direction TransformComponent::direction() const noexcept { return direction_; } + +void TransformComponent::setPosition(int x, int y) noexcept { + x_ = x; + y_ = y; +} + +void TransformComponent::setX(int x) noexcept { x_ = x; } +void TransformComponent::setY(int y) noexcept { y_ = y; } + +void TransformComponent::setFace(Direction face) noexcept { face_ = face; } + +void TransformComponent::setDirection(Direction direction) noexcept { + direction_ = direction; +} + +void TransformComponent::enqueueDirectionChange( + Direction newDirection, PositionBuffer& buffer) noexcept { + if (direction_ == newDirection) { + return; + } + direction_ = newDirection; + if (newDirection == Direction::Left || newDirection == Direction::Right) { + face_ = newDirection; + } + // Buffer position change for chained sprites + PositionBufferSlot slot{x_, y_, direction_}; + buffer.push(slot); +} + +// ============================================================================ +// HealthComponent Implementation +// ============================================================================ + +HealthComponent::HealthComponent(int hp, int totalHp) + : hp_(hp), totalHp_(totalHp) {} + +int HealthComponent::hp() const noexcept { return hp_; } +int HealthComponent::totalHp() const noexcept { return totalHp_; } +bool HealthComponent::isDead() const noexcept { return hp_ <= 0; } +bool HealthComponent::isAlive() const noexcept { return hp_ > 0; } + +void HealthComponent::setHp(int hp) noexcept { hp_ = hp; } +void HealthComponent::setTotalHp(int totalHp) noexcept { totalHp_ = totalHp; } + +void HealthComponent::takeDamage(int damage) noexcept { + hp_ -= damage; + if (hp_ < 0) { + hp_ = 0; + } +} + +void HealthComponent::heal(int amount) noexcept { + hp_ += amount; + if (hp_ > totalHp_) { + hp_ = totalHp_; + } +} + +void HealthComponent::reset() noexcept { hp_ = totalHp_; } + +// ============================================================================ +// RenderComponent Implementation +// ============================================================================ + +RenderComponent::RenderComponent( + const std::shared_ptr& animation) + : animation_(animation) {} + +std::shared_ptr RenderComponent::animation() const noexcept { + return animation_; +} + +bool RenderComponent::hasAnimation() const noexcept { + return animation_ != nullptr; +} + +void RenderComponent::setAnimation( + const std::shared_ptr& animation) noexcept { + animation_ = animation; +} + +void RenderComponent::clearAnimation() noexcept { animation_.reset(); } + +void RenderComponent::updatePosition(int x, int y) noexcept { + if (animation_) { + animation_->setPosition(x, y); + } +} + +// ============================================================================ +// CombatComponent Implementation +// ============================================================================ + +CombatComponent::CombatComponent(Weapon* weapon) : weapon_(weapon) {} + +Weapon* CombatComponent::weapon() const noexcept { return weapon_; } +bool CombatComponent::hasWeapon() const noexcept { return weapon_ != nullptr; } +int CombatComponent::lastAttack() const noexcept { return lastAttack_; } +double CombatComponent::dropRate() const noexcept { return dropRate_; } + +void CombatComponent::setWeapon(Weapon* weapon) noexcept { weapon_ = weapon; } +void CombatComponent::setLastAttack(int lastAttack) noexcept { + lastAttack_ = lastAttack; +} +void CombatComponent::setDropRate(double dropRate) noexcept { + dropRate_ = dropRate; +} +void CombatComponent::recordAttack() noexcept { ++lastAttack_; } + +// ============================================================================ +// BuffComponent Implementation +// ============================================================================ + +const std::array& BuffComponent::buffs() const noexcept { + return buffs_; +} + +std::array& BuffComponent::buffs() noexcept { return buffs_; } + +int BuffComponent::buff(int index) const noexcept { return buffs_[index]; } + +void BuffComponent::setBuff(int index, int value) noexcept { + buffs_[index] = value; +} + +void BuffComponent::decrementBuff(int index) noexcept { + if (buffs_[index] > 0) { + --buffs_[index]; + } +} + +void BuffComponent::clearBuffs() noexcept { buffs_.fill(0); } + +bool BuffComponent::isFrozen() const noexcept { + return buffs_[BUFF_FROZEN] > 0; +} + +bool BuffComponent::isSlowed() const noexcept { + return buffs_[BUFF_SLOWDOWN] > 0; +} + +bool BuffComponent::hasDefense() const noexcept { + return buffs_[BUFF_DEFFENCE] > 0; +} + +bool BuffComponent::hasAttackUp() const noexcept { + return buffs_[BUFF_ATTACK] > 0; +} + +// ============================================================================ +// AIComponent Implementation +// ============================================================================ + +AIComponent::AIComponent(std::shared_ptr behavior) + : behavior_(std::move(behavior)) {} + +AIBehavior* AIComponent::behavior() const noexcept { return behavior_.get(); } + +bool AIComponent::hasBehavior() const noexcept { return behavior_ != nullptr; } + +void AIComponent::setBehavior( + std::shared_ptr behavior) noexcept { + behavior_ = std::move(behavior); +} + +void AIComponent::clearBehavior() noexcept { behavior_.reset(); } + +// ============================================================================ +// ScoreComponent Implementation +// ============================================================================ + +const std::shared_ptr& ScoreComponent::score() const noexcept { + return score_; +} + +std::shared_ptr& ScoreComponent::score() noexcept { return score_; } + +void ScoreComponent::setScore(const std::shared_ptr& score) noexcept { + score_ = score; +} + +void ScoreComponent::reset() noexcept { score_.reset(); } \ No newline at end of file diff --git a/src/component.h b/src/component.h new file mode 100644 index 0000000..a90217b --- /dev/null +++ b/src/component.h @@ -0,0 +1,204 @@ +#ifndef SNAKE_COMPONENT_H_ +#define SNAKE_COMPONENT_H_ + +#include "adt.h" +#include "types.h" + +#include +#include + +class Weapon; +class Animation; +class AIBehavior; +class PositionBuffer; + +// ============================================================================ +// TransformComponent - Handles position, direction, and movement state +// ============================================================================ +class TransformComponent { + public: + TransformComponent() = default; + TransformComponent(int x, int y, Direction direction); + TransformComponent(const TransformComponent&) = default; + TransformComponent& operator=(const TransformComponent&) = default; + TransformComponent(TransformComponent&&) noexcept = default; + TransformComponent& operator=(TransformComponent&&) noexcept = default; + ~TransformComponent() = default; + + int x() const noexcept; + int y() const noexcept; + Direction face() const noexcept; + Direction direction() const noexcept; + + void setPosition(int x, int y) noexcept; + void setX(int x) noexcept; + void setY(int y) noexcept; + void setFace(Direction face) noexcept; + void setDirection(Direction direction) noexcept; + + void enqueueDirectionChange(Direction newDirection, + PositionBuffer& buffer) noexcept; + + private: + int x_ = 0; + int y_ = 0; + Direction face_ = Direction::Right; + Direction direction_ = Direction::Right; +}; + +// ============================================================================ +// HealthComponent - Handles HP, damage, and health state +// ============================================================================ +class HealthComponent { + public: + HealthComponent() = default; + HealthComponent(int hp, int totalHp); + HealthComponent(const HealthComponent&) = default; + HealthComponent& operator=(const HealthComponent&) = default; + HealthComponent(HealthComponent&&) noexcept = default; + HealthComponent& operator=(HealthComponent&&) noexcept = default; + ~HealthComponent() = default; + + int hp() const noexcept; + int totalHp() const noexcept; + bool isDead() const noexcept; + bool isAlive() const noexcept; + + void setHp(int hp) noexcept; + void setTotalHp(int totalHp) noexcept; + void takeDamage(int damage) noexcept; + void heal(int amount) noexcept; + void reset() noexcept; + + private: + int hp_ = 0; + int totalHp_ = 0; +}; + +// ============================================================================ +// RenderComponent - Handles animation and visual representation +// ============================================================================ +class RenderComponent { + public: + RenderComponent() = default; + explicit RenderComponent(const std::shared_ptr& animation); + RenderComponent(const RenderComponent&) = default; + RenderComponent& operator=(const RenderComponent&) = default; + RenderComponent(RenderComponent&&) noexcept = default; + RenderComponent& operator=(RenderComponent&&) noexcept = default; + ~RenderComponent() = default; + + std::shared_ptr animation() const noexcept; + bool hasAnimation() const noexcept; + + void setAnimation(const std::shared_ptr& animation) noexcept; + void clearAnimation() noexcept; + void updatePosition(int x, int y) noexcept; + + private: + std::shared_ptr animation_{}; +}; + +// ============================================================================ +// CombatComponent - Handles weapon and attack state +// ============================================================================ +class CombatComponent { + public: + CombatComponent() = default; + explicit CombatComponent(Weapon* weapon); + CombatComponent(const CombatComponent&) = default; + CombatComponent& operator=(const CombatComponent&) = default; + CombatComponent(CombatComponent&&) noexcept = default; + CombatComponent& operator=(CombatComponent&&) noexcept = default; + ~CombatComponent() = default; + + Weapon* weapon() const noexcept; + bool hasWeapon() const noexcept; + int lastAttack() const noexcept; + double dropRate() const noexcept; + + void setWeapon(Weapon* weapon) noexcept; + void setLastAttack(int lastAttack) noexcept; + void setDropRate(double dropRate) noexcept; + void recordAttack() noexcept; + + private: + Weapon* weapon_ = nullptr; + int lastAttack_ = 0; + double dropRate_ = 0.0; +}; + +// ============================================================================ +// BuffComponent - Handles buff/debuff state +// ============================================================================ +class BuffComponent { + public: + BuffComponent() = default; + BuffComponent(const BuffComponent&) = default; + BuffComponent& operator=(const BuffComponent&) = default; + BuffComponent(BuffComponent&&) noexcept = default; + BuffComponent& operator=(BuffComponent&&) noexcept = default; + ~BuffComponent() = default; + + const std::array& buffs() const noexcept; + std::array& buffs() noexcept; + int buff(int index) const noexcept; + + void setBuff(int index, int value) noexcept; + void decrementBuff(int index) noexcept; + void clearBuffs() noexcept; + bool isFrozen() const noexcept; + bool isSlowed() const noexcept; + bool hasDefense() const noexcept; + bool hasAttackUp() const noexcept; + + private: + std::array buffs_{}; +}; + +// ============================================================================ +// AIComponent - Handles AI behavior strategy +// ============================================================================ +class AIComponent { + public: + AIComponent() = default; + explicit AIComponent(std::shared_ptr behavior); + AIComponent(const AIComponent&) = default; + AIComponent& operator=(const AIComponent&) = default; + AIComponent(AIComponent&&) noexcept = default; + AIComponent& operator=(AIComponent&&) noexcept = default; + ~AIComponent() = default; + + AIBehavior* behavior() const noexcept; + bool hasBehavior() const noexcept; + + void setBehavior(std::shared_ptr behavior) noexcept; + void clearBehavior() noexcept; + + private: + std::shared_ptr behavior_{}; +}; + +// ============================================================================ +// ScoreComponent - Handles scoring state +// ============================================================================ +class ScoreComponent { + public: + ScoreComponent() = default; + ScoreComponent(const ScoreComponent&) = default; + ScoreComponent& operator=(const ScoreComponent&) = default; + ScoreComponent(ScoreComponent&&) noexcept = default; + ScoreComponent& operator=(ScoreComponent&&) noexcept = default; + ~ScoreComponent() = default; + + const std::shared_ptr& score() const noexcept; + std::shared_ptr& score() noexcept; + + void setScore(const std::shared_ptr& score) noexcept; + void reset() noexcept; + + private: + std::shared_ptr score_{}; +}; + +#endif \ No newline at end of file diff --git a/src/game.cpp b/src/game.cpp index be56cbc..0461e87 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -29,60 +29,1043 @@ #ifdef DBG #include #endif + extern const int SCALE_FACTOR; extern const int n, m; -extern Texture textures[TEXTURES_SIZE]; -const int MOVE_STEP = 2; +extern Texture textures[TEXTURES_SIZE]; +const int MOVE_STEP = 2; + +namespace { +std::unique_ptr createGameText(const std::string& text, SDL_Color color) { + SDL_Renderer* sdlRenderer = renderer(); + TTF_Font* ttfFont = font(); + if (!sdlRenderer || !ttfFont) { + return nullptr; + } + return std::make_unique(text, color, sdlRenderer, ttfFont); +} + +class AudioObserver { + public: + explicit AudioObserver(EventBus& bus) : bus_(bus) { + bus_.subscribe([this](const GameEvent& event) { onEvent(event); }); + } + AudioObserver(const AudioObserver&) = delete; + AudioObserver& operator=(const AudioObserver&) = delete; + AudioObserver(AudioObserver&&) = delete; + AudioObserver& operator=(AudioObserver&&) = delete; + ~AudioObserver() = default; + + void onEvent(const GameEvent& event) const { + if (event.type == GameEventType::SoundRequested) { + playAudio(event.audioId); + } + } + + private: + EventBus& bus_; +}; + +class UiObserver { + public: + explicit UiObserver(EventBus& bus) : bus_(bus) { + bus_.subscribe([this](const GameEvent& event) { onEvent(event); }); + } + UiObserver(const UiObserver&) = delete; + UiObserver& operator=(const UiObserver&) = delete; + UiObserver(UiObserver&&) = delete; + UiObserver& operator=(UiObserver&&) = delete; + ~UiObserver() = default; + + void onEvent(const GameEvent& event) const { + if (event.type == GameEventType::ItemPicked) { + initInfo(); + } + if (event.type == GameEventType::PlayerDied) { + initInfo(); + } + } + + private: + EventBus& bus_; +}; +} // namespace + +void EventBus::subscribe(Listener listener) { + listeners_.push_back(std::move(listener)); +} + +void EventBus::emit(const GameEvent& event) const { + for (const auto& listener : listeners_) { + listener(event); + } +} + +void EventBus::clear() { + listeners_.clear(); +} + +extern std::array animationsList; +extern Effect effects[]; + +extern Weapon weapons[WEAPONS_SIZE]; +extern Sprite commonSprites[COMMON_SPRITE_SIZE]; + +// Map +std::array, MAP_SIZE> map; +extern bool hasMap[MAP_SIZE][MAP_SIZE]; +int spikeDamage = 1; + +// Global game context +namespace { +GameContext g_gameContext; +std::unique_ptr g_audioObserver; +std::unique_ptr g_uiObserver; +} + +// Legacy global variables (delegating to GameContext) +std::array, SPRITES_MAX_NUM> spriteSnake{}; +BulletList bullets; +int gameLevel = 0; +int stage = 0; +int spritesCount = 0; +int playersCount = 0; +int flasksCount = 0; +int herosCount = 0; +int flasksSetting = 6; +int herosSetting = 8; +int spritesSetting = 25; +int bossSetting = 2; +int GAME_WIN_NUM = 10; +int termCount = 0; +bool willTerm = false; +int status = 0; +double GAME_LUCKY = 1.0; +double GAME_DROPOUT_YELLOW_FLASKS = 0.3; +double GAME_DROPOUT_WEAPONS = 0.7; +double GAME_TRAP_RATE = 0.005; +extern double AI_LOCK_LIMIT; +double GAME_MONSTERS_HP_ADJUST = 1.0; +double GAME_MONSTERS_WEAPON_BUFF_ADJUST = 1.0; +double GAME_MONSTERS_GEN_FACTOR = 1.0; +std::array, MAP_SIZE> itemMap; +std::array, MAP_SIZE> hasEnemy{}; + +// ============================================================================ +// EntityManager Implementation +// ============================================================================ + +void EntityManager::addSnake(const std::shared_ptr& snake) { + if (spritesCount_ < SPRITES_MAX_NUM) { + snakes_[spritesCount_++] = snake; + } +} + +void EntityManager::removeSnake(int index) { + if (index >= 0 && index < spritesCount_) { + snakes_[index] = nullptr; + } +} + +std::shared_ptr EntityManager::getSnake(int index) const { + if (index >= 0 && index < SPRITES_MAX_NUM) { + return snakes_[index]; + } + return nullptr; +} + +int EntityManager::snakeCount() const { + return spritesCount_; +} + +int EntityManager::playerCount() const { + return playersCount_; +} + +void EntityManager::setPlayerCount(int count) { + playersCount_ = count; +} + +void EntityManager::incrementSpriteCount() { + ++spritesCount_; +} + +int EntityManager::spriteCount() const { + return spritesCount_; +} + +void EntityManager::addBullet(const std::shared_ptr& bullet) { + bullets_.push_back(bullet); +} + +void EntityManager::removeBullet(const std::shared_ptr& bullet) { + bullets_.remove(bullet); +} + +BulletList& EntityManager::bullets() { + return bullets_; +} + +const BulletList& EntityManager::bullets() const { + return bullets_; +} + +std::array, SPRITES_MAX_NUM>& EntityManager::snakes() { + return snakes_; +} + +const std::array, SPRITES_MAX_NUM>& EntityManager::snakes() const { + return snakes_; +} + +void EntityManager::clear() { + for (int i = 0; i < SPRITES_MAX_NUM; ++i) { + snakes_[i] = nullptr; + } + bullets_.clear(); + spritesCount_ = 0; + playersCount_ = 0; +} + +// ============================================================================ +// CollisionManager Implementation +// ============================================================================ + +bool CollisionManager::checkCrush(const std::shared_ptr& sprite, + bool loose, bool useAnimationBox, + EntityManager& entityManager) { + if (!sprite) { + return false; + } + const int x = sprite->x(); + const int y = sprite->y(); + SDL_Rect block; + SDL_Rect box = useAnimationBox ? getSpriteAnimationBox(sprite.get()) + : getSpriteFeetBox(sprite.get()); + + // If the sprite is out of the map, then consider it as crushed + if (!inr(x / UNIT, 0, n - 1) || !inr(y / UNIT, 0, m - 1)) { + return true; + } + // Loop over the cells nearby the sprite to know better if it falls out of map + for (int dx = -1; dx <= 1; dx++) { + for (int dy = -1; dy <= 1; dy++) { + int xx = x / UNIT + dx, yy = y / UNIT + dy; + if (inr(xx, 0, n - 1) && inr(yy, 0, m - 1)) { + block = getMapRect(xx, yy); + if (RectRectCross(&box, &block) && !hasMap[xx][yy]) { + return true; + } + } + } + } + + // If it has crushed on other sprites + const int count = entityManager.snakeCount(); + for (int i = 0; i < count; i++) { + const auto& snake = entityManager.getSnake(i); + if (!snake) { + continue; + } + bool self = false; + for (const auto& other : snake->sprites()) { + if (!other) { + continue; + } + if (other.get() != sprite.get()) { + SDL_Rect otherBox = useAnimationBox ? getSpriteAnimationBox(other.get()) + : getSpriteFeetBox(other.get()); + if (RectRectCross(&box, &otherBox)) { + if ((self && loose)) { + // continue checking + } else { + return true; + } + } + } else { + self = true; + } + } + } + return false; +} + +bool CollisionManager::isPlayer(const std::shared_ptr& snake, + EntityManager& entityManager) const { + const int playerCount = entityManager.playerCount(); + for (int i = 0; i < playerCount; i++) { + if (snake && snake == entityManager.getSnake(i)) { + return true; + } + } + return false; +} + +// ============================================================================ +// ItemManager Implementation +// ============================================================================ + +void ItemManager::initItems(int heroCount, int flaskCount) { + int x = 0; + int y = 0; + herosCount_ = 0; + flasksCount_ = 0; + + while (heroCount--) { + do { + x = randInt(1, n - 2); + y = randInt(1, m - 2); + } while (!hasMap[x][y] || map[x][y].type() != BlockType::Floor || + itemMap_[x][y].type() != ItemType::None || + !hasMap[x - 1][y] + !hasMap[x + 1][y] + !hasMap[x][y + 1] + + !hasMap[x][y - 1] >= + 1); + + const int heroId = randInt(SPRITE_KNIGHT, SPRITE_LIZARD); + auto modelAnimation = commonSprites[heroId].animation(); + if (!modelAnimation) { + continue; + } + auto animation = std::make_shared(*modelAnimation); + animation->setAt(At::BottomCenter); + x *= UNIT; + y *= UNIT; + animation->setPosition(x + UNIT / 2, y + UNIT - 3); + itemMap_[x / UNIT][y / UNIT] = + Item(ItemType::Hero, heroId, 0, animation); + pushAnimationToRender(RENDER_LIST_SPRITE_ID, animation); + herosCount_++; + } + + while (flaskCount--) { + do { + x = randInt(0, n - 1); + y = randInt(0, m - 1); + } while (!hasMap[x][y] || map[x][y].type() != BlockType::Floor || + itemMap_[x][y].type() != ItemType::None); + generateItem(x, y, ItemType::HpMedicine); + flasksCount_++; + } +} + +void ItemManager::clearItems() { + for (int i = 0; i < MAP_SIZE; i++) { + for (int j = 0; j < MAP_SIZE; j++) { + itemMap_[i][j].setType(ItemType::None); + } + } +} + +void ItemManager::generateHeroItem(int x, int y) { + const int heroId = randInt(SPRITE_KNIGHT, SPRITE_LIZARD); + auto modelAnimation = commonSprites[heroId].animation(); + if (!modelAnimation) { + return; + } + auto animation = std::make_shared(*modelAnimation); + animation->setAt(At::BottomCenter); + x *= UNIT; + y *= UNIT; + animation->setPosition(x + UNIT / 2, y + UNIT - 3); + itemMap_[x / UNIT][y / UNIT] = + Item(ItemType::Hero, heroId, 0, animation); + pushAnimationToRender(RENDER_LIST_SPRITE_ID, animation); +} + +void ItemManager::generateItem(int x, int y, ItemType type) { + int textureId = RES_FLASK_BIG_RED, id = 0, belong = SPRITE_KNIGHT; + if (type == ItemType::HpMedicine) { + textureId = RES_FLASK_BIG_RED; + } else if (type == ItemType::HpExtraMedicine) { + textureId = RES_FLASK_BIG_YELLOW; + } else if (type == ItemType::Weapon) { + int kind = randInt(0, 5); + if (kind == 0) { + textureId = RES_ICE_SWORD; + id = WEAPON_ICE_SWORD; + belong = SPRITE_KNIGHT; + } else if (kind == 1) { + textureId = RES_HOLY_SWORD; + id = WEAPON_HOLY_SWORD; + belong = SPRITE_KNIGHT; + } else if (kind == 2) { + textureId = RES_THUNDER_STAFF; + id = WEAPON_THUNDER_STAFF; + belong = SPRITE_WIZZARD; + } else if (kind == 3) { + textureId = RES_PURPLE_STAFF; + id = WEAPON_PURPLE_STAFF; + belong = SPRITE_WIZZARD; + } else if (kind == 4) { + textureId = RES_GRASS_SWORD; + id = WEAPON_SOLID_CLAW; + belong = SPRITE_LIZARD; + } else if (kind == 5) { + textureId = RES_POWERFUL_BOW; + id = WEAPON_POWERFUL_BOW; + belong = SPRITE_ELF; + } + } + auto animation = createAndPushAnimation( + animationsList[RENDER_LIST_MAP_ITEMS_ID], &textures[textureId], nullptr, + LoopType::Infinite, 3, x * UNIT, y * UNIT, SDL_FLIP_NONE, 0, + At::BottomLeft); + itemMap_[x][y] = Item(type, id, belong, animation); +} + +void ItemManager::dropItemNearSprite(const Sprite* sprite, ItemType itemType) { + if (!sprite) { + return; + } + for (int dx = -1; dx <= 1; dx++) { + for (int dy = -1; dy <= 1; dy++) { + const int x = sprite->x() / UNIT + dx; + const int y = sprite->y() / UNIT + dy; + if (inr(x, 0, n - 1) && inr(y, 0, m - 1) && hasMap[x][y] && + itemMap_[x][y].type() == ItemType::None) { + generateItem(x, y, itemType); + return; + } + } + } +} + +bool ItemManager::checkItemPickup(const std::shared_ptr& snake, + EntityManager& entityManager) { + // This is handled in makeSnakeCross for now + // Will be fully migrated in future refactoring + return false; +} + +std::array, MAP_SIZE>& ItemManager::itemMap() { + return itemMap_; +} + +const std::array, MAP_SIZE>& ItemManager::itemMap() const { + return itemMap_; +} + +int ItemManager::heroCount() const { + return herosCount_; +} + +void ItemManager::setHeroCount(int count) { + herosCount_ = count; +} + +int ItemManager::flaskCount() const { + return flasksCount_; +} + +void ItemManager::setFlaskCount(int count) { + flasksCount_ = count; +} + +// ============================================================================ +// BuffManager Implementation +// ============================================================================ + +void BuffManager::freezeSnake(Snake* snake, int duration) { + if (!snake) { + return; + } + auto& buffs = snake->buffs(); + if (buffs[BUFF_FROZEN]) { + return; + } + if (!buffs[BUFF_DEFFENCE]) { + buffs[BUFF_FROZEN] += duration; + } + std::shared_ptr effect; + if (buffs[BUFF_DEFFENCE]) { + effect = std::make_shared(effects[EFFECT_VANISH30]); + duration = 30; + } + for (const auto& sprite : snake->sprites()) { + if (!sprite) { + continue; + } + const Effect* effectPtr = effect ? effect.get() : nullptr; + auto animation = createAndPushAnimation( + animationsList[RENDER_LIST_EFFECT_ID], &textures[RES_ICE], effectPtr, + LoopType::Once, duration, sprite->x(), sprite->y(), SDL_FLIP_NONE, 0, + At::BottomCenter); + animation->setScaled(false); + if (buffs[BUFF_DEFFENCE]) { + continue; + } + bindAnimationToSprite(animation, sprite, true); + } +} + +void BuffManager::slowDownSnake(Snake* snake, int duration) { + if (!snake) { + return; + } + auto& buffs = snake->buffs(); + if (buffs[BUFF_SLOWDOWN]) { + return; + } + if (!buffs[BUFF_DEFFENCE]) { + buffs[BUFF_SLOWDOWN] += duration; + } + std::shared_ptr effect; + if (buffs[BUFF_DEFFENCE]) { + effect = std::make_shared(effects[EFFECT_VANISH30]); + duration = 30; + } + for (const auto& sprite : snake->sprites()) { + if (!sprite) { + continue; + } + const Effect* effectPtr = effect ? effect.get() : nullptr; + auto animation = createAndPushAnimation( + animationsList[RENDER_LIST_EFFECT_ID], &textures[RES_SOLIDFX], effectPtr, + LoopType::Lifespan, 40, sprite->x(), sprite->y(), SDL_FLIP_NONE, 0, + At::BottomCenter); + animation->setLifeSpan(duration); + animation->setScaled(false); + if (buffs[BUFF_DEFFENCE]) { + continue; + } + bindAnimationToSprite(animation, sprite, true); + } +} + +void BuffManager::shieldSnake(const std::shared_ptr& snake, int duration) { + if (!snake) { + return; + } + auto& buffs = snake->buffs(); + if (buffs[BUFF_DEFFENCE]) { + return; + } + buffs[BUFF_DEFFENCE] += duration; + for (const auto& sprite : snake->sprites()) { + if (sprite) { + auto animation = createAndPushAnimation( + animationsList[RENDER_LIST_EFFECT_ID], &textures[RES_HOLY_SHIELD], nullptr, + LoopType::Lifespan, 40, sprite->x(), sprite->y(), SDL_FLIP_NONE, 0, + At::BottomCenter); + bindAnimationToSprite(animation, sprite, true); + animation->setLifeSpan(duration); + } + } +} + +void BuffManager::attackUpSnake(const std::shared_ptr& snake, int duration) { + if (!snake) { + return; + } + auto& buffs = snake->buffs(); + if (buffs[BUFF_ATTACK]) { + return; + } + buffs[BUFF_ATTACK] += duration; + for (const auto& sprite : snake->sprites()) { + if (sprite) { + auto animation = createAndPushAnimation( + animationsList[RENDER_LIST_EFFECT_ID], &textures[RES_ATTACK_UP], nullptr, + LoopType::Lifespan, SPRITE_ANIMATION_DURATION, sprite->x(), sprite->y(), + SDL_FLIP_NONE, 0, At::BottomCenter); + bindAnimationToSprite(animation, sprite, true); + animation->setLifeSpan(duration); + } + } +} + +void BuffManager::updateBuffDurations(EntityManager& entityManager) { + const int count = entityManager.snakeCount(); + for (int i = 0; i < count; i++) { + const auto& snake = entityManager.getSnake(i); + if (!snake) { + continue; + } + auto& buffs = snake->buffs(); + for (int j = BUFF_BEGIN; j < BUFF_END; j++) { + if (buffs[j] > 0) { + buffs[j]--; + } + } + } +} + +namespace { +class BuffEffectStrategy { + public: + virtual ~BuffEffectStrategy() = default; + virtual void apply(const std::shared_ptr& source, + const std::shared_ptr& target, + int duration, + BuffManager& manager) const = 0; +}; + +class FrozenBuffStrategy final : public BuffEffectStrategy { + public: + void apply(const std::shared_ptr&, + const std::shared_ptr& target, + int duration, + BuffManager& manager) const override { + manager.freezeSnake(target.get(), duration); + } +}; + +class SlowdownBuffStrategy final : public BuffEffectStrategy { + public: + void apply(const std::shared_ptr&, + const std::shared_ptr& target, + int duration, + BuffManager& manager) const override { + manager.slowDownSnake(target.get(), duration); + } +}; + +class DefenseBuffStrategy final : public BuffEffectStrategy { + public: + void apply(const std::shared_ptr& source, + const std::shared_ptr&, + int duration, + BuffManager& manager) const override { + if (source) { + manager.shieldSnake(source, duration); + } + } +}; + +class AttackBuffStrategy final : public BuffEffectStrategy { + public: + void apply(const std::shared_ptr& source, + const std::shared_ptr&, + int duration, + BuffManager& manager) const override { + if (source) { + manager.attackUpSnake(source, duration); + } + } +}; + +const std::array, BUFF_END>& + getBuffStrategies() { + static const std::array, BUFF_END> + kStrategies = {std::make_unique(), + std::make_unique(), + std::make_unique(), + std::make_unique()}; + return kStrategies; +} +} // namespace + +void BuffManager::invokeWeaponBuff(const std::shared_ptr& src, + const Weapon& weapon, + const std::shared_ptr& dest, + int) { + if (!dest) { + return; + } + double random = 0.0; + const auto& effects = weapon.effects(); + const auto& strategies = getBuffStrategies(); + for (int i = BUFF_BEGIN; i < BUFF_END; i++) { + random = randDouble(); + if (src && src->team() == GAME_MONSTERS_TEAM) { + random *= GAME_MONSTERS_WEAPON_BUFF_ADJUST; + } + const auto& strategy = strategies[i]; + if (strategy && random < effects[i].chance) { + strategy->apply(src, dest, effects[i].duration, *this); + } + } +} + +// ============================================================================ +// GameContext Implementation +// ============================================================================ + +void GameContext::reset() { + entityManager.clear(); + itemManager.clearItems(); + hasEnemy.fill({}); + gameLevel = 0; + stage = 0; + status = 0; + termCount = 0; + willTerm = false; +} + +void GameContext::setLevel(int level) { + gameLevel = level; + spritesSetting = 25; + bossSetting = 2; + herosSetting = 8; + flasksSetting = 6; + gameLucky = 1; + dropoutYellowFlasks = 0.3; + dropoutWeapons = 0.7; + trapRate = 0.005 * (level + 1); + monstersHpAdjust = 1 + level * 0.8 + stage * level * 0.1; + monstersGenFactor = 1 + level * 0.5 + stage * level * 0.05; + monstersWeaponBuffAdjust = 1 + level * 0.8 + stage * level * 0.02; + AI_LOCK_LIMIT = MAX(1, 7 - level * 2 - stage / 2); + winNum = 10 + level * 5 + stage * 3; + + if (level == 1) { + dropoutWeapons = 0.98; + herosSetting = 5; + flasksSetting = 4; + spritesSetting = 28; + bossSetting = 3; + } else if (level == 2) { + dropoutWeapons = 0.98; + dropoutYellowFlasks = 0.3; + spritesSetting = 28; + herosSetting = 5; + flasksSetting = 3; + bossSetting = 5; + } + spritesSetting += stage / 2 * (level + 1); + bossSetting += stage / 3; +} + +void GameContext::initEnemies() { + for (auto& row : hasEnemy) { + row.fill(false); + } + for (int i = -2; i <= 2; i++) { + for (int j = -2; j <= 2; j++) { + hasEnemy[n / 2 + i][m / 2 + j] = true; + } + } +} + +// Global accessor +GameContext& getGameContext() { + return g_gameContext; +} + +void initializeEventObservers() { + if (!g_audioObserver) { + g_audioObserver = std::make_unique(g_gameContext.eventBus); + } + if (!g_uiObserver) { + g_uiObserver = std::make_unique(g_gameContext.eventBus); + } +} + +// ============================================================================ +// GameLoopManager Implementation +// ============================================================================ + +void GameLoopManager::pauseGame() { + pauseSound(); + getGameContext().eventBus.emit( + {GameEventType::SoundRequested, -1, ItemType::None, AUDIO_BUTTON1}); + dim(); + const char msg[] = "Paused"; + extern SDL_Color WHITE; + if (auto text = createGameText(msg, WHITE)) { + renderCenteredText(text.get(), SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, 1); + } + if (SDL_Renderer* sdlRenderer = renderer()) { + SDL_RenderPresent(sdlRenderer); + } + SDL_Event e; + for (bool quit = false; !quit;) { + while (SDL_PollEvent(&e)) { + if (e.type == SDL_QUIT || e.type == SDL_KEYDOWN) { + quit = true; + break; + } + } + } + resumeSound(); + getGameContext().eventBus.emit( + {GameEventType::SoundRequested, -1, ItemType::None, AUDIO_BUTTON1}); +} + +void GameLoopManager::setTerm(GameContext& ctx, int status) { + stopBgm(); + if (status == 0) { + getGameContext().eventBus.emit( + {GameEventType::SoundRequested, -1, ItemType::None, AUDIO_WIN}); + } else { + getGameContext().eventBus.emit( + {GameEventType::SoundRequested, -1, ItemType::None, AUDIO_LOSE}); + } + ctx.status = status; + ctx.willTerm = true; + ctx.termCount = RENDER_TERM_COUNT; +} + +bool GameLoopManager::isWin(const GameContext& ctx) const { + const int playerCount = ctx.entityManager.playerCount(); + if (playerCount != 1) { + return false; + } + const auto& snake = ctx.entityManager.getSnake(0); + return snake && snake->num() >= ctx.winNum; +} + +bool GameLoopManager::handleLocalKeypress(GameContext& ctx) { + static SDL_Event e; + bool quit = false; + while (SDL_PollEvent(&e)) { + if (e.type == SDL_QUIT) { + quit = true; + setTerm(ctx, 1); + } else if (e.type == SDL_KEYDOWN) { + const int keyValue = e.key.keysym.sym; + if (keyValue == SDLK_ESCAPE) { + pauseGame(); + } + const int playerCount = ctx.entityManager.playerCount(); + for (int id = 0; id <= 1 && id < playerCount; id++) { + const auto& player = ctx.entityManager.getSnake(id); + if (!player || player->playerType() != PlayerType::Local) { + continue; + } + if (player->buffs()[BUFF_FROZEN] || player->sprites().empty()) { + continue; + } + std::optional direction; + if (id == 0) { + switch (keyValue) { + case SDLK_LEFT: direction = Direction::Left; break; + case SDLK_RIGHT: direction = Direction::Right; break; + case SDLK_UP: direction = Direction::Up; break; + case SDLK_DOWN: direction = Direction::Down; break; + default: break; + } + } else { + switch (keyValue) { + case SDLK_a: direction = Direction::Left; break; + case SDLK_d: direction = Direction::Right; break; + case SDLK_w: direction = Direction::Up; break; + case SDLK_s: direction = Direction::Down; break; + default: break; + } + } + if (direction.has_value()) { + sendPlayerMovePacket(id, static_cast(*direction)); + auto head = player->sprites().front(); + if (head) { + auto next = player->sprites().size() > 1 + ? *std::next(player->sprites().begin()) + : nullptr; + head->enqueueDirectionChange(*direction, next); + } + } + } + } + } + return quit; +} + +void GameLoopManager::handleLanKeypress(GameContext& ctx) { + static LanPacket packet; + int status = recvLanPacket(&packet); + if (!status) return; + unsigned type = packet.type; + if (type == HEADER_PLAYERMOVE) { + auto* playerMovePacket = reinterpret_cast(&packet); + const auto& player = ctx.entityManager.getSnake(playerMovePacket->playerId); + int direction = playerMovePacket->direction; + if (player && !player->sprites().empty()) { + auto head = player->sprites().front(); + if (head) { + auto next = player->sprites().size() > 1 + ? *std::next(player->sprites().begin()) + : nullptr; + head->enqueueDirectionChange(static_cast(direction), next); + } + } + } else if (type == HEADER_GAMEOVER) { + setTerm(ctx, 1); + } +} + +void GameLoopManager::updateMap(GameContext& ctx) { + const int maskedTime = static_cast(renderFrames() % SPIKE_TIME_MASK); + for (int i = 0; i < SCREEN_WIDTH / UNIT; i++) { + for (int j = 0; j < SCREEN_HEIGHT / UNIT; j++) { + if (hasMap[i][j] && map[i][j].type() == BlockType::Trap) { + if (!maskedTime) { + createAndPushAnimation(animationsList[RENDER_LIST_MAP_SPECIAL_ID], + &textures[RES_FLOOR_SPIKE_OUT_ANI], nullptr, + LoopType::Once, SPIKE_ANI_DURATION, i * UNIT, + j * UNIT, SDL_FLIP_NONE, 0, At::TopLeft); + } else if (maskedTime == SPIKE_ANI_DURATION - 1) { + map[i][j].setEnabled(true); + if (auto animation = map[i][j].animation()) { + animation->setOrigin(&textures[RES_FLOOR_SPIKE_ENABLED]); + } + } else if (maskedTime == SPIKE_ANI_DURATION + SPIKE_OUT_INTERVAL - 1) { + createAndPushAnimation(animationsList[RENDER_LIST_MAP_SPECIAL_ID], + &textures[RES_FLOOR_SPIKE_IN_ANI], nullptr, + LoopType::Once, SPIKE_ANI_DURATION, i * UNIT, + j * UNIT, SDL_FLIP_NONE, 0, At::TopLeft); + map[i][j].setEnabled(false); + if (auto animation = map[i][j].animation()) { + animation->setOrigin(&textures[RES_FLOOR_SPIKE_DISABLED]); + } + } + } + } + } +} -namespace { -Text* createGameText(const std::string& text, SDL_Color color) { - SDL_Renderer* sdlRenderer = renderer(); - TTF_Font* ttfFont = font(); - if (!sdlRenderer || !ttfFont) { - return nullptr; +void GameLoopManager::updateBuffs(GameContext& ctx) { + ctx.buffManager.updateBuffDurations(ctx.entityManager); +} + +void GameLoopManager::moveEntities(GameContext& ctx) { + const int count = ctx.entityManager.snakeCount(); + for (int i = 0; i < count; i++) { + const auto& snake = ctx.entityManager.getSnake(i); + if (!snake || snake->sprites().empty()) { + continue; + } + if (i >= ctx.entityManager.playerCount() && renderFrames() % AI_DECIDE_RATE == 0) { + AiInput(snake); + } + moveSnake(snake); } - return new Text(text, color, sdlRenderer, ttfFont); } -void destroyGameText(Text* text) { - delete text; +void GameLoopManager::processAttacks(GameContext& ctx) { + const int count = ctx.entityManager.snakeCount(); + for (int i = 0; i < count; i++) { + const auto& snake = ctx.entityManager.getSnake(i); + if (!snake || snake->buffs()[BUFF_FROZEN]) { + continue; + } + for (const auto& sprite : snake->sprites()) { + // Attack logic is handled in makeSpriteAttack in the original code + // This is a placeholder for future refactoring + } + } } -} // namespace -extern std::array animationsList; -extern Effect effects[]; +void GameLoopManager::processCollisions(GameContext& ctx) { + // This is handled in makeCross in the original code + // Placeholder for future refactoring +} -extern Weapon weapons[WEAPONS_SIZE]; -extern Sprite commonSprites[COMMON_SPRITE_SIZE]; +void GameLoopManager::processBullets(GameContext& ctx) { + for (const auto& bullet : ctx.entityManager.bullets()) { + if (bullet) { + bullet->update(); + } + } +} -// Map -std::array, MAP_SIZE> map; -std::array, MAP_SIZE> itemMap; -extern bool hasMap[MAP_SIZE][MAP_SIZE]; -std::array, MAP_SIZE> hasEnemy{}; -int spikeDamage = 1; -// Sprite -std::array, SPRITES_MAX_NUM> spriteSnake{}; -BulletList bullets; +void GameLoopManager::cleanupDeadEntities(GameContext& ctx) { + const int playerCount = ctx.entityManager.playerCount(); + int count = ctx.entityManager.snakeCount(); + for (int i = playerCount; i < count; i++) { + const auto& snake = ctx.entityManager.getSnake(i); + if (!snake || snake->num() == 0) { + destroySnake(snake); + ctx.entityManager.removeSnake(i); + for (int j = i; j + 1 < count; j++) { + // Note: This needs more work to properly clean up + } + } + } +} -int gameLevel, stage; -int spritesCount, playersCount, flasksCount, herosCount, flasksSetting, - herosSetting, spritesSetting, bossSetting; -// Win -int GAME_WIN_NUM; -int termCount; -bool willTerm; -int status; -// Drop rate -double GAME_LUCKY; -double GAME_DROPOUT_YELLOW_FLASKS; -double GAME_DROPOUT_WEAPONS; -double GAME_TRAP_RATE; -extern double AI_LOCK_LIMIT; -double GAME_MONSTERS_HP_ADJUST; -double GAME_MONSTERS_WEAPON_BUFF_ADJUST; -double GAME_MONSTERS_GEN_FACTOR; +void GameLoopManager::checkWinCondition(GameContext& ctx) { + if (ctx.willTerm) { + ctx.termCount--; + return; + } + + const int playerCount = ctx.entityManager.playerCount(); + int alivePlayer = -1; + for (int i = 0; i < playerCount; i++) { + const auto& snake = ctx.entityManager.getSnake(i); + if (!snake || snake->sprites().empty()) { + setTerm(ctx, 1); + sendGameOverPacket(alivePlayer); + return; + } + alivePlayer = i; + } + + if (isWin(ctx)) { + setTerm(ctx, 0); + } +} + +int GameLoopManager::run(GameContext& ctx) { + for (bool quit = false; !quit;) { + quit = handleLocalKeypress(ctx); + if (quit) sendGameOverPacket(3); + if (lanClientSocket != NULL) handleLanKeypress(ctx); + + updateMap(ctx); + + const int count = ctx.entityManager.snakeCount(); + for (int i = 0; i < count; i++) { + const auto& snake = ctx.entityManager.getSnake(i); + if (!snake || snake->sprites().empty()) { + continue; + } + if (i >= ctx.entityManager.playerCount() && renderFrames() % AI_DECIDE_RATE == 0) { + AiInput(snake); + } + moveSnake(snake); + // Attack processing is in original makeSnakeAttack + } + + processBullets(ctx); + + if (renderFrames() % GAME_MAP_RELOAD_PERIOD == 0) { + // initItemMap is called in original code + } + + for (int i = 0; i < count; i++) { + const auto& snake = ctx.entityManager.getSnake(i); + updateAnimationOfSnake(snake); + if (!snake) { + continue; + } + if (snake->buffs()[BUFF_FROZEN]) { + for (const auto& sprite : snake->sprites()) { + if (sprite && sprite->animation()) { + sprite->animation()->setCurrentFrame( + sprite->animation()->currentFrame() - 1); + } + } + } + } + + // makeCross() is called in original + render(); + updateBuffs(ctx); + + // Cleanup dead entities + for (int i = ctx.entityManager.playerCount(); i < ctx.entityManager.snakeCount(); i++) { + const auto& snake = ctx.entityManager.getSnake(i); + if (!snake || snake->num() == 0) { + destroySnake(snake); + // Note: Entity cleanup needs more work + } + } + + checkWinCondition(ctx); + + if (ctx.willTerm) { + ctx.termCount--; + if (!ctx.termCount) { + break; + } + } + } + return ctx.status; +} + +// Legacy function implementations (delegating to managers where appropriate) void setLevel(int level) { gameLevel = level; spritesSetting = 25; @@ -169,7 +1152,7 @@ void appendSpriteToSnake(const std::shared_ptr& snake, int spriteId, y += delta; } } - auto sprite = std::make_shared(commonSprites[spriteId], x, y); + const auto sprite = std::make_shared(commonSprites[spriteId], x, y); sprite->setDirection(direction); if (direction == Direction::Left) { sprite->setFace(Direction::Left); @@ -189,7 +1172,7 @@ void appendSpriteToSnake(const std::shared_ptr& snake, int spriteId, } if (snake->buffs()[BUFF_DEFFENCE]) { - shieldSprite(sprite, snake->buffs()[BUFF_DEFFENCE]); + getGameContext().buffManager.shieldSnake(snake, snake->buffs()[BUFF_DEFFENCE]); } } @@ -476,124 +1459,6 @@ void initEnemies(int enemiesCount) { * Put buff animation on snake */ -void freezeSnake(Snake* snake, int duration) { - if (!snake) { - return; - } - auto& buffs = snake->buffs(); - if (buffs[BUFF_FROZEN]) { - return; - } - if (!buffs[BUFF_DEFFENCE]) { - buffs[BUFF_FROZEN] += duration; - } - std::shared_ptr effect; - if (buffs[BUFF_DEFFENCE]) { - effect = std::make_shared(effects[EFFECT_VANISH30]); - duration = 30; - } - for (const auto& sprite : snake->sprites()) { - if (!sprite) { - continue; - } - const Effect* effectPtr = effect ? effect.get() : nullptr; - auto animation = createAndPushAnimation( - animationsList[RENDER_LIST_EFFECT_ID], &textures[RES_ICE], effectPtr, - LoopType::Once, duration, sprite->x(), sprite->y(), SDL_FLIP_NONE, 0, - At::BottomCenter); - animation->setScaled(false); - if (buffs[BUFF_DEFFENCE]) { - continue; - } - bindAnimationToSprite(animation, sprite, true); - } -} - -void slowDownSnake(Snake* snake, int duration) { - if (!snake) { - return; - } - auto& buffs = snake->buffs(); - if (buffs[BUFF_SLOWDOWN]) { - return; - } - if (!buffs[BUFF_DEFFENCE]) { - buffs[BUFF_SLOWDOWN] += duration; - } - std::shared_ptr effect; - if (buffs[BUFF_DEFFENCE]) { - effect = std::make_shared(effects[EFFECT_VANISH30]); - duration = 30; - } - for (const auto& sprite : snake->sprites()) { - if (!sprite) { - continue; - } - const Effect* effectPtr = effect ? effect.get() : nullptr; - auto animation = createAndPushAnimation( - animationsList[RENDER_LIST_EFFECT_ID], &textures[RES_SOLIDFX], effectPtr, - LoopType::Lifespan, 40, sprite->x(), sprite->y(), SDL_FLIP_NONE, 0, - At::BottomCenter); - animation->setLifeSpan(duration); - animation->setScaled(false); - if (buffs[BUFF_DEFFENCE]) { - continue; - } - bindAnimationToSprite(animation, sprite, true); - } -} - -void shieldSprite(const std::shared_ptr& sprite, int duration) { - if (!sprite) { - return; - } - auto animation = createAndPushAnimation( - animationsList[RENDER_LIST_EFFECT_ID], &textures[RES_HOLY_SHIELD], nullptr, - LoopType::Lifespan, 40, sprite->x(), sprite->y(), SDL_FLIP_NONE, 0, - At::BottomCenter); - bindAnimationToSprite(animation, sprite, true); - animation->setLifeSpan(duration); -} - -void shieldSnake(const std::shared_ptr& snake, int duration) { - if (!snake) { - return; - } - auto& buffs = snake->buffs(); - if (buffs[BUFF_DEFFENCE]) { - return; - } - buffs[BUFF_DEFFENCE] += duration; - for (const auto& sprite : snake->sprites()) { - shieldSprite(sprite, duration); - } -} - -void attackUpSprite(const std::shared_ptr& sprite, int duration) { - if (!sprite) { - return; - } - auto animation = createAndPushAnimation( - animationsList[RENDER_LIST_EFFECT_ID], &textures[RES_ATTACK_UP], nullptr, - LoopType::Lifespan, SPRITE_ANIMATION_DURATION, sprite->x(), sprite->y(), - SDL_FLIP_NONE, 0, At::BottomCenter); - bindAnimationToSprite(animation, sprite, true); - animation->setLifeSpan(duration); -} - -void attackUpSnkae(const std::shared_ptr& snake, int duration) { - if (!snake) { - return; - } - auto& buffs = snake->buffs(); - if (buffs[BUFF_ATTACK]) { - return; - } - buffs[BUFF_ATTACK] += duration; - for (const auto& sprite : snake->sprites()) { - attackUpSprite(sprite, duration); - } -} /* Initialize and deinitialize game and snake @@ -608,6 +1473,7 @@ void initGame(int localPlayers, int remotePlayers, bool localFirst) { initCountDownBar(); // create default hero at (w/2, h/2) (as well push ani) + initializeEventObservers(); for (int i = 0; i < localPlayers + remotePlayers; i++) { PlayerType playerType = PlayerType::Local; if (localFirst) { @@ -616,7 +1482,9 @@ void initGame(int localPlayers, int remotePlayers, bool localFirst) { playerType = i < remotePlayers ? PlayerType::Remote : PlayerType::Local; } initPlayer(static_cast(playerType)); - shieldSnake(spriteSnake[i], 300); + if (spriteSnake[i]) { + getGameContext().buffManager.shieldSnake(spriteSnake[i], 300); + } } initInfo(); // create map @@ -642,8 +1510,7 @@ void destroyGame(int status) { blackout(); const char* msg = status == 0 ? "Stage Clear" : "Game Over"; extern SDL_Color WHITE; - if (Text* rawText = createGameText(msg, WHITE)) { - std::shared_ptr text(rawText, destroyGameText); + if (auto text = createGameText(msg, WHITE)) { renderCenteredText(text.get(), SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, 2); } if (SDL_Renderer* sdlRenderer = renderer()) { @@ -754,42 +1621,6 @@ void dropItem(const std::shared_ptr& sprite) { } } -void invokeWeaponBuff(const std::shared_ptr& src, Weapon* weapon, - const std::shared_ptr& dest, int damage) { - if (!weapon || !dest) { - return; - } - double random; - const auto& effects = weapon->effects(); - for (int i = BUFF_BEGIN; i < BUFF_END; i++) { - random = randDouble(); - if (src && src->team() == GAME_MONSTERS_TEAM) { - random *= GAME_MONSTERS_WEAPON_BUFF_ADJUST; - } - if (random < effects[i].chance) { - switch (i) { - case BUFF_FROZEN: - freezeSnake(dest.get(), effects[i].duration); - break; - case BUFF_SLOWDOWN: - slowDownSnake(dest.get(), effects[i].duration); - break; - case BUFF_DEFFENCE: - if (src) { - shieldSnake(src, effects[i].duration); - } - break; - case BUFF_ATTACK: - if (src) { - attackUpSnkae(src, effects[i].duration); - } - break; - default: - break; - } - } - } -} void dealDamage(const std::shared_ptr& src, const std::shared_ptr& dest, @@ -857,35 +1688,41 @@ bool makeSnakeCross(const std::shared_ptr& snake) { if (RectRectCross(&headBox, &block)) { bool taken = true; auto animation = itemMap[i][j].animation(); - if (itemMap[i][j].type() == ItemType::Hero) { - playAudio(AUDIO_COIN); + const ItemType itemType = itemMap[i][j].type(); + if (itemType == ItemType::Hero) { appendSpriteToSnake(snake, itemMap[i][j].id(), 0, 0, Direction::Right); herosCount--; if (animation) { animationsList[RENDER_LIST_SPRITE_ID].remove(animation); } - } else if (itemMap[i][j].type() == ItemType::HpMedicine || - itemMap[i][j].type() == ItemType::HpExtraMedicine) { - playAudio(AUDIO_MED); + getGameContext().eventBus.emit( + {GameEventType::SoundRequested, -1, itemType, AUDIO_COIN}); + } else if (itemType == ItemType::HpMedicine || + itemType == ItemType::HpExtraMedicine) { takeHpMedcine(snake.get(), GAME_HP_MEDICINE_DELTA, - itemMap[i][j].type() == ItemType::HpExtraMedicine); - flasksCount -= itemMap[i][j].type() == ItemType::HpMedicine; + itemType == ItemType::HpExtraMedicine); + flasksCount -= itemType == ItemType::HpMedicine; if (animation) { animationsList[RENDER_LIST_MAP_ITEMS_ID].remove(animation); } - } else if (itemMap[i][j].type() == ItemType::Weapon) { + getGameContext().eventBus.emit( + {GameEventType::SoundRequested, -1, itemType, AUDIO_MED}); + } else if (itemType == ItemType::Weapon) { taken = takeWeapon(snake.get(), &itemMap[i][j]); if (taken) { - playAudio(AUDIO_MED); if (animation) { animationsList[RENDER_LIST_MAP_ITEMS_ID].remove(animation); } + getGameContext().eventBus.emit( + {GameEventType::SoundRequested, -1, itemType, AUDIO_MED}); } } if (taken) { itemMap[i][j].setType(ItemType::None); + getGameContext().eventBus.emit( + {GameEventType::ItemPicked, -1, itemType, -1}); } } } @@ -897,7 +1734,11 @@ bool makeSnakeCross(const std::shared_ptr& snake) { if (!sprite || sprite->hp() > 0) { continue; } - playAudio(AUDIO_HUMAN_DEATH); + const int playerId = isPlayer(snake) ? 0 : -1; + getGameContext().eventBus.emit( + {GameEventType::SoundRequested, playerId, ItemType::None, AUDIO_HUMAN_DEATH}); + getGameContext().eventBus.emit( + {GameEventType::PlayerDied, playerId, ItemType::None, -1}); Texture* death = sprite->animation() ? sprite->animation()->origin() : nullptr; if (!death) { continue; @@ -971,9 +1812,12 @@ bool makeBulletCross(const std::shared_ptr& bullet) { hit = true; } + GameContext& ctx = getGameContext(); + const WeaponBehavior& behavior = weapon->behavior(); if (!hit) { - for (int i = 0; i < spritesCount; i++) { - const auto& snake = spriteSnake[i]; + const int count = ctx.entityManager.snakeCount(); + for (int i = 0; i < count; i++) { + const auto& snake = ctx.entityManager.getSnake(i); if (!snake || bullet->team() == snake->team()) { continue; } @@ -989,21 +1833,18 @@ bool makeBulletCross(const std::shared_ptr& bullet) { pushAnimationToRender(RENDER_LIST_EFFECT_ID, effect); } hit = true; - if (weapon->type() == WeaponType::GunPoint || - weapon->type() == WeaponType::GunPointMulti) { - dealDamage(bullet->owner(), snake, target, weapon->damage()); - invokeWeaponBuff(bullet->owner(), weapon, snake, weapon->damage()); - return hit; - } - break; + behavior.applyImpact(*weapon, bullet, snake, target); + return hit; } } } } - if (hit) { - playAudio(weapon->deathAudio()); - for (int i = 0; i < spritesCount; i++) { - const auto& snake = spriteSnake[i]; + if (hit && behavior.allowAreaImpact()) { + getGameContext().eventBus.emit( + {GameEventType::SoundRequested, -1, ItemType::None, weapon->deathAudio()}); + const int count = ctx.entityManager.snakeCount(); + for (int i = 0; i < count; i++) { + const auto& snake = ctx.entityManager.getSnake(i); if (!snake || bullet->team() == snake->team()) { continue; } @@ -1014,8 +1855,7 @@ bool makeBulletCross(const std::shared_ptr& bullet) { if (distance(Point{target->x(), target->y()}, Point{bullet->x(), bullet->y()}) <= weapon->effectRange()) { - dealDamage(bullet->owner(), snake, target, weapon->damage()); - invokeWeaponBuff(bullet->owner(), weapon, snake, weapon->damage()); + behavior.applyAreaImpact(*weapon, bullet, snake, target); } } } @@ -1136,9 +1976,12 @@ void makeSpriteAttack(const std::shared_ptr& sprite, if (static_cast(renderFrames()) - sprite->lastAttack() < weapon->gap()) { return; } + GameContext& ctx = getGameContext(); bool attacked = false; - for (int i = 0; i < spritesCount; i++) { - const auto& targetSnake = spriteSnake[i]; + const WeaponBehavior& behavior = weapon->behavior(); + const int count = ctx.entityManager.snakeCount(); + for (int i = 0; i < count; i++) { + const auto& targetSnake = ctx.entityManager.getSnake(i); if (!targetSnake || snake->team() == targetSnake->team()) { continue; } @@ -1150,53 +1993,19 @@ void makeSpriteAttack(const std::shared_ptr& sprite, Point{target->x(), target->y()}) > weapon->shootRange()) { continue; } - const double rad = atan2(target->y() - sprite->y(), - target->x() - sprite->x()); - if (weapon->type() == WeaponType::SwordPoint || - weapon->type() == WeaponType::SwordRange) { - if (const auto& deathAnimation = weapon->deathAnimation()) { - auto effect = std::make_shared(*deathAnimation); - bindAnimationToSprite(effect, target, false); - if (effect->angle() != -1) { - effect->setAngle(rad * 180 / PI); - } - pushAnimationToRender(RENDER_LIST_EFFECT_ID, effect); - } - dealDamage(snake, targetSnake, target, weapon->damage()); - invokeWeaponBuff(snake, weapon, targetSnake, weapon->damage()); - attacked = true; - if (weapon->type() == WeaponType::SwordPoint) { - goto ATTACK_END; - } - } else { - const auto bulletAnimation = weapon->flyAnimation(); - auto bullet = std::make_shared(snake, weapon, sprite->x(), - sprite->y(), rad, snake->team(), - bulletAnimation); - bullets.push_back(bullet); - pushAnimationToRender(RENDER_LIST_EFFECT_ID, bullet->animation()); - attacked = true; - if (weapon->type() != WeaponType::GunPointMulti) { - goto ATTACK_END; - } + const WeaponAttackResult result = + behavior.attack(*weapon, {snake, targetSnake, target, sprite}); + attacked = attacked || result.attacked; + if (attacked && !behavior.allowMultiTarget()) { + break; } } + if (attacked && !behavior.allowMultiTarget()) { + break; + } } -ATTACK_END: if (attacked) { - if (const auto& birthAnimation = weapon->birthAnimation()) { - auto effect = std::make_shared(*birthAnimation); - bindAnimationToSprite(effect, sprite, true); - effect->setAt(At::BottomCenter); - pushAnimationToRender(RENDER_LIST_EFFECT_ID, effect); - } - if (weapon->type() == WeaponType::SwordPoint || - weapon->type() == WeaponType::SwordRange) { - playAudio(weapon->deathAudio()); - } else { - playAudio(weapon->birthAudio()); - } - sprite->setLastAttack(static_cast(renderFrames())); + behavior.onAttack(*weapon, sprite); } } void makeSnakeAttack(const std::shared_ptr& snake) { @@ -1218,9 +2027,11 @@ enum class GameStatus { StageClear, GameOver }; void setTerm(GameStatus s) { stopBgm(); if (s == GameStatus::StageClear) { - playAudio(AUDIO_WIN); + getGameContext().eventBus.emit( + {GameEventType::SoundRequested, -1, ItemType::None, AUDIO_WIN}); } else { - playAudio(AUDIO_LOSE); + getGameContext().eventBus.emit( + {GameEventType::SoundRequested, -1, ItemType::None, AUDIO_LOSE}); } status = s == GameStatus::StageClear ? 0 : 1; willTerm = true; @@ -1228,12 +2039,12 @@ void setTerm(GameStatus s) { } void pauseGame() { pauseSound(); - playAudio(AUDIO_BUTTON1); + getGameContext().eventBus.emit( + {GameEventType::SoundRequested, -1, ItemType::None, AUDIO_BUTTON1}); dim(); const char msg[] = "Paused"; extern SDL_Color WHITE; - if (Text* rawText = createGameText(msg, WHITE)) { - std::shared_ptr text(rawText, destroyGameText); + if (auto text = createGameText(msg, WHITE)) { renderCenteredText(text.get(), SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, 1); } if (SDL_Renderer* sdlRenderer = renderer()) { @@ -1249,7 +2060,8 @@ void pauseGame() { } } resumeSound(); - playAudio(AUDIO_BUTTON1); + getGameContext().eventBus.emit( + {GameEventType::SoundRequested, -1, ItemType::None, AUDIO_BUTTON1}); } std::optional arrowsToDirection(int keyValue) { @@ -1288,7 +2100,7 @@ bool handleLocalKeypress() { quit = true; setTerm(GameStatus::GameOver); } else if (e.type == SDL_KEYDOWN) { - int keyValue = e.key.keysym.sym; + const int keyValue = e.key.keysym.sym; if (keyValue == SDLK_ESCAPE) { pauseGame(); } diff --git a/src/game.h b/src/game.h index 1227592..098ee60 100644 --- a/src/game.h +++ b/src/game.h @@ -5,9 +5,13 @@ #include "player.h" #include "sprite.h" #include "types.h" +#include "map.h" +#include "render.h" #include #include +#include +#include // Control #define SPRITES_MAX_NUM 1024 @@ -37,12 +41,262 @@ // Drop Rate // Win +enum class GameEventType { PlayerDied, ItemPicked, SoundRequested }; + +struct GameEvent { + GameEventType type = GameEventType::SoundRequested; + int playerId = -1; + ItemType itemType = ItemType::None; + int audioId = -1; +}; + +class EventBus { + public: + using Listener = std::function; + + void subscribe(Listener listener); + void emit(const GameEvent& event) const; + void clear(); + + private: + std::vector listeners_{}; +}; + +// Forward declarations +class AIBehavior; +class EntityManager; +class CollisionManager; +class ItemManager; +class BuffManager; +class GameLoopManager; +class MapManager; +class WeaponBehavior; +class Weapon; +struct GameContext; +struct GameEvent; +class EventBus; + +// Global extern declarations (to be removed after full refactoring) +extern const int SCALE_FACTOR; +extern const int n, m; +extern Texture textures[]; +extern Effect effects[]; +extern Weapon weapons[]; +extern Sprite commonSprites[]; +extern std::array animationsList; +extern bool hasMap[MAP_SIZE][MAP_SIZE]; + +// ============================================================================ +// EntityManager - Manages all game entities (snakes, bullets) +// ============================================================================ +class EntityManager { + public: + EntityManager() = default; + EntityManager(const EntityManager&) = delete; + EntityManager& operator=(const EntityManager&) = delete; + EntityManager(EntityManager&&) = default; + EntityManager& operator=(EntityManager&&) = default; + ~EntityManager() = default; + + // Snake management + void addSnake(const std::shared_ptr& snake); + void removeSnake(int index); + std::shared_ptr getSnake(int index) const; + int snakeCount() const; + int playerCount() const; + void setPlayerCount(int count); + void incrementSpriteCount(); + int spriteCount() const; + + // Bullet management + void addBullet(const std::shared_ptr& bullet); + void removeBullet(const std::shared_ptr& bullet); + BulletList& bullets(); + const BulletList& bullets() const; + + // Entity access + std::array, SPRITES_MAX_NUM>& snakes(); + const std::array, SPRITES_MAX_NUM>& snakes() const; + + void clear(); + + private: + std::array, SPRITES_MAX_NUM> snakes_{}; + BulletList bullets_{}; + int spritesCount_ = 0; + int playersCount_ = 0; +}; + +// ============================================================================ +// CollisionManager - Handles all collision detection +// ============================================================================ +class CollisionManager { + public: + CollisionManager() = default; + CollisionManager(const CollisionManager&) = delete; + CollisionManager& operator=(const CollisionManager&) = delete; + CollisionManager(CollisionManager&&) = default; + CollisionManager& operator=(CollisionManager&&) = default; + ~CollisionManager() = default; + + bool checkCrush(const std::shared_ptr& sprite, bool loose, + bool useAnimationBox, EntityManager& entityManager); + bool isPlayer(const std::shared_ptr& snake, EntityManager& entityManager) const; + + private: +}; + +// ============================================================================ +// ItemManager - Handles item generation and management +// ============================================================================ +class ItemManager { + public: + ItemManager() = default; + ItemManager(const ItemManager&) = delete; + ItemManager& operator=(const ItemManager&) = delete; + ItemManager(ItemManager&&) = default; + ItemManager& operator=(ItemManager&&) = default; + ~ItemManager() = default; + + void initItems(int heroCount, int flaskCount); + void clearItems(); + void generateHeroItem(int x, int y); + void generateItem(int x, int y, ItemType type); + void dropItemNearSprite(const Sprite* sprite, ItemType itemType); + bool checkItemPickup(const std::shared_ptr& snake, + EntityManager& entityManager); + + std::array, MAP_SIZE>& itemMap(); + const std::array, MAP_SIZE>& itemMap() const; + + int heroCount() const; + void setHeroCount(int count); + int flaskCount() const; + void setFlaskCount(int count); + + private: + std::array, MAP_SIZE> itemMap_{}; + int herosCount_ = 0; + int flasksCount_ = 0; +}; + +// ============================================================================ +// BuffManager - Handles buff effects on snakes +// ============================================================================ +class BuffManager { + public: + BuffManager() = default; + BuffManager(const BuffManager&) = delete; + BuffManager& operator=(const BuffManager&) = delete; + BuffManager(BuffManager&&) = default; + BuffManager& operator=(BuffManager&&) = default; + ~BuffManager() = default; + + void freezeSnake(Snake* snake, int duration); + void slowDownSnake(Snake* snake, int duration); + void shieldSnake(const std::shared_ptr& snake, int duration); + void attackUpSnake(const std::shared_ptr& snake, int duration); + void updateBuffDurations(EntityManager& entityManager); + void invokeWeaponBuff(const std::shared_ptr& src, const Weapon& weapon, + const std::shared_ptr& dest, int damage); + + private: +}; + +// ============================================================================ +// GameLoopManager - Handles the main game loop logic +// ============================================================================ +class GameLoopManager { + public: + GameLoopManager() = default; + GameLoopManager(const GameLoopManager&) = delete; + GameLoopManager& operator=(const GameLoopManager&) = delete; + GameLoopManager(GameLoopManager&&) = default; + GameLoopManager& operator=(GameLoopManager&&) = default; + ~GameLoopManager() = default; + + // Main game loop + int run(GameContext& ctx); + + // Input handling + bool handleLocalKeypress(GameContext& ctx); + void handleLanKeypress(GameContext& ctx); + + // Game state updates + void updateMap(GameContext& ctx); + void updateBuffs(GameContext& ctx); + void moveEntities(GameContext& ctx); + void processAttacks(GameContext& ctx); + void processCollisions(GameContext& ctx); + void processBullets(GameContext& ctx); + void cleanupDeadEntities(GameContext& ctx); + void checkWinCondition(GameContext& ctx); + + // Game state checks + bool isWin(const GameContext& ctx) const; + + private: + void pauseGame(); + void setTerm(GameContext& ctx, int status); +}; + +// ============================================================================ +// GameContext - Central context holding all game state +// ============================================================================ +struct GameContext { + EntityManager entityManager; + CollisionManager collisionManager; + ItemManager itemManager; + BuffManager buffManager; + std::shared_ptr aiBehavior; + EventBus eventBus; + + // Game state + int gameLevel = 0; + int stage = 0; + int status = 0; + int termCount = 0; + bool willTerm = false; + + // Level settings + int spritesSetting = 25; + int bossSetting = 2; + int herosSetting = 8; + int flasksSetting = 6; + double gameLucky = 1.0; + double dropoutYellowFlasks = 0.3; + double dropoutWeapons = 0.7; + double trapRate = 0.005; + double monstersHpAdjust = 1.0; + double monstersWeaponBuffAdjust = 1.0; + double monstersGenFactor = 1.0; + int winNum = 10; + + // Enemy position tracking + std::array, MAP_SIZE> hasEnemy{}; + + GameContext() = default; + GameContext(const GameContext&) = delete; + GameContext& operator=(const GameContext&) = delete; + GameContext(GameContext&&) = default; + GameContext& operator=(GameContext&&) = default; + ~GameContext() = default; + + void reset(); + void setLevel(int level); + void initEnemies(); +}; + +// ============================================================================ +// Game Functions - High level game operations +// ============================================================================ + void pushMapToRender(); std::vector> startGame(int localPlayers, int remotePlayers, bool localFirst); void initGame(int localPlayers, int remotePlayers, bool localFirst); -void destroyGame(int); +void destroyGame(int status); void destroySnake(const std::shared_ptr& snake); int gameLoop(); void updateAnimationOfSprite(const std::shared_ptr& self); @@ -66,4 +320,8 @@ void appendSpriteToSnake(const std::shared_ptr& snake, int spriteId, int x, int y, Direction direction); void setLevel(int level); +// Get the global game context (temporary during refactoring) +GameContext& getGameContext(); +void initializeEventObservers(); + #endif diff --git a/src/player.cpp b/src/player.cpp index 7f55095..4c39be1 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -2,22 +2,52 @@ Snake::Snake(int step, int team, PlayerType playerType) : moveStep_(step), team_(team), playerType_(playerType) { - buffs_.fill(0); - score_ = std::make_shared(); + scoreComponent_.setScore(std::make_shared()); } +// Identity accessors int Snake::moveStep() const { return moveStep_; } int Snake::team() const { return team_; } int Snake::num() const { return num_; } PlayerType Snake::playerType() const { return playerType_; } -const std::array& Snake::buffs() const { return buffs_; } -std::array& Snake::buffs() { return buffs_; } -const std::shared_ptr& Snake::score() const { return score_; } - void Snake::setMoveStep(int step) { moveStep_ = step; } void Snake::setTeam(int team) { team_ = team; } void Snake::incrementNum() { ++num_; } +// BuffComponent accessors +const std::array& Snake::buffs() const { + return buffComponent_.buffs(); +} +std::array& Snake::buffs() { return buffComponent_.buffs(); } +bool Snake::isFrozen() const { return buffComponent_.isFrozen(); } +bool Snake::isSlowed() const { return buffComponent_.isSlowed(); } +bool Snake::hasDefense() const { return buffComponent_.hasDefense(); } +bool Snake::hasAttackUp() const { return buffComponent_.hasAttackUp(); } + +// ScoreComponent accessors +const std::shared_ptr& Snake::score() const { + return scoreComponent_.score(); +} +std::shared_ptr& Snake::score() { return scoreComponent_.score(); } + +// AIComponent accessors +AIBehavior* Snake::aiBehavior() const { return aiComponent_.behavior(); } +bool Snake::hasAIBehavior() const { return aiComponent_.hasBehavior(); } +void Snake::setAIBehavior(std::shared_ptr behavior) { + aiComponent_.setBehavior(std::move(behavior)); +} + +// Sprite management SpriteList& Snake::sprites() { return sprites_; } const SpriteList& Snake::sprites() const { return sprites_; } + +// Direct component access +BuffComponent& Snake::buffComponent() { return buffComponent_; } +const BuffComponent& Snake::buffComponent() const { return buffComponent_; } +ScoreComponent& Snake::scoreComponent() { return scoreComponent_; } +const ScoreComponent& Snake::scoreComponent() const { + return scoreComponent_; +} +AIComponent& Snake::aiComponent() { return aiComponent_; } +const AIComponent& Snake::aiComponent() const { return aiComponent_; } diff --git a/src/player.h b/src/player.h index 9a9c746..7ea3521 100644 --- a/src/player.h +++ b/src/player.h @@ -2,6 +2,7 @@ #define SNAKE__PLAYER_H_ #include "adt.h" +#include "component.h" #include "types.h" #include @@ -9,6 +10,10 @@ enum class PlayerType { Local, Remote, Computer }; +// ============================================================================ +// Snake - Composes BuffComponent, ScoreComponent, and AIComponent +// to avoid massive base class. Sprite list is managed separately. +// ============================================================================ class Snake { public: Snake(int step, int team, PlayerType playerType); @@ -18,28 +23,53 @@ class Snake { Snake& operator=(Snake&&) noexcept = default; ~Snake() = default; + // Identity accessors int moveStep() const; int team() const; int num() const; PlayerType playerType() const; - const std::array& buffs() const; - std::array& buffs(); - const std::shared_ptr& score() const; void setMoveStep(int step); void setTeam(int team); void incrementNum(); + // BuffComponent accessors + const std::array& buffs() const; + std::array& buffs(); + bool isFrozen() const; + bool isSlowed() const; + bool hasDefense() const; + bool hasAttackUp() const; + + // ScoreComponent accessors + const std::shared_ptr& score() const; + std::shared_ptr& score(); + + // AIComponent accessors + AIBehavior* aiBehavior() const; + bool hasAIBehavior() const; + void setAIBehavior(std::shared_ptr behavior); + + // Sprite management SpriteList& sprites(); const SpriteList& sprites() const; + // Direct component access for advanced usage + BuffComponent& buffComponent(); + const BuffComponent& buffComponent() const; + ScoreComponent& scoreComponent(); + const ScoreComponent& scoreComponent() const; + AIComponent& aiComponent(); + const AIComponent& aiComponent() const; + private: SpriteList sprites_{}; + BuffComponent buffComponent_{}; + ScoreComponent scoreComponent_{}; + AIComponent aiComponent_{}; int moveStep_ = 0; int team_ = 0; int num_ = 0; - std::array buffs_{}; - std::shared_ptr score_{}; PlayerType playerType_ = PlayerType::Local; }; diff --git a/src/render.cpp b/src/render.cpp index 0d02838..711b0e9 100644 --- a/src/render.cpp +++ b/src/render.cpp @@ -34,10 +34,7 @@ std::shared_ptr makeText(const std::string& text, } } // namespace -// Sprite -extern std::array, SPRITES_MAX_NUM> spriteSnake; -extern int spritesCount; -extern int playersCount; +// Sprite - using GameContext for access extern Effect effects[]; extern SDL_Color BLACK; extern SDL_Color WHITE; @@ -78,6 +75,7 @@ void initCountDownBar() { } void initInfo() { extern int stage; + GameContext& ctx = getGameContext(); SDL_Renderer* sdlRenderer = renderer(); if (!sdlRenderer) { return; @@ -90,7 +88,8 @@ void initInfo() { } else { stageText = makeText(buf, sdlRenderer, ttfFont, WHITE); } - for (int i = 0; i < playersCount; i++) { + const int playerCount = ctx.entityManager.playerCount(); + for (int i = 0; i < playerCount; i++) { if (!scoresText[i]) { scoresText[i] = makeText("placeholder", sdlRenderer, ttfFont, WHITE); } @@ -404,8 +403,10 @@ void renderSnakeHp(const std::shared_ptr& snake) { } } void renderHp() { - for (int i = 0; i < spritesCount; i++) { - renderSnakeHp(spriteSnake[i]); + GameContext& ctx = getGameContext(); + const int count = ctx.entityManager.snakeCount(); + for (int i = 0; i < count; i++) { + renderSnakeHp(ctx.entityManager.getSnake(i)); } } void renderCenteredTextBackground(const Text* text, int x, int y, double scale) { @@ -421,9 +422,11 @@ void renderCenteredTextBackground(const Text* text, int x, int y, double scale) SDL_RenderFillRect(sdlRenderer, &dst); } void renderId() { + GameContext& ctx = getGameContext(); const int powerful = getPowerfulPlayer(); - for (int i = 0; i < playersCount; i++) { - const auto& snake = spriteSnake[i]; + const int playerCount = ctx.entityManager.playerCount(); + for (int i = 0; i < playerCount; i++) { + const auto& snake = ctx.entityManager.getSnake(i); if (!snake || snake->sprites().empty()) { continue; } @@ -449,6 +452,7 @@ void renderCountDown() { } } void renderInfo() { + GameContext& ctx = getGameContext(); SDL_Renderer* sdlRenderer = renderer(); if (!sdlRenderer) { return; @@ -460,9 +464,10 @@ void renderInfo() { renderText(stageText.get(), startX, startY, 1); } startY += lineGap; - for (int i = 0; i < playersCount; i++) { + const int playerCount = ctx.entityManager.playerCount(); + for (int i = 0; i < playerCount; i++) { char buf[1 << 8]; - const auto& snake = spriteSnake[i]; + const auto& snake = ctx.entityManager.getSnake(i); if (!snake) { continue; } @@ -475,11 +480,12 @@ void renderInfo() { } startY += lineGap; } - if (playersCount == 1) { + if (playerCount == 1) { extern int GAME_WIN_NUM; char buf[1 << 8]; - const int remaining = spriteSnake[0] - ? GAME_WIN_NUM - spriteSnake[0]->num() + const auto& snake0 = ctx.entityManager.getSnake(0); + const int remaining = snake0 + ? GAME_WIN_NUM - snake0->num() : GAME_WIN_NUM; sprintf(buf, "Find %d more heros!", remaining > 0 ? remaining : 0); if (taskText) { diff --git a/src/sprite.cpp b/src/sprite.cpp index 5d24dae..32c9e06 100644 --- a/src/sprite.cpp +++ b/src/sprite.cpp @@ -20,66 +20,76 @@ const PositionBufferSlot& PositionBuffer::at(int index) const { } Sprite::Sprite(const Sprite& model, int x, int y) - : x_(x), - y_(y), - hp_(model.hp_), - totalHp_(model.totalHp_), - weapon_(model.weapon_), - animation_(model.animation_ ? std::make_shared(*model.animation_) - : nullptr), - face_(model.face_), - direction_(model.direction_), - lastAttack_(model.lastAttack_), - dropRate_(model.dropRate_) { - if (animation_) { - animation_->setPosition(x_, y_); + : transform_(x, y, model.transform_.direction()), + health_(model.health_), + render_(model.render_.animation() + ? std::make_shared(*model.render_.animation()) + : nullptr), + combat_(model.combat_), + positionBuffer_() { + if (render_.hasAnimation()) { + render_.updatePosition(x, y); } } -int Sprite::x() const { return x_; } -int Sprite::y() const { return y_; } -int Sprite::hp() const { return hp_; } -int Sprite::totalHp() const { return totalHp_; } -Weapon* Sprite::weapon() const { return weapon_; } -std::shared_ptr Sprite::animation() const { return animation_; } -Direction Sprite::face() const { return face_; } -Direction Sprite::direction() const { return direction_; } -int Sprite::lastAttack() const { return lastAttack_; } -double Sprite::dropRate() const { return dropRate_; } +// TransformComponent accessors +int Sprite::x() const { return transform_.x(); } +int Sprite::y() const { return transform_.y(); } +Direction Sprite::face() const { return transform_.face(); } +Direction Sprite::direction() const { return transform_.direction(); } void Sprite::setPosition(int x, int y) { - x_ = x; - y_ = y; - if (animation_) { - animation_->setPosition(x_, y_); - } + transform_.setPosition(x, y); + render_.updatePosition(x, y); +} + +void Sprite::setFace(Direction face) { transform_.setFace(face); } +void Sprite::setDirection(Direction direction) { + transform_.setDirection(direction); } -void Sprite::setHp(int hp) { hp_ = hp; } -void Sprite::setTotalHp(int totalHp) { totalHp_ = totalHp; } -void Sprite::setWeapon(Weapon* weapon) { weapon_ = weapon; } +// HealthComponent accessors +int Sprite::hp() const { return health_.hp(); } +int Sprite::totalHp() const { return health_.totalHp(); } +void Sprite::setHp(int hp) { health_.setHp(hp); } +void Sprite::setTotalHp(int totalHp) { health_.setTotalHp(totalHp); } + +// RenderComponent accessors +std::shared_ptr Sprite::animation() const { + return render_.animation(); +} void Sprite::setAnimation(const std::shared_ptr& animation) { - animation_ = animation; + render_.setAnimation(animation); } -void Sprite::setFace(Direction face) { face_ = face; } -void Sprite::setDirection(Direction direction) { direction_ = direction; } -void Sprite::setLastAttack(int lastAttack) { lastAttack_ = lastAttack; } -void Sprite::setDropRate(double dropRate) { dropRate_ = dropRate; } +// CombatComponent accessors +Weapon* Sprite::weapon() const { return combat_.weapon(); } +int Sprite::lastAttack() const { return combat_.lastAttack(); } +double Sprite::dropRate() const { return combat_.dropRate(); } +void Sprite::setWeapon(Weapon* weapon) { combat_.setWeapon(weapon); } +void Sprite::setLastAttack(int lastAttack) { combat_.setLastAttack(lastAttack); } +void Sprite::setDropRate(double dropRate) { combat_.setDropRate(dropRate); } + +// PositionBuffer PositionBuffer& Sprite::positionBuffer() { return positionBuffer_; } const PositionBuffer& Sprite::positionBuffer() const { return positionBuffer_; } void Sprite::enqueueDirectionChange(Direction newDirection, const std::shared_ptr& next) { - if (direction_ == newDirection) { - return; - } - direction_ = newDirection; - if (newDirection == Direction::Left || newDirection == Direction::Right) { - face_ = newDirection; - } + transform_.enqueueDirectionChange(newDirection, positionBuffer_); if (next) { - PositionBufferSlot slot{x_, y_, direction_}; + PositionBufferSlot slot{transform_.x(), transform_.y(), + transform_.direction()}; next->positionBuffer().push(slot); } } + +// Direct component access +TransformComponent& Sprite::transform() { return transform_; } +const TransformComponent& Sprite::transform() const { return transform_; } +HealthComponent& Sprite::health() { return health_; } +const HealthComponent& Sprite::health() const { return health_; } +RenderComponent& Sprite::render() { return render_; } +const RenderComponent& Sprite::render() const { return render_; } +CombatComponent& Sprite::combat() { return combat_; } +const CombatComponent& Sprite::combat() const { return combat_; } diff --git a/src/sprite.h b/src/sprite.h index 6a37207..be72bba 100644 --- a/src/sprite.h +++ b/src/sprite.h @@ -2,6 +2,7 @@ #define SNAKE_SPRITE_H_ #include "adt.h" +#include "component.h" #include "types.h" #include "weapon.h" @@ -30,6 +31,10 @@ class PositionBuffer { int size_ = 0; }; +// ============================================================================ +// Sprite - Composes TransformComponent, HealthComponent, RenderComponent, +// and CombatComponent to avoid massive base class +// ============================================================================ class Sprite { public: Sprite(); @@ -40,45 +45,56 @@ class Sprite { Sprite& operator=(Sprite&&) noexcept = default; ~Sprite() = default; + // TransformComponent accessors int x() const; int y() const; - int hp() const; - int totalHp() const; - Weapon* weapon() const; - std::shared_ptr animation() const; Direction face() const; Direction direction() const; - int lastAttack() const; - double dropRate() const; - void setPosition(int x, int y); + void setFace(Direction face); + void setDirection(Direction direction); + + // HealthComponent accessors + int hp() const; + int totalHp() const; void setHp(int hp); void setTotalHp(int totalHp); - void setWeapon(Weapon* weapon); + + // RenderComponent accessors + std::shared_ptr animation() const; void setAnimation(const std::shared_ptr& animation); - void setFace(Direction face); - void setDirection(Direction direction); + + // CombatComponent accessors + Weapon* weapon() const; + int lastAttack() const; + double dropRate() const; + void setWeapon(Weapon* weapon); void setLastAttack(int lastAttack); void setDropRate(double dropRate); + // PositionBuffer for chained movement PositionBuffer& positionBuffer(); const PositionBuffer& positionBuffer() const; void enqueueDirectionChange(Direction newDirection, const std::shared_ptr& next); + // Direct component access for advanced usage + TransformComponent& transform(); + const TransformComponent& transform() const; + HealthComponent& health(); + const HealthComponent& health() const; + RenderComponent& render(); + const RenderComponent& render() const; + CombatComponent& combat(); + const CombatComponent& combat() const; + private: - int x_ = 0; - int y_ = 0; - int hp_ = 0; - int totalHp_ = 0; - Weapon* weapon_ = nullptr; - std::shared_ptr animation_{}; - Direction face_ = Direction::Right; - Direction direction_ = Direction::Right; + TransformComponent transform_{}; + HealthComponent health_{}; + RenderComponent render_{}; + CombatComponent combat_{}; PositionBuffer positionBuffer_{}; - int lastAttack_ = 0; - double dropRate_ = 0.0; }; #endif diff --git a/src/ui.cpp b/src/ui.cpp index 02a2ad5..1de220f 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -3,11 +3,11 @@ #include #include #include -#include -#include -#include -#include + #include +#include +#include +#include #include #include "audio.h" @@ -31,19 +31,266 @@ extern Effect effects[]; int cursorPos; namespace { -Text* createUiText(const std::string& text, SDL_Color color) { +std::unique_ptr createUiText(const std::string& text, SDL_Color color) { SDL_Renderer* sdlRenderer = renderer(); TTF_Font* ttfFont = font(); if (!sdlRenderer || !ttfFont) { return nullptr; } - return new Text(text, color, sdlRenderer, ttfFont); + return std::make_unique(text, color, sdlRenderer, ttfFont); } -void destroyUiText(Text* text) { - delete text; -} +class GameStateManager; + +class GameState { + public: + GameState() = default; + GameState(const GameState&) = delete; + GameState& operator=(const GameState&) = delete; + GameState(GameState&&) = default; + GameState& operator=(GameState&&) = default; + virtual ~GameState() = default; + + virtual void handleInput(GameStateManager& manager) = 0; + virtual void update(GameStateManager& manager) = 0; + virtual void render(GameStateManager& manager) = 0; +}; + +class GameStateManager { + public: + GameStateManager() = default; + GameStateManager(const GameStateManager&) = delete; + GameStateManager& operator=(const GameStateManager&) = delete; + GameStateManager(GameStateManager&&) = default; + GameStateManager& operator=(GameStateManager&&) = default; + ~GameStateManager() = default; + + void setState(std::unique_ptr state) { + currentState_ = std::move(state); + } + + void clearState() { + currentState_.reset(); + nextState_.reset(); + } + + void setNextState(std::unique_ptr state) { + nextState_ = std::move(state); + } + + void run() { + while (currentState_) { + currentState_->handleInput(*this); + currentState_->update(*this); + currentState_->render(*this); + if (nextState_) { + currentState_ = std::move(nextState_); + } + } + } + + private: + std::unique_ptr currentState_{}; + std::unique_ptr nextState_{}; +}; + +class ExitState final : public GameState { + public: + void handleInput(GameStateManager& manager) override { + manager.clearState(); + } + void update(GameStateManager&) override {} + void render(GameStateManager&) override {} +}; + +class MainMenuState final : public GameState { + public: + void handleInput(GameStateManager& manager) override; + void update(GameStateManager&) override {} + void render(GameStateManager&) override {} +}; + +class LocalRankListState final : public GameState { + public: + void handleInput(GameStateManager& manager) override { + localRankListUi(); + manager.setNextState(std::make_unique()); + } + void update(GameStateManager&) override {} + void render(GameStateManager&) override {} +}; + +class LanGameState final : public GameState { + public: + void handleInput(GameStateManager& manager) override { + launchLanGame(); + manager.setNextState(std::make_unique()); + } + void update(GameStateManager&) override {} + void render(GameStateManager&) override {} +}; + +class LocalGameState final : public GameState { + public: + explicit LocalGameState(int localPlayers) : localPlayers_(localPlayers) {} + + void handleInput(GameStateManager& manager) override { + launchLocalGame(localPlayers_); + manager.setNextState(std::make_unique()); + } + void update(GameStateManager&) override {} + void render(GameStateManager&) override {} + + private: + int localPlayers_ = 1; +}; + +class ChooseLevelState final : public GameState { + public: + explicit ChooseLevelState(int localPlayers) : localPlayers_(localPlayers) {} + + void handleInput(GameStateManager& manager) override { + if (!chooseLevelUi()) { + manager.setNextState(std::make_unique()); + return; + } + manager.setNextState(std::make_unique(localPlayers_)); + } + void update(GameStateManager&) override {} + void render(GameStateManager&) override {} + + private: + int localPlayers_ = 1; +}; + +class ChooseModeState final : public GameState { + public: + void handleInput(GameStateManager& manager) override { + int mode = chooseOnLanUi(); + if (mode == 0) { + manager.setNextState(std::make_unique(2)); + return; + } + manager.setNextState(std::make_unique()); + } + void update(GameStateManager&) override {} + void render(GameStateManager&) override {} +}; + +void MainMenuState::handleInput(GameStateManager& manager) { + baseUi(30, 12); + playBgm(0); + const int startY = SCREEN_HEIGHT / 2 - 70; + const int startX = SCREEN_WIDTH / 5 + 32; + createAndPushAnimation(animationsList[RENDER_LIST_UI_ID], + &textures[RES_TITLE], nullptr, LoopType::Infinite, 80, + SCREEN_WIDTH / 2, 280, SDL_FLIP_NONE, 0, At::Center); + createAndPushAnimation(animationsList[RENDER_LIST_SPRITE_ID], + &textures[RES_KNIGHT_M], nullptr, LoopType::Infinite, + SPRITE_ANIMATION_DURATION, startX, startY, + SDL_FLIP_NONE, 0, At::BottomCenter); + auto swordFx = createAndPushAnimation( + animationsList[RENDER_LIST_EFFECT_ID], &textures[RES_SwordFx], nullptr, + LoopType::Infinite, SPRITE_ANIMATION_DURATION, + startX + UI_MAIN_GAP_ALT * 2, startY - 32, SDL_FLIP_NONE, 0, + At::BottomCenter); + if (swordFx) { + swordFx->setScaled(false); + } + createAndPushAnimation(animationsList[RENDER_LIST_SPRITE_ID], + &textures[RES_CHORT], nullptr, LoopType::Infinite, + SPRITE_ANIMATION_DURATION, + startX + UI_MAIN_GAP_ALT * 2, startY - 32, + SDL_FLIP_HORIZONTAL, 0, At::BottomCenter); + + const int elfX = startX + UI_MAIN_GAP_ALT * (6 + 2 * randDouble()); + const int elfY = startY + UI_MAIN_GAP * (1 + randDouble()); + createAndPushAnimation(animationsList[RENDER_LIST_SPRITE_ID], + &textures[RES_ELF_M], nullptr, LoopType::Infinite, + SPRITE_ANIMATION_DURATION, elfX, elfY, + SDL_FLIP_HORIZONTAL, 0, At::BottomCenter); + createAndPushAnimation(animationsList[RENDER_LIST_EFFECT_ID], + &textures[RES_HALO_EXPLOSION2], nullptr, + LoopType::Infinite, SPRITE_ANIMATION_DURATION, + elfX - UI_MAIN_GAP * 1.5, elfY, SDL_FLIP_NONE, 0, + At::BottomCenter); + createAndPushAnimation(animationsList[RENDER_LIST_SPRITE_ID], + &textures[RES_ZOMBIE], nullptr, LoopType::Infinite, + SPRITE_ANIMATION_DURATION, elfX - UI_MAIN_GAP * 1.5, + elfY, SDL_FLIP_NONE, 0, At::BottomCenter); + + const int wizardX = elfX - UI_MAIN_GAP_ALT * (1 + 2 * randDouble()); + const int wizardY = elfY + UI_MAIN_GAP * (2 + randDouble()); + createAndPushAnimation(animationsList[RENDER_LIST_SPRITE_ID], + &textures[RES_WIZZARD_M], nullptr, LoopType::Infinite, + SPRITE_ANIMATION_DURATION, wizardX, wizardY, + SDL_FLIP_NONE, 0, At::BottomCenter); + createAndPushAnimation(animationsList[RENDER_LIST_EFFECT_ID], + &textures[RES_FIREBALL], nullptr, LoopType::Infinite, + SPRITE_ANIMATION_DURATION, wizardX + UI_MAIN_GAP, + wizardY, SDL_FLIP_NONE, 0, At::BottomCenter); + + const int lizardX = wizardX + UI_MAIN_GAP_ALT * (18 + 4 * randDouble()); + const int lizardY = wizardY - UI_MAIN_GAP * (1 + 3 * randDouble()); + createAndPushAnimation(animationsList[RENDER_LIST_SPRITE_ID], + &textures[RES_LIZARD_M], nullptr, LoopType::Infinite, + SPRITE_ANIMATION_DURATION, lizardX, lizardY, + SDL_FLIP_NONE, 0, At::BottomCenter); + createAndPushAnimation( + animationsList[RENDER_LIST_EFFECT_ID], &textures[RES_CLAWFX2], nullptr, + LoopType::Infinite, SPRITE_ANIMATION_DURATION, lizardX, + lizardY - UI_MAIN_GAP + 16, SDL_FLIP_NONE, 0, At::BottomCenter); + createAndPushAnimation( + animationsList[RENDER_LIST_SPRITE_ID], &textures[RES_MUDDY], nullptr, + LoopType::Infinite, SPRITE_ANIMATION_DURATION, lizardX, + lizardY - UI_MAIN_GAP, SDL_FLIP_HORIZONTAL, 0, At::BottomCenter); + + createAndPushAnimation( + animationsList[RENDER_LIST_EFFECT_ID], &textures[RES_CLAWFX2], nullptr, + LoopType::Infinite, SPRITE_ANIMATION_DURATION, lizardX + UI_MAIN_GAP, + lizardY - UI_MAIN_GAP + 16, SDL_FLIP_NONE, 0, At::BottomCenter); + createAndPushAnimation( + animationsList[RENDER_LIST_SPRITE_ID], &textures[RES_SWAMPY], nullptr, + LoopType::Infinite, SPRITE_ANIMATION_DURATION, lizardX + UI_MAIN_GAP, + lizardY - UI_MAIN_GAP, SDL_FLIP_HORIZONTAL, 0, At::BottomCenter); + + createAndPushAnimation(animationsList[RENDER_LIST_EFFECT_ID], + &textures[RES_CLAWFX2], nullptr, LoopType::Infinite, + SPRITE_ANIMATION_DURATION, lizardX + UI_MAIN_GAP, + lizardY + 16, SDL_FLIP_NONE, 0, At::BottomCenter); + createAndPushAnimation(animationsList[RENDER_LIST_SPRITE_ID], + &textures[RES_SWAMPY], nullptr, LoopType::Infinite, + SPRITE_ANIMATION_DURATION, lizardX + UI_MAIN_GAP, + lizardY, SDL_FLIP_HORIZONTAL, 0, At::BottomCenter); + + const int optsNum = 4; + std::vector opts; + opts.reserve(optsNum); + for (int i = 0; i < optsNum; i++) { + opts.push_back(texts + i + 6); + } + const int opt = chooseOptions(optsNum, opts.data()); + + blackout(); + clearRenderer(); + switch (opt) { + case 0: + manager.setNextState(std::make_unique(1)); + break; + case 1: + manager.setNextState(std::make_unique()); + break; + case 2: + manager.setNextState(std::make_unique()); + break; + case 3: + default: + manager.setNextState(std::make_unique()); + break; + } } +} // namespace + bool moveCursor(int optsNum) { SDL_Event e; bool quit = false; @@ -84,9 +331,9 @@ int chooseOptions(int optionsNum, Text** options) { auto player = std::make_shared(2, 0, PlayerType::Local); appendSpriteToSnake(player, SPRITE_KNIGHT, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, Direction::Up); - int lineGap = FONT_SIZE + FONT_SIZE / 2, - totalHeight = lineGap * (optionsNum - 1); - int startY = (SCREEN_HEIGHT - totalHeight) / 2; + const int lineGap = FONT_SIZE + FONT_SIZE / 2; + const int totalHeight = lineGap * (optionsNum - 1); + const int startY = (SCREEN_HEIGHT - totalHeight) / 2; while (!moveCursor(optionsNum)) { auto head = player->sprites().empty() ? nullptr : player->sprites().front(); if (head && head->animation()) { @@ -117,11 +364,16 @@ void baseUi(int w, int h) { } bool chooseLevelUi() { baseUi(30, 12); - int optsNum = 3; - Text** opts = static_cast(malloc(sizeof(Text*) * optsNum)); - for (int i = 0; i < optsNum; i++) opts[i] = texts + i + 10; - int opt = chooseOptions(optsNum, opts); - if (opt != optsNum) setLevel(opt); + const int optsNum = 3; + std::vector opts; + opts.reserve(optsNum); + for (int i = 0; i < optsNum; i++) { + opts.push_back(texts + i + 10); + } + const int opt = chooseOptions(optsNum, opts.data()); + if (opt != optsNum) { + setLevel(opt); + } clearRenderer(); return opt != optsNum; } @@ -137,42 +389,43 @@ void launchLocalGame(int localPlayerNum) { for (int i = 0; i < localPlayerNum; i++) updateLocalRanklist(rawScores[i]); } int rangeOptions(int start, int end) { - int optsNum = end - start + 1; - Text** opts = static_cast(malloc(sizeof(Text*) * optsNum)); - for (int i = 0; i < optsNum; i++) opts[i] = texts + i + start; - int opt = chooseOptions(optsNum, opts); - free(opts); + const int optsNum = end - start + 1; + std::vector opts; + opts.reserve(optsNum); + for (int i = 0; i < optsNum; i++) { + opts.push_back(texts + i + start); + } + const int opt = chooseOptions(optsNum, opts.data()); return opt; } -char* inputUi() { - const int MAX_LEN = 30; +std::optional inputUi() { + constexpr int kMaxLen = 30; baseUi(20, 10); - char* ret = static_cast(malloc(MAX_LEN)); - int retLen = 0; - memset(ret, 0, MAX_LEN); + std::string ret; + ret.reserve(kMaxLen); extern SDL_Color WHITE; - Text* text = NULL; - Text* placeholder = createUiText("Enter IP", WHITE); + std::unique_ptr text(nullptr); + std::unique_ptr placeholder(createUiText("Enter IP", WHITE)); SDL_StartTextInput(); SDL_Event e; bool quit = false; bool finished = false; while (!quit && !finished) { - const Text* displayText = NULL; - if (ret[0]) { + const Text* displayText = nullptr; + if (!ret.empty()) { if (text) { text->setText(ret, renderer(), font()); } else { text = createUiText(ret, WHITE); } - displayText = text; + displayText = text.get(); } else { - displayText = placeholder; + displayText = placeholder.get(); } renderCenteredText(displayText, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, 2); if (SDL_Renderer* sdlRenderer = renderer()) { @@ -187,25 +440,25 @@ char* inputUi() { break; } else if (e.type == SDL_KEYDOWN) { if (e.key.keysym.sym == SDLK_BACKSPACE) { - if (retLen) ret[--retLen] = 0; + if (!ret.empty()) { + ret.pop_back(); + } } else if (e.key.keysym.sym == SDLK_RETURN) { finished = true; break; } } else if (e.type == SDL_TEXTINPUT) { - strcpy(ret + retLen, e.text.text); - retLen += strlen(e.text.text); + if (ret.size() < kMaxLen) { + ret.append(e.text.text); + } } } } SDL_StopTextInput(); - destroyUiText(placeholder); - destroyUiText(text); if (quit) { - free(ret); - return NULL; + return std::nullopt; } return ret; @@ -219,10 +472,11 @@ void launchLanGame() { if (opt == 0) { hostGame(); } else { - char* ip = inputUi(); - if (ip == NULL) return; - joinGame(ip, LAN_LISTEN_PORT); - free(ip); + const auto ip = inputUi(); + if (!ip.has_value()) { + return; + } + joinGame(ip->c_str(), LAN_LISTEN_PORT); } } @@ -233,162 +487,35 @@ int chooseOnLanUi() { return opt; } -void mainUi() { - baseUi(30, 12); - playBgm(0); - int startY = SCREEN_HEIGHT / 2 - 70; - int startX = SCREEN_WIDTH / 5 + 32; - createAndPushAnimation(animationsList[RENDER_LIST_UI_ID], - &textures[RES_TITLE], nullptr, LoopType::Infinite, 80, - SCREEN_WIDTH / 2, 280, SDL_FLIP_NONE, 0, At::Center); - createAndPushAnimation(animationsList[RENDER_LIST_SPRITE_ID], - &textures[RES_KNIGHT_M], nullptr, LoopType::Infinite, - SPRITE_ANIMATION_DURATION, startX, startY, - SDL_FLIP_NONE, 0, At::BottomCenter); - auto swordFx = createAndPushAnimation( - animationsList[RENDER_LIST_EFFECT_ID], &textures[RES_SwordFx], nullptr, - LoopType::Infinite, SPRITE_ANIMATION_DURATION, - startX + UI_MAIN_GAP_ALT * 2, startY - 32, SDL_FLIP_NONE, 0, - At::BottomCenter); - if (swordFx) { - swordFx->setScaled(false); - } - createAndPushAnimation(animationsList[RENDER_LIST_SPRITE_ID], - &textures[RES_CHORT], nullptr, LoopType::Infinite, - SPRITE_ANIMATION_DURATION, - startX + UI_MAIN_GAP_ALT * 2, startY - 32, - SDL_FLIP_HORIZONTAL, 0, At::BottomCenter); - - startX += UI_MAIN_GAP_ALT * (6 + 2 * randDouble()); - startY += UI_MAIN_GAP * (1 + randDouble()); - createAndPushAnimation(animationsList[RENDER_LIST_SPRITE_ID], - &textures[RES_ELF_M], nullptr, LoopType::Infinite, - SPRITE_ANIMATION_DURATION, startX, startY, - SDL_FLIP_HORIZONTAL, 0, At::BottomCenter); - createAndPushAnimation(animationsList[RENDER_LIST_EFFECT_ID], - &textures[RES_HALO_EXPLOSION2], nullptr, - LoopType::Infinite, SPRITE_ANIMATION_DURATION, - startX - UI_MAIN_GAP * 1.5, startY, SDL_FLIP_NONE, 0, - At::BottomCenter); - createAndPushAnimation(animationsList[RENDER_LIST_SPRITE_ID], - &textures[RES_ZOMBIE], nullptr, LoopType::Infinite, - SPRITE_ANIMATION_DURATION, startX - UI_MAIN_GAP * 1.5, - startY, SDL_FLIP_NONE, 0, At::BottomCenter); - - startX -= UI_MAIN_GAP_ALT * (1 + 2 * randDouble()); - startY += UI_MAIN_GAP * (2 + randDouble()); - createAndPushAnimation(animationsList[RENDER_LIST_SPRITE_ID], - &textures[RES_WIZZARD_M], nullptr, LoopType::Infinite, - SPRITE_ANIMATION_DURATION, startX, startY, - SDL_FLIP_NONE, 0, At::BottomCenter); - createAndPushAnimation(animationsList[RENDER_LIST_EFFECT_ID], - &textures[RES_FIREBALL], nullptr, LoopType::Infinite, - SPRITE_ANIMATION_DURATION, startX + UI_MAIN_GAP, - startY, SDL_FLIP_NONE, 0, At::BottomCenter); - - startX += UI_MAIN_GAP_ALT * (18 + 4 * randDouble()); - startY -= UI_MAIN_GAP * (1 + 3 * randDouble()); - createAndPushAnimation(animationsList[RENDER_LIST_SPRITE_ID], - &textures[RES_LIZARD_M], nullptr, LoopType::Infinite, - SPRITE_ANIMATION_DURATION, startX, startY, - SDL_FLIP_NONE, 0, At::BottomCenter); - createAndPushAnimation( - animationsList[RENDER_LIST_EFFECT_ID], &textures[RES_CLAWFX2], nullptr, - LoopType::Infinite, SPRITE_ANIMATION_DURATION, startX, - startY - UI_MAIN_GAP + 16, SDL_FLIP_NONE, 0, At::BottomCenter); - createAndPushAnimation( - animationsList[RENDER_LIST_SPRITE_ID], &textures[RES_MUDDY], nullptr, - LoopType::Infinite, SPRITE_ANIMATION_DURATION, startX, - startY - UI_MAIN_GAP, SDL_FLIP_HORIZONTAL, 0, At::BottomCenter); - - createAndPushAnimation( - animationsList[RENDER_LIST_EFFECT_ID], &textures[RES_CLAWFX2], nullptr, - LoopType::Infinite, SPRITE_ANIMATION_DURATION, startX + UI_MAIN_GAP, - startY - UI_MAIN_GAP + 16, SDL_FLIP_NONE, 0, At::BottomCenter); - createAndPushAnimation( - animationsList[RENDER_LIST_SPRITE_ID], &textures[RES_SWAMPY], nullptr, - LoopType::Infinite, SPRITE_ANIMATION_DURATION, startX + UI_MAIN_GAP, - startY - UI_MAIN_GAP, SDL_FLIP_HORIZONTAL, 0, At::BottomCenter); - - createAndPushAnimation(animationsList[RENDER_LIST_EFFECT_ID], - &textures[RES_CLAWFX2], nullptr, LoopType::Infinite, - SPRITE_ANIMATION_DURATION, startX + UI_MAIN_GAP, - startY + 16, SDL_FLIP_NONE, 0, At::BottomCenter); - createAndPushAnimation(animationsList[RENDER_LIST_SPRITE_ID], - &textures[RES_SWAMPY], nullptr, LoopType::Infinite, - SPRITE_ANIMATION_DURATION, startX + UI_MAIN_GAP, - startY, SDL_FLIP_HORIZONTAL, 0, At::BottomCenter); - /* - startX = SCREEN_WIDTH/3*2; - startY = SCREEN_HEIGHT/3 + 10; - int colNum = 8; - for (int i = RES_TINY_ZOMBIE; i <= RES_CHORT; i+=2) { - int col = (i - RES_TINY_ZOMBIE)%colNum; - int row = (i - RES_TINY_ZOMBIE)/colNum; - createAndPushAnimation(&animationsList[RENDER_LIST_SPRITE_ID], - &textures[i], NULL, LOOP_INFI, - SPRITE_ANIMATION_DURATION, startX + - col*UI_MAIN_GAP_ALT, startY + row*UI_MAIN_GAP, SDL_FLIP_HORIZONTAL, 0, - AT_BOTTOM_CENTER); - } - for (int i = RES_BIG_ZOMBIE; i <= RES_BIG_DEMON; i+=2) { - createAndPushAnimation(&animationsList[RENDER_LIST_SPRITE_ID], - &textures[i], NULL, LOOP_INFI, - SPRITE_ANIMATION_DURATION, startX + - (i-RES_BIG_ZOMBIE)*UNIT, startY + 200, SDL_FLIP_HORIZONTAL, 0, - AT_BOTTOM_CENTER); - } - */ - int optsNum = 4; - Text** opts = static_cast(malloc(sizeof(Text*) * optsNum)); - for (int i = 0; i < optsNum; i++) opts[i] = texts + i + 6; - int opt = chooseOptions(optsNum, opts); - free(opts); +namespace { +void runUiStateMachine() { + GameStateManager manager; + manager.setState(std::make_unique()); + manager.run(); +} +} // namespace - blackout(); - clearRenderer(); - int lan; - switch (opt) { - case 0: - if (!chooseLevelUi()) break; - launchLocalGame(1); - break; - case 1: - lan = chooseOnLanUi(); - if (lan == 0) { - if (!chooseLevelUi()) break; - launchLocalGame(2); - } else if (lan == 1) { - launchLanGame(); - } - break; - case 2: - localRankListUi(); - break; - case 3: - break; - } - if (opt == optsNum) return; - if (opt != 3) { - mainUi(); - } +void mainUi() { + runUiStateMachine(); } void rankListUi(int count, Score** scores) { baseUi(30, 12 + MAX(0, count - 4)); playBgm(0); - Text** opts = static_cast(malloc(sizeof(Text*) * count)); + std::vector> ownedTexts; + ownedTexts.reserve(count); + std::vector opts; + opts.reserve(count); char buf[1 << 8]; for (int i = 0; i < count; i++) { sprintf(buf, "Score: %-6.0lf Got: %-6d Kill: %-6d Damage: %-6d Stand: %-6d", scores[i]->rank(), scores[i]->got(), scores[i]->killed(), scores[i]->damage(), scores[i]->stand()); - opts[i] = createUiText(buf, WHITE); + ownedTexts.push_back(createUiText(buf, WHITE)); + opts.push_back(ownedTexts.back().get()); } - chooseOptions(count, opts); + chooseOptions(count, opts.data()); - for (int i = 0; i < count; i++) destroyUiText(opts[i]); - free(opts); blackout(); clearRenderer(); } diff --git a/src/ui.h b/src/ui.h index 3fea828..b84095c 100644 --- a/src/ui.h +++ b/src/ui.h @@ -1,12 +1,21 @@ #ifndef SNAKE_UI_H_ #define SNAKE_UI_H_ #include + +#include +#include + #include "types.h" #define UI_MAIN_GAP 40 #define UI_MAIN_GAP_ALT 22 int chooseOptions(int optsNum, Text** options); -void baseUi(int,int); +void baseUi(int w, int h); void mainUi(); -void rankListUi(int,Score**); +void rankListUi(int count, Score** scores); void localRankListUi(); +std::optional inputUi(); +void launchLanGame(); +void launchLocalGame(int localPlayerNum); +bool chooseLevelUi(); +int chooseOnLanUi(); #endif diff --git a/src/weapon.cpp b/src/weapon.cpp index 5b23636..22ef45e 100644 --- a/src/weapon.cpp +++ b/src/weapon.cpp @@ -2,6 +2,11 @@ #include +#include "ai.h" +#include "audio.h" +#include "bullet.h" +#include "game.h" +#include "helper.h" #include "render.h" #include "res.h" #include "types.h" @@ -31,6 +36,183 @@ void initWeapon(Weapon& weapon, int birthTextureId, int deathTextureId, } } // namespace +namespace { +void applyWeaponDamage(const std::shared_ptr& src, + const std::shared_ptr& dest, + const std::shared_ptr& target, + const Weapon& weapon) { + GameContext& ctx = getGameContext(); + if (!dest || !target) { + return; + } + double calcDamage = weapon.damage(); + if (dest->buffs()[BUFF_FROZEN]) { + calcDamage *= GAME_FROZEN_DAMAGE_K; + } + if (src && src->team() != GAME_MONSTERS_TEAM) { + if (src->buffs()[BUFF_ATTACK]) { + calcDamage *= GAME_BUFF_ATTACK_K; + } + } + if (dest->team() != GAME_MONSTERS_TEAM) { + if (dest->buffs()[BUFF_DEFFENCE]) { + calcDamage /= GAME_BUFF_DEFENSE_K; + } + } + target->setHp(target->hp() - static_cast(calcDamage)); + if (src) { + src->score()->addDamage(static_cast(calcDamage)); + if (target->hp() <= 0) { + src->score()->addKilled(1); + } + } + dest->score()->addStand(weapon.damage()); + ctx.buffManager.invokeWeaponBuff(src, weapon, dest, weapon.damage()); +} + +class SwordBehavior final : public WeaponBehavior { + public: + WeaponAttackResult attack(const Weapon& weapon, + const WeaponAttackContext& context) const override { + if (!context.attacker || !context.target || !context.targetSprite || + !context.attackerSprite) { + return {}; + } + const double rad = + atan2(context.targetSprite->y() - context.attackerSprite->y(), + context.targetSprite->x() - context.attackerSprite->x()); + if (const auto& deathAnimation = weapon.deathAnimation()) { + auto effect = std::make_shared(*deathAnimation); + bindAnimationToSprite(effect, context.targetSprite, false); + if (effect->angle() != -1) { + effect->setAngle(rad * 180 / PI); + } + pushAnimationToRender(RENDER_LIST_EFFECT_ID, effect); + } + applyWeaponDamage(context.attacker, context.target, context.targetSprite, + weapon); + return {.attacked = true}; + } + + void onAttack(const Weapon& weapon, + const std::shared_ptr& sprite) const override { + if (!sprite) { + return; + } + if (const auto& birthAnimation = weapon.birthAnimation()) { + auto effect = std::make_shared(*birthAnimation); + bindAnimationToSprite(effect, sprite, true); + effect->setAt(At::BottomCenter); + pushAnimationToRender(RENDER_LIST_EFFECT_ID, effect); + } + playAudio(weapon.deathAudio()); + sprite->setLastAttack(static_cast(renderFrames())); + } + + bool allowMultiTarget() const override { return true; } + bool allowAreaImpact() const override { return false; } + + void applyImpact(const Weapon& weapon, const std::shared_ptr&, + const std::shared_ptr& hitSnake, + const std::shared_ptr& hitSprite) const override { + if (!hitSnake || !hitSprite) { + return; + } + applyWeaponDamage(nullptr, hitSnake, hitSprite, weapon); + } + + void applyAreaImpact(const Weapon& weapon, const std::shared_ptr&, + const std::shared_ptr& hitSnake, + const std::shared_ptr& hitSprite) const override { + if (!hitSnake || !hitSprite) { + return; + } + applyWeaponDamage(nullptr, hitSnake, hitSprite, weapon); + } +}; + +class RangedBehavior final : public WeaponBehavior { + public: + explicit RangedBehavior(bool multiShot) : multiShot_(multiShot) {} + + WeaponAttackResult attack(const Weapon& weapon, + const WeaponAttackContext& context) const override { + if (!context.attacker || !context.attackerSprite || !context.targetSprite) { + return {}; + } + const double rad = + atan2(context.targetSprite->y() - context.attackerSprite->y(), + context.targetSprite->x() - context.attackerSprite->x()); + auto bullet = std::make_shared(context.attacker, + const_cast(&weapon), + context.attackerSprite->x(), + context.attackerSprite->y(), rad, + context.attacker->team(), + weapon.flyAnimation()); + GameContext& ctx = getGameContext(); + ctx.entityManager.addBullet(bullet); + pushAnimationToRender(RENDER_LIST_EFFECT_ID, bullet->animation()); + return {.attacked = true}; + } + + void onAttack(const Weapon& weapon, + const std::shared_ptr& sprite) const override { + if (!sprite) { + return; + } + if (const auto& birthAnimation = weapon.birthAnimation()) { + auto effect = std::make_shared(*birthAnimation); + bindAnimationToSprite(effect, sprite, true); + effect->setAt(At::BottomCenter); + pushAnimationToRender(RENDER_LIST_EFFECT_ID, effect); + } + playAudio(weapon.birthAudio()); + sprite->setLastAttack(static_cast(renderFrames())); + } + + bool allowMultiTarget() const override { return multiShot_; } + bool allowAreaImpact() const override { return true; } + + void applyImpact(const Weapon& weapon, const std::shared_ptr& bullet, + const std::shared_ptr& hitSnake, + const std::shared_ptr& hitSprite) const override { + if (!bullet || !hitSnake || !hitSprite) { + return; + } + applyWeaponDamage(bullet->owner(), hitSnake, hitSprite, weapon); + } + + void applyAreaImpact(const Weapon& weapon, const std::shared_ptr& bullet, + const std::shared_ptr& hitSnake, + const std::shared_ptr& hitSprite) const override { + if (!bullet || !hitSnake || !hitSprite) { + return; + } + applyWeaponDamage(bullet->owner(), hitSnake, hitSprite, weapon); + } + + private: + bool multiShot_ = false; +}; + +class WeaponBehaviorFactory final { + public: + WeaponBehaviorFactory() = delete; + static std::shared_ptr makeBehavior(WeaponType type) { + if (type == WeaponType::SwordPoint || type == WeaponType::SwordRange) { + return std::make_shared(); + } + if (type == WeaponType::GunPoint || type == WeaponType::GunRange) { + return std::make_shared(false); + } + if (type == WeaponType::GunPointMulti) { + return std::make_shared(true); + } + return std::make_shared(); + } +}; +} // namespace + Weapon::Weapon(WeaponType type, int shootRange, int effectRange, int damage, int gap, int bulletSpeed, const std::shared_ptr& birthAnimation, @@ -47,7 +229,8 @@ Weapon::Weapon(WeaponType type, int shootRange, int effectRange, int damage, deathAnimation_(deathAnimation), flyAnimation_(flyAnimation), birthAudio_(birthAudio), - deathAudio_(deathAudio) {} + deathAudio_(deathAudio), + behavior_(WeaponBehaviorFactory::makeBehavior(type)) {} WeaponType Weapon::type() const { return type_; } int Weapon::shootRange() const { return shootRange_; } @@ -70,8 +253,14 @@ const std::array& Weapon::effects() const { return effects_; } std::array& Weapon::effects() { return effects_; } +const WeaponBehavior& Weapon::behavior() const { + return behavior_ ? *behavior_ : *WeaponBehaviorFactory::makeBehavior(type_); +} -void Weapon::setType(WeaponType type) { type_ = type; } +void Weapon::setType(WeaponType type) { + type_ = type; + behavior_ = WeaponBehaviorFactory::makeBehavior(type); +} void Weapon::setShootRange(int shootRange) { shootRange_ = shootRange; } void Weapon::setEffectRange(int effectRange) { effectRange_ = effectRange; } void Weapon::setDamage(int damage) { damage_ = damage; } @@ -88,6 +277,13 @@ void Weapon::setFlyAnimation(const std::shared_ptr& animation) { } void Weapon::setBirthAudio(int audio) { birthAudio_ = audio; } void Weapon::setDeathAudio(int audio) { deathAudio_ = audio; } +void Weapon::setBehavior(const std::shared_ptr& behavior) { + behavior_ = behavior; +} + +std::shared_ptr makeWeaponBehavior(WeaponType type) { + return WeaponBehaviorFactory::makeBehavior(type); +} Weapon weapons[WEAPONS_SIZE]; diff --git a/src/weapon.h b/src/weapon.h index fc5e8db..74ed64b 100644 --- a/src/weapon.h +++ b/src/weapon.h @@ -1,9 +1,11 @@ #ifndef SNAKE_WEAPON_H_ #define SNAKE_WEAPON_H_ +#include "adt.h" #include "types.h" #include +#include #include #define WEAPONS_SIZE 128 @@ -40,6 +42,41 @@ struct WeaponBuff { int duration = 0; }; +class Bullet; +class Snake; +class Weapon; +class WeaponBehavior; + +struct WeaponAttackContext { + std::shared_ptr attacker; + std::shared_ptr target; + std::shared_ptr targetSprite; + std::shared_ptr attackerSprite; +}; + +struct WeaponAttackResult { + bool attacked = false; +}; + +class WeaponBehavior { + public: + virtual ~WeaponBehavior() = default; + virtual WeaponAttackResult attack(const Weapon& weapon, + const WeaponAttackContext& context) const = 0; + virtual void onAttack(const Weapon& weapon, + const std::shared_ptr& sprite) const = 0; + virtual bool allowMultiTarget() const = 0; + virtual bool allowAreaImpact() const = 0; + virtual void applyImpact(const Weapon& weapon, + const std::shared_ptr& bullet, + const std::shared_ptr& hitSnake, + const std::shared_ptr& hitSprite) const = 0; + virtual void applyAreaImpact(const Weapon& weapon, + const std::shared_ptr& bullet, + const std::shared_ptr& hitSnake, + const std::shared_ptr& hitSprite) const = 0; +}; + class Weapon { public: Weapon() = default; @@ -67,6 +104,7 @@ class Weapon { int deathAudio() const; const std::array& effects() const; std::array& effects(); + const WeaponBehavior& behavior() const; void setType(WeaponType type); void setShootRange(int shootRange); @@ -79,6 +117,7 @@ class Weapon { void setFlyAnimation(const std::shared_ptr& animation); void setBirthAudio(int audio); void setDeathAudio(int audio); + void setBehavior(const std::shared_ptr& behavior); private: WeaponType type_ = WeaponType::SwordPoint; @@ -93,8 +132,10 @@ class Weapon { int birthAudio_ = -1; int deathAudio_ = -1; std::array effects_{}; + std::shared_ptr behavior_{}; }; void initWeapons(); +std::shared_ptr makeWeaponBehavior(WeaponType type); #endif