Browse Source

Merge pull request #239 from djeada/copilot/add-hold-mode-archers

🏹 Implement Hold Mode for Archers with Stat Boosts, Sky-Pointing Animation, and Stand-Up Cooldown
Adam Djellouli 2 months ago
parent
commit
b09421d147

+ 45 - 0
app/controllers/command_controller.cpp

@@ -93,6 +93,51 @@ CommandResult CommandController::onStopCommand() {
   return result;
   return result;
 }
 }
 
 
+CommandResult CommandController::onHoldCommand() {
+  CommandResult result;
+  if (!m_selectionSystem || !m_world) {
+    return result;
+  }
+
+  const auto &selected = m_selectionSystem->getSelectedUnits();
+  if (selected.empty())
+    return result;
+
+  for (auto id : selected) {
+    auto *entity = m_world->getEntity(id);
+    if (!entity)
+      continue;
+
+    resetMovement(entity);
+
+    entity->removeComponent<Engine::Core::AttackTargetComponent>();
+
+    if (auto *patrol = entity->getComponent<Engine::Core::PatrolComponent>()) {
+      patrol->patrolling = false;
+      patrol->waypoints.clear();
+    }
+
+    auto *holdMode = entity->getComponent<Engine::Core::HoldModeComponent>();
+    if (!holdMode) {
+      holdMode = entity->addComponent<Engine::Core::HoldModeComponent>();
+    }
+    holdMode->active = true;
+
+    auto *movement = entity->getComponent<Engine::Core::MovementComponent>();
+    if (movement) {
+      movement->hasTarget = false;
+      movement->path.clear();
+      movement->pathPending = false;
+      movement->vx = 0.0f;
+      movement->vz = 0.0f;
+    }
+  }
+
+  result.inputConsumed = true;
+  result.resetCursorToNormal = true;
+  return result;
+}
+
 CommandResult CommandController::onPatrolClick(qreal sx, qreal sy,
 CommandResult CommandController::onPatrolClick(qreal sx, qreal sy,
                                                int viewportWidth,
                                                int viewportWidth,
                                                int viewportHeight,
                                                int viewportHeight,

+ 1 - 0
app/controllers/command_controller.h

@@ -36,6 +36,7 @@ public:
   CommandResult onAttackClick(qreal sx, qreal sy, int viewportWidth,
   CommandResult onAttackClick(qreal sx, qreal sy, int viewportWidth,
                               int viewportHeight, void *camera);
                               int viewportHeight, void *camera);
   CommandResult onStopCommand();
   CommandResult onStopCommand();
+  CommandResult onHoldCommand();
   CommandResult onPatrolClick(qreal sx, qreal sy, int viewportWidth,
   CommandResult onPatrolClick(qreal sx, qreal sy, int viewportWidth,
                               int viewportHeight, void *camera);
                               int viewportHeight, void *camera);
   CommandResult setRallyAtScreen(qreal sx, qreal sy, int viewportWidth,
   CommandResult setRallyAtScreen(qreal sx, qreal sy, int viewportWidth,

+ 11 - 0
app/core/game_engine.cpp

@@ -306,6 +306,17 @@ void GameEngine::onStopCommand() {
   }
   }
 }
 }
 
 
+void GameEngine::onHoldCommand() {
+  if (!m_commandController)
+    return;
+  ensureInitialized();
+
+  auto result = m_commandController->onHoldCommand();
+  if (result.resetCursorToNormal) {
+    setCursorMode(CursorMode::Normal);
+  }
+}
+
 void GameEngine::onPatrolClick(qreal sx, qreal sy) {
 void GameEngine::onPatrolClick(qreal sx, qreal sy) {
   if (!m_commandController || !m_camera)
   if (!m_commandController || !m_camera)
     return;
     return;

+ 1 - 0
app/core/game_engine.h

@@ -110,6 +110,7 @@ public:
   Q_INVOKABLE void setHoverAtScreen(qreal sx, qreal sy);
   Q_INVOKABLE void setHoverAtScreen(qreal sx, qreal sy);
   Q_INVOKABLE void onAttackClick(qreal sx, qreal sy);
   Q_INVOKABLE void onAttackClick(qreal sx, qreal sy);
   Q_INVOKABLE void onStopCommand();
   Q_INVOKABLE void onStopCommand();
+  Q_INVOKABLE void onHoldCommand();
   Q_INVOKABLE void onPatrolClick(qreal sx, qreal sy);
   Q_INVOKABLE void onPatrolClick(qreal sx, qreal sy);
 
 
   Q_INVOKABLE void cameraMove(float dx, float dz);
   Q_INVOKABLE void cameraMove(float dx, float dz);

+ 10 - 0
game/core/component.h

@@ -200,4 +200,14 @@ public:
   PendingRemovalComponent() = default;
   PendingRemovalComponent() = default;
 };
 };
 
 
+class HoldModeComponent : public Component {
+public:
+  HoldModeComponent()
+      : active(true), exitCooldown(0.0f), standUpDuration(0.8f) {}
+
+  bool active;
+  float exitCooldown;
+  float standUpDuration;
+};
+
 } // namespace Engine::Core
 } // namespace Engine::Core

+ 17 - 0
game/systems/combat_system.cpp

@@ -109,6 +109,14 @@ void CombatSystem::processAttacks(Engine::Core::World *world, float deltaTime) {
       range = attackerAtk->getCurrentRange();
       range = attackerAtk->getCurrentRange();
       damage = attackerAtk->getCurrentDamage();
       damage = attackerAtk->getCurrentDamage();
       cooldown = attackerAtk->getCurrentCooldown();
       cooldown = attackerAtk->getCurrentCooldown();
+
+      auto *holdMode = attacker->getComponent<Engine::Core::HoldModeComponent>();
+      if (holdMode && holdMode->active &&
+          attackerUnit->unitType == "archer") {
+        range *= 1.5f;
+        damage = static_cast<int>(damage * 1.3f);
+      }
+
       attackerAtk->timeSinceLast += deltaTime;
       attackerAtk->timeSinceLast += deltaTime;
       tAccum = &attackerAtk->timeSinceLast;
       tAccum = &attackerAtk->timeSinceLast;
     } else {
     } else {
@@ -155,6 +163,15 @@ void CombatSystem::processAttacks(Engine::Core::World *world, float deltaTime) {
             }
             }
           } else if (attackTarget->shouldChase) {
           } else if (attackTarget->shouldChase) {
 
 
+            auto *holdMode =
+                attacker->getComponent<Engine::Core::HoldModeComponent>();
+            if (holdMode && holdMode->active) {
+              if (!isInRange(attacker, target, range)) {
+                attacker->removeComponent<Engine::Core::AttackTargetComponent>();
+              }
+              continue;
+            }
+
             auto *targetTransform =
             auto *targetTransform =
                 target->getComponent<Engine::Core::TransformComponent>();
                 target->getComponent<Engine::Core::TransformComponent>();
             auto *attackerTransformComponent =
             auto *attackerTransformComponent =

+ 10 - 0
game/systems/command_service.cpp

@@ -113,6 +113,11 @@ void CommandService::moveUnits(Engine::Core::World &world,
     if (!e)
     if (!e)
       continue;
       continue;
 
 
+    auto *holdMode = e->getComponent<Engine::Core::HoldModeComponent>();
+    if (holdMode) {
+      holdMode->active = false;
+    }
+
     auto *atk = e->getComponent<Engine::Core::AttackComponent>();
     auto *atk = e->getComponent<Engine::Core::AttackComponent>();
     if (atk && atk->inMeleeLock) {
     if (atk && atk->inMeleeLock) {
 
 
@@ -676,6 +681,11 @@ void CommandService::attackTarget(
     if (!e)
     if (!e)
       continue;
       continue;
 
 
+    auto *holdMode = e->getComponent<Engine::Core::HoldModeComponent>();
+    if (holdMode) {
+      holdMode->active = false;
+    }
+
     auto *attackTarget = e->getComponent<Engine::Core::AttackTargetComponent>();
     auto *attackTarget = e->getComponent<Engine::Core::AttackTargetComponent>();
     if (!attackTarget) {
     if (!attackTarget) {
       attackTarget = e->addComponent<Engine::Core::AttackTargetComponent>();
       attackTarget = e->addComponent<Engine::Core::AttackTargetComponent>();

+ 25 - 0
game/systems/movement_system.cpp

@@ -105,6 +105,31 @@ void MovementSystem::moveUnit(Engine::Core::Entity *entity,
     return;
     return;
   }
   }
 
 
+  auto *holdMode = entity->getComponent<Engine::Core::HoldModeComponent>();
+  if (holdMode) {
+    if (holdMode->exitCooldown > 0.0f) {
+      holdMode->exitCooldown = std::max(0.0f, holdMode->exitCooldown - deltaTime);
+    }
+
+    if (holdMode->active) {
+      movement->hasTarget = false;
+      movement->vx = 0.0f;
+      movement->vz = 0.0f;
+      movement->path.clear();
+      movement->pathPending = false;
+      return;
+    }
+
+    if (holdMode->exitCooldown > 0.0f) {
+      movement->hasTarget = false;
+      movement->vx = 0.0f;
+      movement->vz = 0.0f;
+      movement->path.clear();
+      movement->pathPending = false;
+      return;
+    }
+  }
+
   auto *atk = entity->getComponent<Engine::Core::AttackComponent>();
   auto *atk = entity->getComponent<Engine::Core::AttackComponent>();
   if (atk && atk->inMeleeLock) {
   if (atk && atk->inMeleeLock) {
 
 

+ 44 - 0
game/units/unit.cpp

@@ -45,6 +45,13 @@ void Unit::moveTo(float x, float z) {
     m_mv->pathPending = false;
     m_mv->pathPending = false;
     m_mv->pendingRequestId = 0;
     m_mv->pendingRequestId = 0;
   }
   }
+
+  if (auto *e = entity()) {
+    auto *holdComp = e->getComponent<Engine::Core::HoldModeComponent>();
+    if (holdComp) {
+      holdComp->active = false;
+    }
+  }
 }
 }
 
 
 bool Unit::isAlive() const {
 bool Unit::isAlive() const {
@@ -63,5 +70,42 @@ QVector3D Unit::position() const {
   return QVector3D();
   return QVector3D();
 }
 }
 
 
+void Unit::setHoldMode(bool enabled) {
+  auto *e = entity();
+  if (!e)
+    return;
+
+  auto *holdComp = e->getComponent<Engine::Core::HoldModeComponent>();
+
+  if (enabled) {
+    if (!holdComp) {
+      holdComp = e->addComponent<Engine::Core::HoldModeComponent>();
+    }
+    holdComp->active = true;
+    holdComp->exitCooldown = 0.0f;
+
+    auto *mv = e->getComponent<Engine::Core::MovementComponent>();
+    if (mv) {
+      mv->hasTarget = false;
+      mv->path.clear();
+      mv->pathPending = false;
+    }
+  } else {
+    if (holdComp) {
+      holdComp->active = false;
+      holdComp->exitCooldown = holdComp->standUpDuration;
+    }
+  }
+}
+
+bool Unit::isInHoldMode() const {
+  auto *e = entity();
+  if (!e)
+    return false;
+
+  auto *holdComp = e->getComponent<Engine::Core::HoldModeComponent>();
+  return holdComp && holdComp->active;
+}
+
 } // namespace Units
 } // namespace Units
 } // namespace Game
 } // namespace Game

+ 3 - 0
game/units/unit.h

@@ -41,6 +41,9 @@ public:
   bool isAlive() const;
   bool isAlive() const;
   QVector3D position() const;
   QVector3D position() const;
 
 
+  void setHoldMode(bool enabled);
+  bool isInHoldMode() const;
+
 protected:
 protected:
   Unit(Engine::Core::World &world, const std::string &type);
   Unit(Engine::Core::World &world, const std::string &type);
   Engine::Core::Entity *entity() const;
   Engine::Core::Entity *entity() const;

+ 32 - 6
render/entity/archer_renderer.cpp

@@ -72,13 +72,39 @@ public:
     float armAsymmetry = (hash01(seed ^ 0xDEF0u) - 0.5f) * 0.04f;
     float armAsymmetry = (hash01(seed ^ 0xDEF0u) - 0.5f) * 0.04f;
 
 
     float bowX = 0.0f;
     float bowX = 0.0f;
-    pose.handL = QVector3D(bowX - 0.05f + armAsymmetry,
-                           HP::SHOULDER_Y + 0.05f + armHeightJitter, 0.55f);
-    pose.handR =
-        QVector3D(0.15f - armAsymmetry * 0.5f,
-                  HP::SHOULDER_Y + 0.15f + armHeightJitter * 0.8f, 0.20f);
 
 
-    if (anim.isAttacking) {
+    if (anim.isInHoldMode || anim.holdExitProgress > 0.0f) {
+      float t = anim.isInHoldMode ? 1.0f : (1.0f - anim.holdExitProgress);
+
+      float targetFootOffset = -0.25f * t;
+      pose.footYOffset = targetFootOffset;
+      pose.footL.setY(HP::GROUND_Y + pose.footYOffset);
+      pose.footR.setY(HP::GROUND_Y + pose.footYOffset);
+
+      pose.shoulderL.setY(pose.shoulderL.y() - 0.15f * t);
+      pose.shoulderR.setY(pose.shoulderR.y() - 0.15f * t);
+      pose.headPos.setY(pose.headPos.y() - 0.10f * t);
+      pose.neckBase.setY(pose.neckBase.y() - 0.12f * t);
+
+      QVector3D holdHandL(bowX - 0.12f, HP::SHOULDER_Y + 0.20f, 0.45f);
+      QVector3D holdHandR(bowX + 0.08f, HP::SHOULDER_Y + 0.50f, 0.15f);
+      QVector3D normalHandL(bowX - 0.05f + armAsymmetry,
+                            HP::SHOULDER_Y + 0.05f + armHeightJitter, 0.55f);
+      QVector3D normalHandR(0.15f - armAsymmetry * 0.5f,
+                            HP::SHOULDER_Y + 0.15f + armHeightJitter * 0.8f,
+                            0.20f);
+
+      pose.handL = normalHandL * (1.0f - t) + holdHandL * t;
+      pose.handR = normalHandR * (1.0f - t) + holdHandR * t;
+    } else {
+      pose.handL = QVector3D(bowX - 0.05f + armAsymmetry,
+                             HP::SHOULDER_Y + 0.05f + armHeightJitter, 0.55f);
+      pose.handR =
+          QVector3D(0.15f - armAsymmetry * 0.5f,
+                    HP::SHOULDER_Y + 0.15f + armHeightJitter * 0.8f, 0.20f);
+    }
+
+    if (anim.isAttacking && !anim.isInHoldMode) {
       float attackCycleTime = 1.2f;
       float attackCycleTime = 1.2f;
       float attackPhase = fmod(anim.time * (1.0f / attackCycleTime), 1.0f);
       float attackPhase = fmod(anim.time * (1.0f / attackCycleTime), 1.0f);
 
 

+ 8 - 0
render/humanoid_base.cpp

@@ -85,6 +85,8 @@ AnimationInputs HumanoidRendererBase::sampleAnimState(const DrawContext &ctx) {
   anim.isMoving = false;
   anim.isMoving = false;
   anim.isAttacking = false;
   anim.isAttacking = false;
   anim.isMelee = false;
   anim.isMelee = false;
+  anim.isInHoldMode = false;
+  anim.holdExitProgress = 0.0f;
 
 
   if (!ctx.entity)
   if (!ctx.entity)
     return anim;
     return anim;
@@ -95,7 +97,13 @@ AnimationInputs HumanoidRendererBase::sampleAnimState(const DrawContext &ctx) {
       ctx.entity->getComponent<Engine::Core::AttackTargetComponent>();
       ctx.entity->getComponent<Engine::Core::AttackTargetComponent>();
   auto *transform =
   auto *transform =
       ctx.entity->getComponent<Engine::Core::TransformComponent>();
       ctx.entity->getComponent<Engine::Core::TransformComponent>();
+  auto *holdMode = ctx.entity->getComponent<Engine::Core::HoldModeComponent>();
 
 
+  anim.isInHoldMode = (holdMode && holdMode->active);
+  if (holdMode && !holdMode->active && holdMode->exitCooldown > 0.0f) {
+    anim.holdExitProgress =
+        1.0f - (holdMode->exitCooldown / holdMode->standUpDuration);
+  }
   anim.isMoving = (movement && movement->hasTarget);
   anim.isMoving = (movement && movement->hasTarget);
 
 
   if (attack && attackTarget && attackTarget->targetId > 0 && transform) {
   if (attack && attackTarget && attackTarget->targetId > 0 && transform) {

+ 2 - 0
render/humanoid_base.h

@@ -20,6 +20,8 @@ struct AnimationInputs {
   bool isMoving;
   bool isMoving;
   bool isAttacking;
   bool isAttacking;
   bool isMelee;
   bool isMelee;
+  bool isInHoldMode;
+  float holdExitProgress;
 };
 };
 
 
 struct FormationParams {
 struct FormationParams {

+ 4 - 4
ui/qml/HUDBottom.qml

@@ -339,10 +339,10 @@ RowLayout {
                 focusPolicy: Qt.NoFocus
                 focusPolicy: Qt.NoFocus
                 enabled: bottomRoot.hasMovableUnits
                 enabled: bottomRoot.hasMovableUnits
                 onClicked: {
                 onClicked: {
-                    bottomRoot.commandModeChanged("hold");
-                    Qt.callLater(function() {
-                        bottomRoot.commandModeChanged("normal");
-                    });
+                    if (typeof game !== 'undefined' && game.onHoldCommand)
+                        game.onHoldCommand();
+
+                    bottomRoot.commandModeChanged("normal");
                 }
                 }
                 ToolTip.visible: hovered
                 ToolTip.visible: hovered
                 ToolTip.text: bottomRoot.hasMovableUnits ? "Hold position and defend" : "Select troops first"
                 ToolTip.text: bottomRoot.hasMovableUnits ? "Hold position and defend" : "Select troops first"