Browse Source

Merge pull request #261 from djeada/copilot/fix-archer-engagement-behavior

🏹 Fix AI Archer Behavior — Stop and Engage from Range in All Scenarios
Adam Djellouli 2 months ago
parent
commit
809b6de782

+ 14 - 1
game/systems/ai_system/IMPROVEMENTS.md

@@ -129,9 +129,22 @@ All files compile cleanly. New dependencies:
 - ai_tactical.cpp
 - retreat_behavior.cpp
 
+## Recent Improvements
+
+### 9. Ranged Unit Combat Behavior (Archers)
+- **Problem**: Archers continued walking into melee range even when enemies were in firing range
+- **Solution**: Enhanced CombatSystem to detect ranged units and stop movement when within attack range
+- **Implementation**:
+  - Ranged units stop all movement when any enemy is within range, regardless of movement reason
+  - Works for archers moving to defend, attack, or any other command
+  - Maintain optimal firing distance (85% of max range) when approaching targets during chase
+  - Hold position instead of continuing movement when enemies enter effective range
+  - Prevents ranged units from walking into melee combat unnecessarily
+- **Impact**: Archers now engage from safe distance in all scenarios, maintain tactical advantage
+
 ## Next Steps (Optional Enhancements)
 
-1. **Kiting Behavior** - Ranged units retreat while attacking
+1. **Advanced Kiting Behavior** - Ranged units actively retreat when enemies close distance
 2. **Squad System** - Group units into persistent squads
 3. **Threat Map** - Track enemy positions over time
 4. **Difficulty Levels** - Configurable reaction times, mistakes

+ 3 - 1
game/systems/ai_system/behaviors/attack_behavior.cpp

@@ -234,10 +234,12 @@ void AttackBehavior::execute(const AISnapshot &snapshot, AIContext &context,
   command.units = std::move(claimedUnits);
   command.targetId = targetInfo.targetId;
 
-  command.shouldChase =
+  bool shouldChaseAggressive =
       (context.state == AIState::Attacking || context.barracksUnderThreat) &&
       assessment.forceRatio >= 0.8f;
 
+  command.shouldChase = shouldChaseAggressive;
+
   outCommands.push_back(std::move(command));
 }
 bool AttackBehavior::shouldExecute(const AISnapshot &snapshot,

+ 166 - 73
game/systems/combat_system.cpp

@@ -163,6 +163,30 @@ void CombatSystem::processAttacks(Engine::Core::World *world, float deltaTime) {
           if (isInRange(attacker, target, range)) {
             bestTarget = target;
 
+            bool isRangedUnit = false;
+            if (attackerAtk && attackerAtk->canRanged &&
+                attackerAtk->currentMode ==
+                    Engine::Core::AttackComponent::CombatMode::Ranged) {
+              isRangedUnit = true;
+            }
+
+            if (isRangedUnit) {
+              auto *movement =
+                  attacker->getComponent<Engine::Core::MovementComponent>();
+              if (movement && movement->hasTarget) {
+                movement->hasTarget = false;
+                movement->vx = 0.0f;
+                movement->vz = 0.0f;
+                movement->path.clear();
+                if (attackerTransform) {
+                  movement->targetX = attackerTransform->position.x;
+                  movement->targetY = attackerTransform->position.z;
+                  movement->goalX = attackerTransform->position.x;
+                  movement->goalY = attackerTransform->position.z;
+                }
+              }
+            }
+
             if (auto *attT =
                     attacker
                         ->getComponent<Engine::Core::TransformComponent>()) {
@@ -188,91 +212,136 @@ void CombatSystem::processAttacks(Engine::Core::World *world, float deltaTime) {
               continue;
             }
 
-            auto *targetTransform =
-                target->getComponent<Engine::Core::TransformComponent>();
-            auto *attackerTransformComponent =
-                attacker->getComponent<Engine::Core::TransformComponent>();
-            if (targetTransform && attackerTransformComponent) {
-              QVector3D attackerPos(attackerTransformComponent->position.x,
-                                    0.0f,
-                                    attackerTransformComponent->position.z);
-              QVector3D targetPos(targetTransform->position.x, 0.0f,
-                                  targetTransform->position.z);
-              QVector3D desiredPos = targetPos;
-              bool holdPosition = false;
-
-              bool targetIsBuilding =
-                  target->hasComponent<Engine::Core::BuildingComponent>();
-              if (targetIsBuilding) {
-                float scaleX = targetTransform->scale.x;
-                float scaleZ = targetTransform->scale.z;
-                float targetRadius = std::max(scaleX, scaleZ) * 0.5f;
-                QVector3D direction = targetPos - attackerPos;
-                float distanceSq = direction.lengthSquared();
-                if (distanceSq > 0.000001f) {
-                  float distance = std::sqrt(distanceSq);
-                  direction /= distance;
-                  float desiredDistance =
-                      targetRadius + std::max(range - 0.2f, 0.2f);
-                  if (distance > desiredDistance + 0.15f) {
-                    desiredPos = targetPos - direction * desiredDistance;
-                  } else {
-                    holdPosition = true;
-                  }
-                }
-              }
+            bool isRangedUnit = false;
+            if (attackerAtk && attackerAtk->canRanged &&
+                attackerAtk->currentMode ==
+                    Engine::Core::AttackComponent::CombatMode::Ranged) {
+              isRangedUnit = true;
+            }
 
+            bool currentlyInRange = isInRange(attacker, target, range);
+
+            if (isRangedUnit && currentlyInRange) {
               auto *movement =
                   attacker->getComponent<Engine::Core::MovementComponent>();
-              if (!movement) {
-                movement =
-                    attacker->addComponent<Engine::Core::MovementComponent>();
-              }
-
               if (movement) {
-                if (holdPosition) {
-                  movement->hasTarget = false;
-                  movement->vx = 0.0f;
-                  movement->vz = 0.0f;
-                  movement->path.clear();
-                  if (attackerTransformComponent) {
-                    movement->targetX = attackerTransformComponent->position.x;
-                    movement->targetY = attackerTransformComponent->position.z;
-                    movement->goalX = attackerTransformComponent->position.x;
-                    movement->goalY = attackerTransformComponent->position.z;
-                  }
-                } else {
-                  QVector3D plannedTarget(movement->targetX, 0.0f,
-                                          movement->targetY);
-                  if (!movement->path.empty()) {
-                    const auto &finalNode = movement->path.back();
-                    plannedTarget =
-                        QVector3D(finalNode.first, 0.0f, finalNode.second);
+                movement->hasTarget = false;
+                movement->vx = 0.0f;
+                movement->vz = 0.0f;
+                movement->path.clear();
+                auto *attackerTransformComponent =
+                    attacker->getComponent<Engine::Core::TransformComponent>();
+                if (attackerTransformComponent) {
+                  movement->targetX = attackerTransformComponent->position.x;
+                  movement->targetY = attackerTransformComponent->position.z;
+                  movement->goalX = attackerTransformComponent->position.x;
+                  movement->goalY = attackerTransformComponent->position.z;
+                }
+              }
+              bestTarget = target;
+            } else {
+
+              auto *targetTransform =
+                  target->getComponent<Engine::Core::TransformComponent>();
+              auto *attackerTransformComponent =
+                  attacker->getComponent<Engine::Core::TransformComponent>();
+              if (targetTransform && attackerTransformComponent) {
+                QVector3D attackerPos(attackerTransformComponent->position.x,
+                                      0.0f,
+                                      attackerTransformComponent->position.z);
+                QVector3D targetPos(targetTransform->position.x, 0.0f,
+                                    targetTransform->position.z);
+                QVector3D desiredPos = targetPos;
+                bool holdPosition = false;
+
+                bool targetIsBuilding =
+                    target->hasComponent<Engine::Core::BuildingComponent>();
+                if (targetIsBuilding) {
+                  float scaleX = targetTransform->scale.x;
+                  float scaleZ = targetTransform->scale.z;
+                  float targetRadius = std::max(scaleX, scaleZ) * 0.5f;
+                  QVector3D direction = targetPos - attackerPos;
+                  float distanceSq = direction.lengthSquared();
+                  if (distanceSq > 0.000001f) {
+                    float distance = std::sqrt(distanceSq);
+                    direction /= distance;
+                    float desiredDistance =
+                        targetRadius + std::max(range - 0.2f, 0.2f);
+                    if (distance > desiredDistance + 0.15f) {
+                      desiredPos = targetPos - direction * desiredDistance;
+                    } else {
+                      holdPosition = true;
+                    }
                   }
-
-                  float diffSq = (plannedTarget - desiredPos).lengthSquared();
-                  bool needNewCommand = !movement->pathPending;
-                  if (movement->hasTarget && diffSq <= 0.25f * 0.25f) {
-                    needNewCommand = false;
+                } else if (isRangedUnit) {
+                  QVector3D direction = targetPos - attackerPos;
+                  float distanceSq = direction.lengthSquared();
+                  if (distanceSq > 0.000001f) {
+                    float distance = std::sqrt(distanceSq);
+                    direction /= distance;
+                    float optimalRange = range * 0.85f;
+                    if (distance > optimalRange + 0.5f) {
+                      desiredPos = targetPos - direction * optimalRange;
+                    } else {
+                      holdPosition = true;
+                    }
                   }
+                }
 
-                  if (needNewCommand) {
-                    CommandService::MoveOptions options;
-                    options.clearAttackIntent = false;
+                auto *movement =
+                    attacker->getComponent<Engine::Core::MovementComponent>();
+                if (!movement) {
+                  movement =
+                      attacker->addComponent<Engine::Core::MovementComponent>();
+                }
 
-                    options.allowDirectFallback = true;
-                    std::vector<Engine::Core::EntityID> unitIds = {
-                        attacker->getId()};
-                    std::vector<QVector3D> moveTargets = {desiredPos};
-                    CommandService::moveUnits(*world, unitIds, moveTargets,
-                                              options);
+                if (movement) {
+                  if (holdPosition) {
+                    movement->hasTarget = false;
+                    movement->vx = 0.0f;
+                    movement->vz = 0.0f;
+                    movement->path.clear();
+                    if (attackerTransformComponent) {
+                      movement->targetX =
+                          attackerTransformComponent->position.x;
+                      movement->targetY =
+                          attackerTransformComponent->position.z;
+                      movement->goalX = attackerTransformComponent->position.x;
+                      movement->goalY = attackerTransformComponent->position.z;
+                    }
+                  } else {
+                    QVector3D plannedTarget(movement->targetX, 0.0f,
+                                            movement->targetY);
+                    if (!movement->path.empty()) {
+                      const auto &finalNode = movement->path.back();
+                      plannedTarget =
+                          QVector3D(finalNode.first, 0.0f, finalNode.second);
+                    }
+
+                    float diffSq = (plannedTarget - desiredPos).lengthSquared();
+                    bool needNewCommand = !movement->pathPending;
+                    if (movement->hasTarget && diffSq <= 0.25f * 0.25f) {
+                      needNewCommand = false;
+                    }
+
+                    if (needNewCommand) {
+                      CommandService::MoveOptions options;
+                      options.clearAttackIntent = false;
+
+                      options.allowDirectFallback = true;
+                      std::vector<Engine::Core::EntityID> unitIds = {
+                          attacker->getId()};
+                      std::vector<QVector3D> moveTargets = {desiredPos};
+                      CommandService::moveUnits(*world, unitIds, moveTargets,
+                                                options);
+                    }
                   }
                 }
               }
-            }
 
-            if (isInRange(attacker, target, range)) {
-              bestTarget = target;
+              if (isInRange(attacker, target, range)) {
+                bestTarget = target;
+              }
             }
           } else {
 
@@ -340,6 +409,30 @@ void CombatSystem::processAttacks(Engine::Core::World *world, float deltaTime) {
         }
       }
 
+      bool isRangedUnit = false;
+      if (attackerAtk && attackerAtk->canRanged &&
+          attackerAtk->currentMode ==
+              Engine::Core::AttackComponent::CombatMode::Ranged) {
+        isRangedUnit = true;
+      }
+
+      if (isRangedUnit) {
+        auto *movement =
+            attacker->getComponent<Engine::Core::MovementComponent>();
+        if (movement && movement->hasTarget) {
+          movement->hasTarget = false;
+          movement->vx = 0.0f;
+          movement->vz = 0.0f;
+          movement->path.clear();
+          if (attackerTransform) {
+            movement->targetX = attackerTransform->position.x;
+            movement->targetY = attackerTransform->position.z;
+            movement->goalX = attackerTransform->position.x;
+            movement->goalY = attackerTransform->position.z;
+          }
+        }
+      }
+
       if (auto *attT =
               attacker->getComponent<Engine::Core::TransformComponent>()) {
         if (auto *tgtT =

+ 7 - 0
game/systems/command_service.cpp

@@ -717,8 +717,12 @@ void CommandService::attackTarget(
     QVector3D desiredPos = targetPos;
 
     float range = 2.0f;
+    bool isRangedUnit = false;
     if (auto *atk = e->getComponent<Engine::Core::AttackComponent>()) {
       range = std::max(0.1f, atk->range);
+      if (atk->canRanged && atk->range > atk->meleeRange * 1.5f) {
+        isRangedUnit = true;
+      }
     }
 
     QVector3D direction = targetPos - attackerPos;
@@ -735,6 +739,9 @@ void CommandService::attackTarget(
         }
       } else {
         float desiredDistance = std::max(range - 0.2f, 0.2f);
+        if (isRangedUnit) {
+          desiredDistance = range * 0.85f;
+        }
         if (distance > desiredDistance + 0.15f) {
           desiredPos = targetPos - direction * desiredDistance;
         }