From 8c713f73c18358b28b30252ee5f98dc7081ab9ff Mon Sep 17 00:00:00 2001 From: Andrej Dudenhefner Date: Thu, 6 Nov 2025 15:13:00 +0100 Subject: [PATCH 1/6] Fix violated assert(direction != BattleHex::NONE) in BattleFieldController::fromWhichHexAttack when unit surrounded --- client/battle/BattleActionsController.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/battle/BattleActionsController.cpp b/client/battle/BattleActionsController.cpp index dfa14e95a..48a5ffc39 100644 --- a/client/battle/BattleActionsController.cpp +++ b/client/battle/BattleActionsController.cpp @@ -653,7 +653,8 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, c case PossiblePlayerBattleAction::WALK_AND_ATTACK: case PossiblePlayerBattleAction::ATTACK_AND_RETURN: { - if (owner.fieldController->isTileAttackable(targetHex)) // move isTileAttackable to be part of battleCanAttack? + auto activeStackHex = owner.stacksController->getActiveStack()->getPosition(); + if (targetHex != activeStackHex && owner.fieldController->isTileAttackable(targetHex)) // move isTileAttackable to be part of battleCanAttack? { BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex); if(owner.getBattle()->battleCanAttack(owner.stacksController->getActiveStack(), targetStack, attackFromHex)) From 7161ce858edb4769aad268d958713c246cae8144 Mon Sep 17 00:00:00 2001 From: Andrej Dudenhefner Date: Thu, 6 Nov 2025 18:11:10 +0100 Subject: [PATCH 2/6] Consider wide units (avoiding self attack trouble) --- client/battle/BattleActionsController.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/battle/BattleActionsController.cpp b/client/battle/BattleActionsController.cpp index 48a5ffc39..77e968fc0 100644 --- a/client/battle/BattleActionsController.cpp +++ b/client/battle/BattleActionsController.cpp @@ -653,11 +653,11 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, c case PossiblePlayerBattleAction::WALK_AND_ATTACK: case PossiblePlayerBattleAction::ATTACK_AND_RETURN: { - auto activeStackHex = owner.stacksController->getActiveStack()->getPosition(); - if (targetHex != activeStackHex && owner.fieldController->isTileAttackable(targetHex)) // move isTileAttackable to be part of battleCanAttack? + auto activeStack = owner.stacksController->getActiveStack(); + if (targetStack && targetStack != activeStack && owner.fieldController->isTileAttackable(targetHex)) // move isTileAttackable to be part of battleCanAttack? { BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex); - if(owner.getBattle()->battleCanAttack(owner.stacksController->getActiveStack(), targetStack, attackFromHex)) + if(owner.getBattle()->battleCanAttack(activeStack, targetStack, attackFromHex)) return true; } return false; From 3b72a74161bcbb5936f98350288cd027b91b0d5c Mon Sep 17 00:00:00 2001 From: Andrej Dudenhefner Date: Fri, 7 Nov 2025 11:44:56 +0100 Subject: [PATCH 3/6] Add isMeleeAttacker to Unit --- lib/battle/CBattleInfoCallback.cpp | 14 ++++++++------ lib/battle/CUnitState.cpp | 16 ++++++++++++++++ lib/battle/CUnitState.h | 1 + lib/battle/Unit.h | 1 + 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index f33c98207..ce0fae897 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -280,8 +280,11 @@ std::vector CBattleInfoCallback::getClientActionsFor if(stack->hasBonusOfType(BonusType::RETURN_AFTER_STRIKE)) allowedActionList.push_back(PossiblePlayerBattleAction::ATTACK_AND_RETURN); - allowedActionList.push_back(PossiblePlayerBattleAction::ATTACK); //all active stacks can attack - allowedActionList.push_back(PossiblePlayerBattleAction::WALK_AND_ATTACK); //not all stacks can always walk, but we will check this elsewhere + if (stack->isMeleeAttacker()) //not all stacks can actually attack or walk and attack, check this elsewhere + { + allowedActionList.push_back(PossiblePlayerBattleAction::ATTACK); + allowedActionList.push_back(PossiblePlayerBattleAction::WALK_AND_ATTACK); + } if(stack->canMove() && stack->getMovementRange(0)) //probably no reason to try move war machines or bound stacks allowedActionList.push_back(PossiblePlayerBattleAction::MOVE_STACK); @@ -683,6 +686,9 @@ bool CBattleInfoCallback::battleCanAttack(const battle::Unit * stack, const batt if(!battleMatchOwner(stack, target)) return false; + if (!stack->isMeleeAttacker()) + return false; + if (stack->getPosition() != dest) { for (const auto & obstacle : battleGetAllObstacles()) @@ -695,10 +701,6 @@ bool CBattleInfoCallback::battleCanAttack(const battle::Unit * stack, const batt } } - auto id = stack->unitType()->getId(); - if (id == CreatureID::FIRST_AID_TENT || id == CreatureID::CATAPULT) - return false; - return target->alive(); } diff --git a/lib/battle/CUnitState.cpp b/lib/battle/CUnitState.cpp index a6b64a472..237d8eef5 100644 --- a/lib/battle/CUnitState.cpp +++ b/lib/battle/CUnitState.cpp @@ -545,6 +545,22 @@ bool CUnitState::isShooter() const return shots.total() > 0; } +bool CUnitState::isMeleeAttacker() const +{ + //exclude non melee attackers + static const std::set nonMeleeAttackers{ + CreatureID::FIRST_AID_TENT, + CreatureID::CATAPULT, + CreatureID::BALLISTA, + CreatureID::AMMO_CART, + CreatureID::ARROW_TOWERS + }; + if (vstd::contains(nonMeleeAttackers, creatureId())) + return false; + + return true; +} + int32_t CUnitState::getKilled() const { int32_t res = unitBaseAmount() - health.getCount() + health.getResurrected(); diff --git a/lib/battle/CUnitState.h b/lib/battle/CUnitState.h index b1ec4a6d4..9dede8c62 100644 --- a/lib/battle/CUnitState.h +++ b/lib/battle/CUnitState.h @@ -205,6 +205,7 @@ public: bool canShootBlocked() const override; bool canShoot() const override; bool isShooter() const override; + bool isMeleeAttacker() const override; int32_t getKilled() const override; int32_t getCount() const override; diff --git a/lib/battle/Unit.h b/lib/battle/Unit.h index 37affcc0f..439ab0102 100644 --- a/lib/battle/Unit.h +++ b/lib/battle/Unit.h @@ -97,6 +97,7 @@ public: virtual bool canShootBlocked() const = 0; virtual bool canShoot() const = 0; virtual bool isShooter() const = 0; + virtual bool isMeleeAttacker() const = 0; /// returns initial size of this unit virtual int32_t getCount() const = 0; From 31c093baae8732152a4e9bd7ad6910740e0fe795 Mon Sep 17 00:00:00 2001 From: Andrej Dudenhefner Date: Fri, 7 Nov 2025 12:01:51 +0100 Subject: [PATCH 4/6] Remove condition that a unit can always attack itself --- client/battle/BattleFieldController.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/battle/BattleFieldController.cpp b/client/battle/BattleFieldController.cpp index c21788ed7..6305c7300 100644 --- a/client/battle/BattleFieldController.cpp +++ b/client/battle/BattleFieldController.cpp @@ -819,7 +819,7 @@ bool BattleFieldController::isTileAttackable(const BattleHex & number) const for (auto & elem : occupiableHexes) { - if (BattleHex::mutualPosition(elem, number) != BattleHex::EDir::NONE || elem == number) + if (BattleHex::mutualPosition(elem, number) != BattleHex::EDir::NONE) return true; } return false; From 7c4b7aa3f0cacecb77d278c4984239fcdc6bd85c Mon Sep 17 00:00:00 2001 From: Andrej Dudenhefner Date: Fri, 7 Nov 2025 12:13:16 +0100 Subject: [PATCH 5/6] Move isMeleeAttacker to Unit --- lib/battle/CUnitState.cpp | 16 ---------------- lib/battle/CUnitState.h | 1 - lib/battle/Unit.cpp | 16 ++++++++++++++++ lib/battle/Unit.h | 2 +- 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/lib/battle/CUnitState.cpp b/lib/battle/CUnitState.cpp index 237d8eef5..a6b64a472 100644 --- a/lib/battle/CUnitState.cpp +++ b/lib/battle/CUnitState.cpp @@ -545,22 +545,6 @@ bool CUnitState::isShooter() const return shots.total() > 0; } -bool CUnitState::isMeleeAttacker() const -{ - //exclude non melee attackers - static const std::set nonMeleeAttackers{ - CreatureID::FIRST_AID_TENT, - CreatureID::CATAPULT, - CreatureID::BALLISTA, - CreatureID::AMMO_CART, - CreatureID::ARROW_TOWERS - }; - if (vstd::contains(nonMeleeAttackers, creatureId())) - return false; - - return true; -} - int32_t CUnitState::getKilled() const { int32_t res = unitBaseAmount() - health.getCount() + health.getResurrected(); diff --git a/lib/battle/CUnitState.h b/lib/battle/CUnitState.h index 9dede8c62..b1ec4a6d4 100644 --- a/lib/battle/CUnitState.h +++ b/lib/battle/CUnitState.h @@ -205,7 +205,6 @@ public: bool canShootBlocked() const override; bool canShoot() const override; bool isShooter() const override; - bool isMeleeAttacker() const override; int32_t getKilled() const override; int32_t getCount() const override; diff --git a/lib/battle/Unit.cpp b/lib/battle/Unit.cpp index f3eb42c2a..3c579a0b7 100644 --- a/lib/battle/Unit.cpp +++ b/lib/battle/Unit.cpp @@ -38,6 +38,22 @@ bool Unit::isTurret() const return creatureIndex() == CreatureID::ARROW_TOWERS; } +bool Unit::isMeleeAttacker() const +{ + //exclude non melee attackers + static const std::set nonMeleeAttackers{ + CreatureID::FIRST_AID_TENT, + CreatureID::CATAPULT, + CreatureID::BALLISTA, + CreatureID::AMMO_CART, + CreatureID::ARROW_TOWERS + }; + if (vstd::contains(nonMeleeAttackers, creatureId())) + return false; + + return true; +} + std::string Unit::getDescription() const { boost::format fmt("Unit %d of side %d"); diff --git a/lib/battle/Unit.h b/lib/battle/Unit.h index 439ab0102..790a5be22 100644 --- a/lib/battle/Unit.h +++ b/lib/battle/Unit.h @@ -97,7 +97,7 @@ public: virtual bool canShootBlocked() const = 0; virtual bool canShoot() const = 0; virtual bool isShooter() const = 0; - virtual bool isMeleeAttacker() const = 0; + bool isMeleeAttacker() const; /// returns initial size of this unit virtual int32_t getCount() const = 0; From 3b68389d676ee4b095a9d5f199db46280b58ce42 Mon Sep 17 00:00:00 2001 From: Andrej Dudenhefner Date: Tue, 11 Nov 2025 19:38:25 +0100 Subject: [PATCH 6/6] extend isMeleeAttacker() == false to all war machines --- lib/battle/Unit.cpp | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/lib/battle/Unit.cpp b/lib/battle/Unit.cpp index 3c579a0b7..35946461d 100644 --- a/lib/battle/Unit.cpp +++ b/lib/battle/Unit.cpp @@ -40,17 +40,11 @@ bool Unit::isTurret() const bool Unit::isMeleeAttacker() const { - //exclude non melee attackers - static const std::set nonMeleeAttackers{ - CreatureID::FIRST_AID_TENT, - CreatureID::CATAPULT, - CreatureID::BALLISTA, - CreatureID::AMMO_CART, - CreatureID::ARROW_TOWERS - }; - if (vstd::contains(nonMeleeAttackers, creatureId())) + //exclude war machines + if (hasBonusOfType(BonusType::SIEGE_WEAPON)) return false; + //TODO consider that a mod may introduce a melee war machine. Possibly a new bonus type NO_MELEE_ATTACK is needed. return true; }