Browse Source

Implement barrack capture system with visual animations

Co-authored-by: djeada <[email protected]>
copilot-swe-agent[bot] 2 months ago
parent
commit
c322ef2c71

+ 2 - 0
app/core/game_engine.cpp

@@ -34,6 +34,7 @@
 #include "game/systems/arrow_system.h"
 #include "game/systems/building_collision_registry.h"
 #include "game/systems/camera_service.h"
+#include "game/systems/capture_system.h"
 #include "game/systems/combat_system.h"
 #include "game/systems/command_service.h"
 #include "game/systems/formation_planner.h"
@@ -120,6 +121,7 @@ GameEngine::GameEngine() {
   m_world->addSystem(std::make_unique<Game::Systems::MovementSystem>());
   m_world->addSystem(std::make_unique<Game::Systems::PatrolSystem>());
   m_world->addSystem(std::make_unique<Game::Systems::CombatSystem>());
+  m_world->addSystem(std::make_unique<Game::Systems::CaptureSystem>(*m_world));
   m_world->addSystem(std::make_unique<Game::Systems::AISystem>());
   m_world->addSystem(std::make_unique<Game::Systems::ProductionSystem>());
   m_world->addSystem(std::make_unique<Game::Systems::TerrainAlignmentSystem>());

+ 1 - 0
game/CMakeLists.txt

@@ -39,6 +39,7 @@ add_library(game_systems STATIC
     systems/command_service.cpp
     systems/production_service.cpp
     systems/production_system.cpp
+    systems/capture_system.cpp
     systems/terrain_alignment_system.cpp
     systems/owner_registry.cpp
     systems/troop_count_registry.cpp

+ 12 - 0
game/core/component.h

@@ -183,4 +183,16 @@ public:
   AIControlledComponent() = default;
 };
 
+class CaptureComponent : public Component {
+public:
+  CaptureComponent()
+      : capturingPlayerId(-1), captureProgress(0.0f), requiredTime(5.0f),
+        isBeingCaptured(false) {}
+
+  int capturingPlayerId;
+  float captureProgress;
+  float requiredTime;
+  bool isBeingCaptured;
+};
+
 } // namespace Engine::Core

+ 10 - 0
game/core/event_manager.h

@@ -160,4 +160,14 @@ public:
   int damage;
 };
 
+class BarrackCapturedEvent : public Event {
+public:
+  BarrackCapturedEvent(EntityID barrackId, int previousOwnerId, int newOwnerId)
+      : barrackId(barrackId), previousOwnerId(previousOwnerId),
+        newOwnerId(newOwnerId) {}
+  EntityID barrackId;
+  int previousOwnerId;
+  int newOwnerId;
+};
+
 } // namespace Engine::Core

+ 11 - 0
game/systems/building_collision_registry.cpp

@@ -91,6 +91,17 @@ void BuildingCollisionRegistry::updateBuildingPosition(unsigned int entityId,
   }
 }
 
+void BuildingCollisionRegistry::updateBuildingOwner(unsigned int entityId,
+                                                    int ownerId) {
+  auto it = m_entityToIndex.find(entityId);
+  if (it == m_entityToIndex.end()) {
+    return;
+  }
+
+  size_t index = it->second;
+  m_buildings[index].ownerId = ownerId;
+}
+
 bool BuildingCollisionRegistry::isPointInBuilding(
     float x, float z, unsigned int ignoreEntityId) const {
   for (const auto &building : m_buildings) {

+ 2 - 0
game/systems/building_collision_registry.h

@@ -40,6 +40,8 @@ public:
   void updateBuildingPosition(unsigned int entityId, float centerX,
                               float centerZ);
 
+  void updateBuildingOwner(unsigned int entityId, int ownerId);
+
   const std::vector<BuildingFootprint> &getAllBuildings() const {
     return m_buildings;
   }

+ 186 - 0
game/systems/capture_system.cpp

@@ -0,0 +1,186 @@
+#include "capture_system.h"
+#include "../core/component.h"
+#include "../core/event_manager.h"
+#include "../core/ownership_constants.h"
+#include "../core/world.h"
+#include "../units/troop_config.h"
+#include "../visuals/team_colors.h"
+#include "building_collision_registry.h"
+#include <cmath>
+#include <iostream>
+
+namespace Game::Systems {
+
+CaptureSystem::CaptureSystem(Engine::Core::World &world) : m_world(world) {}
+
+void CaptureSystem::update(Engine::Core::World *world, float deltaTime) {
+  processBarrackCapture(deltaTime);
+}
+
+int CaptureSystem::countNearbyTroops(float barrackX, float barrackZ,
+                                     int ownerId, float radius) {
+  int totalTroops = 0;
+  auto entities = m_world.getEntitiesWith<Engine::Core::UnitComponent>();
+
+  for (auto *e : entities) {
+    auto *unit = e->getComponent<Engine::Core::UnitComponent>();
+    auto *transform = e->getComponent<Engine::Core::TransformComponent>();
+
+    if (!unit || !transform || unit->health <= 0)
+      continue;
+
+    if (unit->ownerId != ownerId)
+      continue;
+
+    if (unit->unitType == "barracks")
+      continue;
+
+    float dx = transform->position.x - barrackX;
+    float dz = transform->position.z - barrackZ;
+    float distSq = dx * dx + dz * dz;
+
+    if (distSq <= radius * radius) {
+      int individualsPerUnit =
+          Game::Units::TroopConfig::instance().getIndividualsPerUnit(
+              unit->unitType);
+      totalTroops += individualsPerUnit;
+    }
+  }
+
+  return totalTroops;
+}
+
+void CaptureSystem::transferBarrackOwnership(Engine::Core::Entity *barrack,
+                                             int newOwnerId) {
+  auto *unit = barrack->getComponent<Engine::Core::UnitComponent>();
+  auto *renderable = barrack->getComponent<Engine::Core::RenderableComponent>();
+  auto *transform = barrack->getComponent<Engine::Core::TransformComponent>();
+  auto *prod = barrack->getComponent<Engine::Core::ProductionComponent>();
+
+  if (!unit || !renderable || !transform)
+    return;
+
+  int previousOwnerId = unit->ownerId;
+  unit->ownerId = newOwnerId;
+
+  QVector3D tc = Game::Visuals::teamColorForOwner(newOwnerId);
+  renderable->color[0] = tc.x();
+  renderable->color[1] = tc.y();
+  renderable->color[2] = tc.z();
+
+  Game::Systems::BuildingCollisionRegistry::instance().updateBuildingOwner(
+      barrack->getId(), newOwnerId);
+
+  if (!Game::Core::isNeutralOwner(newOwnerId) && !prod) {
+    prod = barrack->addComponent<Engine::Core::ProductionComponent>();
+    if (prod) {
+      prod->productType = "archer";
+      prod->buildTime = 10.0f;
+      prod->maxUnits = 150;
+      prod->inProgress = false;
+      prod->timeRemaining = 0.0f;
+      prod->producedCount = 0;
+      prod->rallyX = transform->position.x + 4.0f;
+      prod->rallyZ = transform->position.z + 2.0f;
+      prod->rallySet = true;
+      prod->villagerCost =
+          Game::Units::TroopConfig::instance().getIndividualsPerUnit(
+              prod->productType);
+    }
+  } else if (Game::Core::isNeutralOwner(newOwnerId) && prod) {
+    barrack->removeComponent<Engine::Core::ProductionComponent>();
+  }
+
+  Engine::Core::EventManager::instance().publish(
+      Engine::Core::BarrackCapturedEvent(barrack->getId(), previousOwnerId,
+                                         newOwnerId));
+}
+
+void CaptureSystem::processBarrackCapture(float deltaTime) {
+  constexpr float CAPTURE_RADIUS = 8.0f;
+  constexpr int TROOP_ADVANTAGE_MULTIPLIER = 3;
+
+  auto barracks = m_world.getEntitiesWith<Engine::Core::BuildingComponent>();
+
+  for (auto *barrack : barracks) {
+    auto *unit = barrack->getComponent<Engine::Core::UnitComponent>();
+    auto *transform = barrack->getComponent<Engine::Core::TransformComponent>();
+
+    if (!unit || !transform)
+      continue;
+
+    if (unit->unitType != "barracks")
+      continue;
+
+    auto *capture = barrack->getComponent<Engine::Core::CaptureComponent>();
+    if (!capture) {
+      capture = barrack->addComponent<Engine::Core::CaptureComponent>();
+    }
+
+    float barrackX = transform->position.x;
+    float barrackZ = transform->position.z;
+    int barrackOwnerId = unit->ownerId;
+
+    int maxEnemyTroops = 0;
+    int capturingPlayerId = -1;
+
+    auto entities = m_world.getEntitiesWith<Engine::Core::UnitComponent>();
+    std::vector<int> playerIds;
+    for (auto *e : entities) {
+      auto *u = e->getComponent<Engine::Core::UnitComponent>();
+      if (u && u->ownerId != barrackOwnerId &&
+          !Game::Core::isNeutralOwner(u->ownerId)) {
+        if (std::find(playerIds.begin(), playerIds.end(), u->ownerId) ==
+            playerIds.end()) {
+          playerIds.push_back(u->ownerId);
+        }
+      }
+    }
+
+    for (int playerId : playerIds) {
+      int troopCount = countNearbyTroops(barrackX, barrackZ, playerId,
+                                         CAPTURE_RADIUS);
+      if (troopCount > maxEnemyTroops) {
+        maxEnemyTroops = troopCount;
+        capturingPlayerId = playerId;
+      }
+    }
+
+    int defenderTroops = 0;
+    if (!Game::Core::isNeutralOwner(barrackOwnerId)) {
+      defenderTroops =
+          countNearbyTroops(barrackX, barrackZ, barrackOwnerId, CAPTURE_RADIUS);
+    }
+
+    bool canCapture =
+        maxEnemyTroops >= (defenderTroops * TROOP_ADVANTAGE_MULTIPLIER);
+
+    if (canCapture && capturingPlayerId != -1) {
+      if (capture->capturingPlayerId != capturingPlayerId) {
+        capture->capturingPlayerId = capturingPlayerId;
+        capture->captureProgress = 0.0f;
+      }
+
+      capture->isBeingCaptured = true;
+      capture->captureProgress += deltaTime;
+
+      if (capture->captureProgress >= capture->requiredTime) {
+        transferBarrackOwnership(barrack, capturingPlayerId);
+        capture->captureProgress = 0.0f;
+        capture->isBeingCaptured = false;
+        capture->capturingPlayerId = -1;
+      }
+    } else {
+      if (capture->isBeingCaptured) {
+        capture->captureProgress -= deltaTime * 2.0f;
+        if (capture->captureProgress <= 0.0f) {
+          capture->captureProgress = 0.0f;
+          capture->isBeingCaptured = false;
+          capture->capturingPlayerId = -1;
+        }
+      }
+    }
+  }
+}
+
+} // namespace Game::Systems

+ 27 - 0
game/systems/capture_system.h

@@ -0,0 +1,27 @@
+#pragma once
+
+#include "../core/entity.h"
+#include "../core/system.h"
+
+namespace Engine::Core {
+class World;
+}
+
+namespace Game::Systems {
+
+class CaptureSystem : public Engine::Core::System {
+public:
+  explicit CaptureSystem(Engine::Core::World &world);
+
+  void update(Engine::Core::World *world, float deltaTime) override;
+
+private:
+  Engine::Core::World &m_world;
+
+  void processBarrackCapture(float deltaTime);
+  int countNearbyTroops(float barrackX, float barrackZ, int ownerId,
+                        float radius);
+  void transferBarrackOwnership(Engine::Core::Entity *barrack, int newOwnerId);
+};
+
+} // namespace Game::Systems

+ 73 - 6
render/entity/barracks_renderer.cpp

@@ -1,5 +1,6 @@
 #include "barracks_renderer.h"
 #include "../../game/core/component.h"
+#include "../../game/visuals/team_colors.h"
 #include "../geom/flag.h"
 #include "../geom/math_utils.h"
 #include "../geom/transforms.h"
@@ -538,6 +539,34 @@ static inline void drawBannerAndPole(const DrawContext &p, ISubmitter &out,
 
   float beamLength = targetWidth * 0.45f;
   float beamY = poleHeight - targetHeight * 0.25f;
+
+  QVector3D teamColor = C.team;
+  QVector3D teamTrimColor = C.teamTrim;
+  float flagY = poleHeight - targetHeight / 2.0f;
+
+  if (p.entity) {
+    auto *capture = p.entity->getComponent<Engine::Core::CaptureComponent>();
+    if (capture && capture->isBeingCaptured) {
+      float progress =
+          std::clamp(capture->captureProgress / capture->requiredTime, 0.0f,
+                     1.0f);
+
+      QVector3D newTeamColor = Game::Visuals::teamColorForOwner(
+          capture->capturingPlayerId);
+      teamColor = lerp(C.team, clampVec01(newTeamColor), progress);
+      teamTrimColor = lerp(
+          C.teamTrim,
+          clampVec01(QVector3D(newTeamColor.x() * 0.6f, newTeamColor.y() * 0.6f,
+                               newTeamColor.z() * 0.6f)),
+          progress);
+
+      float loweredAmount = progress * targetHeight * 0.3f;
+      flagY -= loweredAmount;
+      targetHeight *= (1.0f - progress * 0.2f);
+      beamY -= loweredAmount;
+    }
+  }
+
   QVector3D beamStart(poleX + 0.02f, beamY, poleZ);
   QVector3D beamEnd(poleX + beamLength + 0.02f, beamY, poleZ);
   drawCylinder(out, p.model, beamStart, beamEnd, poleRadius * 0.35f, C.timber,
@@ -550,16 +579,16 @@ static inline void drawBannerAndPole(const DrawContext &p, ISubmitter &out,
 
   float panelX = beamEnd.x() + (targetWidth * 0.5f - beamLength);
   unitBox(out, unit, white, p.model,
-          QVector3D(panelX, poleHeight - targetHeight / 2.0f, poleZ + 0.01f),
+          QVector3D(panelX, flagY, poleZ + 0.01f),
           QVector3D(targetWidth / 2.0f, targetHeight / 2.0f, panelDepth),
-          C.team);
+          teamColor);
 
   unitBox(out, unit, white, p.model,
-          QVector3D(panelX, poleHeight - targetHeight + 0.04f, poleZ + 0.01f),
-          QVector3D(targetWidth / 2.0f + 0.02f, 0.04f, 0.015f), C.teamTrim);
+          QVector3D(panelX, flagY - targetHeight / 2.0f + 0.04f, poleZ + 0.01f),
+          QVector3D(targetWidth / 2.0f + 0.02f, 0.04f, 0.015f), teamTrimColor);
   unitBox(out, unit, white, p.model,
-          QVector3D(panelX, poleHeight - 0.04f, poleZ + 0.01f),
-          QVector3D(targetWidth / 2.0f + 0.02f, 0.04f, 0.015f), C.teamTrim);
+          QVector3D(panelX, flagY + targetHeight / 2.0f - 0.04f, poleZ + 0.01f),
+          QVector3D(targetWidth / 2.0f + 0.02f, 0.04f, 0.015f), teamTrimColor);
 }
 
 static inline void drawRallyFlagIfAny(const DrawContext &p, ISubmitter &out,
@@ -619,6 +648,43 @@ static inline void drawHealthBar(const DrawContext &p, ISubmitter &out,
           fgColor);
 }
 
+static inline void drawCaptureProgress(const DrawContext &p, ISubmitter &out,
+                                       Mesh *unit, Texture *white) {
+  if (!p.entity)
+    return;
+
+  auto *capture = p.entity->getComponent<Engine::Core::CaptureComponent>();
+  if (!capture || !capture->isBeingCaptured)
+    return;
+
+  float progress = std::clamp(capture->captureProgress / capture->requiredTime,
+                              0.0f, 1.0f);
+
+  constexpr float baseHeight = BuildingProportions::baseHeight;
+  constexpr float roofPitch = BuildingProportions::roofPitch;
+  float roofPeak = baseHeight + roofPitch;
+  float barY = roofPeak + 0.30f;
+
+  constexpr float barWidth = BuildingProportions::baseWidth * 0.8f;
+  constexpr float barHeight = 0.10f;
+  constexpr float barDepth = 0.14f;
+
+  QVector3D bgColor(0.12f, 0.12f, 0.12f);
+  unitBox(out, unit, white, p.model, QVector3D(0.0f, barY, 0.0f),
+          QVector3D(barWidth / 2.0f, barHeight / 2.0f, barDepth / 2.0f),
+          bgColor);
+
+  float fillWidth = barWidth * progress;
+  float fillX = -(barWidth - fillWidth) * 0.5f;
+
+  QVector3D captureColor(0.95f, 0.75f, 0.15f);
+
+  unitBox(out, unit, white, p.model, QVector3D(fillX, barY + 0.006f, 0.0f),
+          QVector3D(fillWidth / 2.0f, (barHeight / 2.0f) * 0.85f,
+                    (barDepth / 2.0f) * 0.90f),
+          captureColor);
+}
+
 static inline void drawSelectionFX(const DrawContext &p, ISubmitter &out) {
   QMatrix4x4 M;
   QVector3D pos = p.model.column(3).toVector3D();
@@ -657,6 +723,7 @@ static void drawBarracks(const DrawContext &p, ISubmitter &out) {
 
   drawRallyFlagIfAny(p, out, white, C);
   drawHealthBar(p, out, unit, white);
+  drawCaptureProgress(p, out, unit, white);
   drawSelectionFX(p, out);
 }