Browse Source

refactor logic to CBattleInfoCallback and leave UI in BattleFieldController, BattleActionsController

- move fromWhichHexAttack to CBattleInfoCallback
- add toWhichHexMove (unifying incoherent duplicates)
- add battleGetOccupiableHexes
- add battleCanAttackHex for spatial attack check
- add battleCanAttackUnit for non-spatial attack check
- add headDirection to Unit (removing destShiftDir from CStack)
- remove redundant game logic from selectAttackDirection
- remove redundant game logic from BattleFieldController and BattleActionsController
- fix no consideration for double-wide tail attack in BattleFlowProcessor
- fix #6302 wrong moat stopping condition
- throw exception on attacker nullptr in battleCanAttackHex, fromWhichHexAttack
- safer actionIsLegal on attack, move
- remove redundant canStackMoveHere from ui code
- throw exception on nullptr unit in battleGetOccupiableHexes
- ensure activeStack in redrawBackgroundWithHexes
- test point validity in selectAttackDirection
Andrej Dudenhefner 3 weeks ago
parent
commit
e72dcce8ba

+ 16 - 51
client/battle/BattleActionsController.cpp

@@ -527,7 +527,8 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle
 		case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return
 			{
 				const auto * attacker = owner.stacksController->getActiveStack();
-				BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex);
+				BattleHex attackFromHex = owner.getBattle()->fromWhichHexAttack(attacker, targetHex, owner.fieldController->selectAttackDirection(targetHex));
+				assert(attackFromHex.isValid());
 				int distance = attacker->position.isValid() ? owner.getBattle()->battleGetDistances(attacker, attacker->getPosition())[attackFromHex.toInt()] : 0;
 				DamageEstimation retaliation;
 				BattleAttackInfo attackInfo(attacker, targetStack, distance, false );
@@ -644,8 +645,8 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, c
 		case PossiblePlayerBattleAction::MOVE_STACK:
 			if (!(targetStack && targetStack->alive())) //we can walk on dead stacks
 			{
-				if(canStackMoveHere(owner.stacksController->getActiveStack(), targetHex))
-					return true;
+				const CStack * currentStack = owner.stacksController->getActiveStack();
+				return currentStack && owner.getBattle()->toWhichHexMove(currentStack, targetHex).isValid();
 			}
 			return false;
 
@@ -653,16 +654,11 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, c
 		case PossiblePlayerBattleAction::WALK_AND_ATTACK:
 		case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
 			{
-				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(activeStack, targetStack, attackFromHex))
-						return true;
-				}
-				return false;
+				const CStack * currentStack = owner.stacksController->getActiveStack();
+				return currentStack &&
+					owner.getBattle()->battleCanAttackUnit(currentStack, targetStack) &&
+					owner.getBattle()->battleCanAttackHex(currentStack, targetHex);
 			}
-
 		case PossiblePlayerBattleAction::SHOOT:
 			{
 				auto currentStack = owner.stacksController->getActiveStack();
@@ -740,26 +736,9 @@ void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, c
 		case PossiblePlayerBattleAction::MOVE_STACK:
 		{
 			const auto * activeStack = owner.stacksController->getActiveStack();
-
-			if(activeStack->doubleWide())
-			{
-				BattleHexArray availableHexes = owner.getBattle()->battleGetAvailableHexes(activeStack, false);
-				BattleHex shiftedDest = targetHex.cloneInDirection(activeStack->destShiftDir(), false);
-				const bool canMoveHeadHere = availableHexes.contains(targetHex);
-				const bool canMoveTailHere = availableHexes.contains(shiftedDest);
-				const bool backwardsMove = activeStack->unitSide() == BattleSide::ATTACKER ?
-											   targetHex.getX() < activeStack->getPosition().getX():
-											   targetHex.getX() > activeStack->getPosition().getX();
-
-				if(canMoveTailHere && (backwardsMove || !canMoveHeadHere))
-					owner.giveCommand(EActionType::WALK, shiftedDest);
-				else
-					owner.giveCommand(EActionType::WALK, targetHex);
-			}
-			else
-			{
-				owner.giveCommand(EActionType::WALK, targetHex);
-			}
+			auto toHex = owner.getBattle()->toWhichHexMove(activeStack, targetHex);
+			assert(toHex.isValid());
+			owner.giveCommand(EActionType::WALK, toHex);
 			return;
 		}
 
@@ -768,12 +747,11 @@ void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, c
 		case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return
 		{
 			bool returnAfterAttack = action.get() == PossiblePlayerBattleAction::ATTACK_AND_RETURN;
-			BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex);
-			if(attackFromHex.isValid()) //we can be in this line when unreachable creature is L - clicked (as of revision 1308)
-			{
-				BattleAction command = BattleAction::makeMeleeAttack(owner.stacksController->getActiveStack(), targetHex, attackFromHex, returnAfterAttack);
-				owner.sendCommand(command, owner.stacksController->getActiveStack());
-			}
+			auto attacker = owner.stacksController->getActiveStack();
+			BattleHex attackFromHex = owner.getBattle()->fromWhichHexAttack(attacker, targetHex, owner.fieldController->selectAttackDirection(targetHex));
+			assert(attackFromHex.isValid());
+			BattleAction command = BattleAction::makeMeleeAttack(attacker, targetHex, attackFromHex, returnAfterAttack);
+			owner.sendCommand(command, attacker);
 			return;
 		}
 
@@ -1032,19 +1010,6 @@ bool BattleActionsController::isCastingPossibleHere(const CSpell * currentSpell,
 	return m->canBeCastAt(target, problem);
 }
 
-bool BattleActionsController::canStackMoveHere(const CStack * stackToMove, const BattleHex & myNumber) const
-{
-	BattleHexArray acc = owner.getBattle()->battleGetAvailableHexes(stackToMove, false);
-	BattleHex shiftedDest = myNumber.cloneInDirection(stackToMove->destShiftDir(), false);
-
-	if (acc.contains(myNumber))
-		return true;
-	else if (stackToMove->doubleWide() && acc.contains(shiftedDest))
-		return true;
-	else
-		return false;
-}
-
 void BattleActionsController::activateStack()
 {
 	const CStack * s = owner.stacksController->getActiveStack();

+ 0 - 1
client/battle/BattleActionsController.h

@@ -45,7 +45,6 @@ class BattleActionsController
 	const CStack * selectedStack;
 
 	bool isCastingPossibleHere (const CSpell * spell, const CStack *shere, const BattleHex & myNumber);
-	bool canStackMoveHere (const CStack *sactive, const BattleHex & MyNumber) const; //TODO: move to BattleState / callback
 	std::vector<PossiblePlayerBattleAction> getPossibleActionsForStack (const CStack *stack) const; //called when stack gets its turn
 	void reorderPossibleActionsPriority(const CStack * stack, const CStack * targetStack);
 

+ 43 - 178
client/battle/BattleFieldController.cpp

@@ -263,9 +263,10 @@ void BattleFieldController::showBackgroundImageWithHexes(Canvas & canvas)
 void BattleFieldController::redrawBackgroundWithHexes()
 {
 	const CStack *activeStack = owner.stacksController->getActiveStack();
-	BattleHexArray attackableHexes;
 	if(activeStack)
-		occupiableHexes = owner.getBattle()->battleGetAvailableHexes(activeStack, false, true, &attackableHexes);
+		availableHexes = owner.getBattle()->battleGetAvailableHexes(activeStack, false);
+	else
+		availableHexes.clear();
 
 	// prepare background graphic with hexes and shaded hexes
 	backgroundWithHexes->draw(background, Point(0,0));
@@ -274,13 +275,16 @@ void BattleFieldController::redrawBackgroundWithHexes()
 		owner.siegeController->showAbsoluteObstacles(*backgroundWithHexes);
 
 	// show shaded hexes for active's stack valid movement and the hexes that it can attack
-	if(settings["battle"]["stackRange"].Bool())
+	if(activeStack && settings["battle"]["stackRange"].Bool())
 	{
-		BattleHexArray hexesToShade = occupiableHexes;
-		hexesToShade.insert(attackableHexes);
-		for(const BattleHex & hex : hexesToShade)
+		auto occupiableHexes = owner.getBattle()->battleGetOccupiableHexes(availableHexes, activeStack);
+		for(si16 hex = 0; hex < GameConstants::BFIELD_SIZE; hex++)
 		{
-			showHighlightedHex(*backgroundWithHexes, cellShade, hex, false);
+			//shade occupiable and attackable hexes
+			if (occupiableHexes.contains(hex) ||
+				(owner.getBattle()->battleCanAttackUnit(activeStack, owner.getBattle()->battleGetStackByPos(hex, true)) &&
+					owner.getBattle()->battleCanAttackHex(availableHexes, activeStack, hex)))
+				showHighlightedHex(*backgroundWithHexes, cellShade, hex, false);
 		}
 	}
 
@@ -330,10 +334,7 @@ BattleHexArray BattleFieldController::getMovementRangeForHoveredStack()
 		return BattleHexArray();
 
 	auto hoveredStack = getHoveredStack();
-	if(hoveredStack)
-		return owner.getBattle()->battleGetAvailableHexes(hoveredStack, true, true, nullptr);
-	else
-		return BattleHexArray();
+	return hoveredStack ? owner.getBattle()->battleGetOccupiableHexes(hoveredStack, true) : BattleHexArray();
 }
 
 BattleHexArray BattleFieldController::getHighlightedHexesForSpellRange()
@@ -371,45 +372,26 @@ BattleHexArray BattleFieldController::getHighlightedHexesForMovementTarget()
 	if(!stack)
 		return {};
 
-	BattleHexArray availableHexes = owner.getBattle()->battleGetAvailableHexes(stack, false, false, nullptr);
-
 	auto hoveredStack = owner.getBattle()->battleGetStackByPos(hoveredHex, true);
 
-	if(owner.getBattle()->battleCanAttack(stack, hoveredStack, hoveredHex) && isTileAttackable(hoveredHex))
+	if(owner.getBattle()->battleCanAttackUnit(stack, hoveredStack) && owner.getBattle()->battleCanAttackHex(availableHexes, stack, hoveredHex))
 	{
-		BattleHex attackFromHex = fromWhichHexAttack(hoveredHex);
-		if(owner.getBattle()->battleCanAttack(stack, hoveredStack, attackFromHex))
-		{
-			if(stack->doubleWide())
-				return {attackFromHex, stack->occupiedHex(attackFromHex)};
+		BattleHex fromHex = owner.getBattle()->fromWhichHexAttack(stack, hoveredHex, selectAttackDirection(hoveredHex));
+		assert(fromHex.isValid());
+		if(stack->doubleWide())
+			return {fromHex, stack->occupiedHex(fromHex)};
 
-			return {attackFromHex};
-		}
+		return {fromHex};
 	}
 
-	if (stack->doubleWide())
-	{
-		const bool canMoveHeadHere = hoveredHex.isAvailable() && availableHexes.contains(hoveredHex);
-		const bool canMoveTailHere = hoveredHex.isAvailable() && availableHexes.contains(hoveredHex.cloneInDirection(stack->destShiftDir()));
-		const bool backwardsMove = stack->unitSide() == BattleSide::ATTACKER ?
-									   hoveredHex.getX() < stack->getPosition().getX():
-									   hoveredHex.getX() > stack->getPosition().getX();
-
-		if(canMoveTailHere && (backwardsMove || !canMoveHeadHere))
-			return {hoveredHex, hoveredHex.cloneInDirection(stack->destShiftDir())};
-
-		if (canMoveHeadHere)
-			return {hoveredHex, stack->occupiedHex(hoveredHex)};
-
+	auto toHex = owner.getBattle()->toWhichHexMove(availableHexes, stack, hoveredHex);
+	if (!toHex.isValid())
 		return {};
-	}
+	
+	if (stack->doubleWide())
+		return {toHex, stack->occupiedHex(toHex)};
 	else
-	{
-		if (availableHexes.contains(hoveredHex))
-			return {hoveredHex};
-		else
-			return {};
-	}
+		return {toHex};
 }
 
 // Range limit highlight helpers
@@ -667,162 +649,45 @@ BattleHex BattleFieldController::getHexAtPosition(Point hoverPos)
 
 BattleHex::EDir BattleFieldController::selectAttackDirection(const BattleHex & myNumber) const
 {
-	const bool doubleWide = owner.stacksController->getActiveStack()->doubleWide();
+	auto attacker = owner.stacksController->getActiveStack();
+	assert(attacker);
 	const BattleHexArray & neighbours = myNumber.getAllNeighbouringTiles();
-	//   0 1
-	//  5 x 2
-	//   4 3
-
-	// if true - our current stack can move into this hex (and attack)
-	std::array<bool, 8> attackAvailability;
-
-	if (doubleWide)
-	{
-		// For double-hexes we need to ensure that both hexes needed for this direction are occupyable:
-		// |    -0-   |   -1-    |    -2-   |   -3-    |    -4-   |   -5-    |    -6-   |   -7-
-		// |  o o -   |   - o o  |    - -   |   - -    |    - -   |   - -    |    o o   |   - -
-		// |   - x -  |  - x -   |   - x o o|  - x -   |   - x -  |o o x -   |   - x -  |  - x -
-		// |    - -   |   - -    |    - -   |   - o o  |  o o -   |   - -    |    - -   |   o o
-
-		for (size_t i : { 1, 2, 3})
-		{
-			BattleHex target = neighbours[i].cloneInDirection(BattleHex::RIGHT, false);
-			attackAvailability[i] = neighbours[i].isValid() && occupiableHexes.contains(neighbours[i]) && target.isValid() && occupiableHexes.contains(target);
-		}
-
-		for (size_t i : { 4, 5, 0})
-		{
-			BattleHex target = neighbours[i].cloneInDirection(BattleHex::LEFT, false);
-			attackAvailability[i] = neighbours[i].isValid() && occupiableHexes.contains(neighbours[i]) && target.isValid() && occupiableHexes.contains(target);
-		}
-
-		attackAvailability[6] = neighbours[0].isValid() && neighbours[1].isValid() && occupiableHexes.contains(neighbours[0]) && occupiableHexes.contains(neighbours[1]);
-		attackAvailability[7] = neighbours[3].isValid() && neighbours[4].isValid() && occupiableHexes.contains(neighbours[3]) && occupiableHexes.contains(neighbours[4]);
-	}
-	else
-	{
-		for (size_t i = 0; i < 6; ++i)
-			attackAvailability[i] = neighbours[i].isValid() && occupiableHexes.contains(neighbours[i]);
-
-		attackAvailability[6] = false;
-		attackAvailability[7] = false;
-	}
-
-	// Zero available tiles to attack from
-	if ( vstd::find(attackAvailability, true) == attackAvailability.end())
-	{
-		logGlobal->error("Error: cannot find a hex to attack hex %d from!", myNumber);
-		return BattleHex::NONE;
-	}
-
 	// For each valid direction, select position to test against
 	std::array<Point, 8> testPoint;
+	testPoint.fill(Point::makeInvalid());
 
 	for (size_t i = 0; i < 6; ++i)
-		if (attackAvailability[i])
+		if (owner.getBattle()->battleCanAttackHex(availableHexes, attacker, myNumber, BattleHex::EDir(i)))
 			testPoint[i] = hexPositionAbsolute(neighbours[i]).center();
 
 	// For bottom/top directions select central point, but move it a bit away from true center to reduce zones allocated to them
-	if (attackAvailability[6])
+	if (owner.getBattle()->battleCanAttackHex(availableHexes, attacker, myNumber, BattleHex::EDir(6)))
 		testPoint[6] = (hexPositionAbsolute(neighbours[0]).center() + hexPositionAbsolute(neighbours[1]).center()) / 2 + Point(0, -5);
 
-	if (attackAvailability[7])
+	if (owner.getBattle()->battleCanAttackHex(availableHexes, attacker, myNumber, BattleHex::EDir(7)))
 		testPoint[7] = (hexPositionAbsolute(neighbours[3]).center() + hexPositionAbsolute(neighbours[4]).center()) / 2 + Point(0,  5);
 
 	// Compute distance between tested position & cursor position and pick nearest
-	std::array<int, 8> distance2;
-
-	for (size_t i = 0; i < 8; ++i)
-		if (attackAvailability[i])
-			distance2[i] = (testPoint[i].y - currentAttackOriginPoint.y)*(testPoint[i].y - currentAttackOriginPoint.y) + (testPoint[i].x - currentAttackOriginPoint.x)*(testPoint[i].x - currentAttackOriginPoint.x);
-
+	int nearestDistance = std::numeric_limits<int>::max();
 	size_t nearest = -1;
-	for (size_t i = 0; i < 8; ++i)
-		if (attackAvailability[i] && (nearest == -1 || distance2[i] < distance2[nearest]) )
-			nearest = i;
-
-	assert(nearest != -1);
-	return BattleHex::EDir(nearest);
-}
-
-BattleHex BattleFieldController::fromWhichHexAttack(const BattleHex & attackTarget)
-{
-	BattleHex::EDir direction = selectAttackDirection(attackTarget);
-
-	const CStack * attacker = owner.stacksController->getActiveStack();
 
-	assert(direction != BattleHex::NONE);
-	assert(attacker);
-
-	if (!attacker->doubleWide())
-	{
-		assert(direction != BattleHex::BOTTOM);
-		assert(direction != BattleHex::TOP);
-		return attackTarget.cloneInDirection(direction);
-	}
-	else
+	for (size_t i = 0; i < 8; ++i)
 	{
-		// We need to find position of right hex of double-hex creature (or left for defending side)
-		// | TOP_LEFT |TOP_RIGHT |   RIGHT  |BOTTOM_RIGHT|BOTTOM_LEFT|  LEFT    |    TOP   |BOTTOM
-		// |  o o -   |   - o o  |    - -   |   - -      |    - -    |   - -    |    o o   |   - -
-		// |   - x -  |  - x -   |   - x o o|  - x -     |   - x -   |o o x -   |   - x -  |  - x -
-		// |    - -   |   - -    |    - -   |   - o o    |  o o -    |   - -    |    - -   |   o o
-
-		switch (direction)
+		if (testPoint[i].isValid())
 		{
-		case BattleHex::TOP_LEFT:
-		case BattleHex::LEFT:
-		case BattleHex::BOTTOM_LEFT:
-		{
-			if ( attacker->unitSide() == BattleSide::ATTACKER )
-				return attackTarget.cloneInDirection(direction);
-			else
-				return attackTarget.cloneInDirection(direction).cloneInDirection(BattleHex::LEFT);
-		}
-
-		case BattleHex::TOP_RIGHT:
-		case BattleHex::RIGHT:
-		case BattleHex::BOTTOM_RIGHT:
-		{
-			if ( attacker->unitSide() == BattleSide::ATTACKER )
-				return attackTarget.cloneInDirection(direction).cloneInDirection(BattleHex::RIGHT);
-			else
-				return attackTarget.cloneInDirection(direction);
-		}
-
-		case BattleHex::TOP:
-		{
-			if ( attacker->unitSide() == BattleSide::ATTACKER )
-				return attackTarget.cloneInDirection(BattleHex::TOP_RIGHT);
-			else
-				return attackTarget.cloneInDirection(BattleHex::TOP_LEFT);
-		}
-
-		case BattleHex::BOTTOM:
-		{
-			if ( attacker->unitSide() == BattleSide::ATTACKER )
-				return attackTarget.cloneInDirection(BattleHex::BOTTOM_RIGHT);
-			else
-				return attackTarget.cloneInDirection(BattleHex::BOTTOM_LEFT);
-		}
-		default:
-			assert(0);
-			return BattleHex::INVALID;
+			int distance = (testPoint[i].y - currentAttackOriginPoint.y)*(testPoint[i].y - currentAttackOriginPoint.y) + (testPoint[i].x - currentAttackOriginPoint.x)*(testPoint[i].x - currentAttackOriginPoint.x);
+			if (nearest == -1 || distance < nearestDistance)
+			{
+				nearestDistance = distance;
+				nearest = i;
+			}
 		}
 	}
-}
-
-bool BattleFieldController::isTileAttackable(const BattleHex & number) const
-{
-	if(!number.isValid())
-		return false;
 
-	for (auto & elem : occupiableHexes)
-	{
-		if (BattleHex::mutualPosition(elem, number) != BattleHex::EDir::NONE)
-			return true;
-	}
-	return false;
+	if (nearest == -1)
+		// Zero available tiles to attack from
+		logGlobal->error("Error: cannot find a hex to attack hex %d from!", myNumber);
+	return BattleHex::EDir(nearest);
 }
 
 void BattleFieldController::updateAccessibleHexes()

+ 2 - 7
client/battle/BattleFieldController.h

@@ -46,8 +46,8 @@ class BattleFieldController : public CIntObject
 	/// hex currently under mouse hover
 	BattleHex hoveredHex;
 
-	/// hexes to which currently active stack can move
-	BattleHexArray occupiableHexes;
+	/// hexes to which the currently active stack can move (for double-wide units only the head is considered)
+	BattleHexArray availableHexes;
 
 	/// hexes that when in front of a unit cause it's amount box to move back
 	std::array<bool, GameConstants::BFIELD_SIZE> stackCountOutsideHexes;
@@ -126,13 +126,8 @@ public:
 	/// Returns the currently hovered stack
 	const CStack* getHoveredStack();
 
-	/// returns true if selected tile can be attacked in melee by current stack
-	bool isTileAttackable(const BattleHex & number) const;
-
 	/// returns true if stack should render its stack count image in default position - outside own hex
 	bool stackCountOutsideHex(const BattleHex & number) const;
 
 	BattleHex::EDir selectAttackDirection(const BattleHex & myNumber) const;
-
-	BattleHex fromWhichHexAttack(const BattleHex & myNumber);
 };

+ 0 - 15
lib/CStack.cpp

@@ -107,21 +107,6 @@ si32 CStack::magicResistance() const
 	return static_cast<si32>(100 - castChance);
 }
 
-BattleHex::EDir CStack::destShiftDir() const
-{
-	if(doubleWide())
-	{
-		if(side == BattleSide::ATTACKER)
-			return BattleHex::EDir::RIGHT;
-		else
-			return BattleHex::EDir::LEFT;
-	}
-	else
-	{
-		return BattleHex::EDir::NONE;
-	}
-}
-
 std::vector<SpellID> CStack::activeSpells() const
 {
 	std::vector<SpellID> ret;

+ 0 - 2
lib/CStack.h

@@ -66,8 +66,6 @@ public:
 	static BattleHexArray meleeAttackHexes(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos = BattleHex::INVALID, BattleHex defenderPos = BattleHex::INVALID);
 	static bool isMeleeAttackPossible(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos = BattleHex::INVALID, BattleHex defenderPos = BattleHex::INVALID);
 
-	BattleHex::EDir destShiftDir() const;
-
 	void prepareAttacked(BattleStackAttacked & bsa, vstd::RNG & rand) const; //requires bsa.damageAmount filled
 	static void prepareAttacked(BattleStackAttacked & bsa,
 								vstd::RNG & rand,

+ 131 - 53
lib/battle/CBattleInfoCallback.cpp

@@ -616,91 +616,169 @@ BattleHexArray CBattleInfoCallback::battleGetAvailableHexes(const ReachabilityIn
 	return ret;
 }
 
-BattleHexArray CBattleInfoCallback::battleGetAvailableHexes(const battle::Unit * unit, bool obtainMovementRange, bool addOccupiable, BattleHexArray * attackable) const
+BattleHexArray CBattleInfoCallback::battleGetOccupiableHexes(const battle::Unit * unit, bool obtainMovementRange) const
 {
-	BattleHexArray ret = battleGetAvailableHexes(unit, obtainMovementRange);
-
-	if(ret.empty())
-		return ret;
-
-	if(addOccupiable && unit->doubleWide())
-	{
-		BattleHexArray occupiable;
+	return battleGetOccupiableHexes(battleGetAvailableHexes(unit, obtainMovementRange), unit);
+}
 
-		for(const auto & hex : ret)
-			occupiable.insert(unit->occupiedHex(hex));
+BattleHexArray CBattleInfoCallback::battleGetOccupiableHexes(const BattleHexArray & availableHexes, const battle::Unit * unit) const
+{
+	RETURN_IF_NOT_BATTLE(BattleHexArray());
+	if (!unit)
+		throw std::runtime_error("Undefined unit in battleGetOccupiableHexes!");
 
-		ret.insert(occupiable);
+	if (unit->doubleWide())
+	{ 
+		auto occupiableHexes = BattleHexArray(availableHexes);
+		for (auto hex : availableHexes)
+			occupiableHexes.insert(unit->occupiedHex(hex));
+		return occupiableHexes;
 	}
+	return availableHexes;
+}
+
+BattleHex CBattleInfoCallback::fromWhichHexAttack(const battle::Unit * attacker, const BattleHex & target, const BattleHex::EDir & direction) const
+{
+	RETURN_IF_NOT_BATTLE(BattleHex::INVALID);
+	if (!attacker)
+		throw std::runtime_error("Undefined attacker in fromWhichHexAttack!");
 
+	if (!target.isValid() || direction == BattleHex::NONE)
+		return BattleHex::INVALID;
 
-	if(attackable)
+	bool isAttacker = attacker->unitSide() == BattleSide::ATTACKER;
+	if (attacker->doubleWide())
 	{
-		auto meleeAttackable = [&](const BattleHex & hex) -> bool
-		{
-			// Return true if given hex has at least one available neighbour.
-			// Available hexes are already present in ret vector.
-			auto availableNeighbour = boost::find_if(ret, [=] (const BattleHex & availableHex)
-			{
-				return BattleHex::mutualPosition(hex, availableHex) >= 0;
-			});
-			return availableNeighbour != ret.end();
-		};
-		for(const auto * otherSt : battleAliveUnits(otherSide(unit->unitSide())))
+		// We need to find position of right hex of double-hex creature (or left for defending side)
+		// | TOP_LEFT | TOP_RIGHT |  RIGHT  |BOTTOM_RIGHT|BOTTOM_LEFT|  LEFT   |  TOP   | BOTTOM
+		// |  o o -   |    - o o  |  - -    |   - -      |    - -    |    - -  |  o o   |   - -
+		// |   - x -  |   - x -   | - x o o |  - x -     |   - x -   | o o x - | - x -  |  - x -
+		// |    - -   |    - -    |  - -    |   - o o    |  o o -    |    - -  |  - -   |   o o
+
+		switch (direction)
 		{
-			if(!otherSt->isValidTarget(false))
-				continue;
+			case BattleHex::TOP_LEFT:
+			case BattleHex::LEFT:
+			case BattleHex::BOTTOM_LEFT:
+				return target.cloneInDirection(direction, false)
+					.cloneInDirection(isAttacker ? BattleHex::NONE : BattleHex::LEFT, false);
+
+			case BattleHex::TOP_RIGHT:
+			case BattleHex::RIGHT:
+			case BattleHex::BOTTOM_RIGHT:
+				return target.cloneInDirection(direction, false)
+					.cloneInDirection(isAttacker ? BattleHex::RIGHT : BattleHex::NONE, false);
+
+			case BattleHex::TOP:
+				return target.cloneInDirection(isAttacker ? BattleHex::TOP_RIGHT : BattleHex::TOP_LEFT, false);
+
+			case BattleHex::BOTTOM:
+				return target.cloneInDirection(isAttacker ? BattleHex::BOTTOM_RIGHT : BattleHex::BOTTOM_LEFT, false);
+
+			default:
+				return BattleHex::INVALID;
+		}
+	}
+	if (direction == BattleHex::TOP || direction == BattleHex::BOTTOM)
+		return BattleHex::INVALID;
+	return target.cloneInDirection(direction, false);
+}
 
-			const BattleHexArray & occupied = otherSt->getHexes();
+BattleHex CBattleInfoCallback::toWhichHexMove(const battle::Unit * unit, const BattleHex & position) const
+{
+	return toWhichHexMove(battleGetAvailableHexes(unit, false), unit, position);
+}
 
-			if(battleCanShoot(unit, otherSt->getPosition()))
-			{
-				attackable->insert(occupied);
-				continue;
-			}
+BattleHex CBattleInfoCallback::toWhichHexMove(const BattleHexArray & availableHexes, const battle::Unit * unit, const BattleHex & position) const
+{
+	RETURN_IF_NOT_BATTLE(false);
 
-			for(const BattleHex & he : occupied)
-			{
-				if(meleeAttackable(he))
-					attackable->insert(he);
-			}
-		}
+	if (!unit)
+		throw std::runtime_error("Undefined unit in toWhichHexMove!");
+	if (!position.isValid())
+		return BattleHex::INVALID;
+
+	if (availableHexes.contains(position))
+		return position;
+	if (unit->doubleWide())
+	{
+		auto headPosition = position.cloneInDirection(unit->headDirection(), false);
+		if (availableHexes.contains(headPosition))
+			return headPosition;
 	}
+	return BattleHex::INVALID;
+}
 
-	return ret;
+bool CBattleInfoCallback::battleCanAttackHex(const battle::Unit * attacker, const BattleHex & position) const
+{
+	return battleCanAttackHex(battleGetAvailableHexes(attacker, false), attacker, position);
 }
 
-bool CBattleInfoCallback::battleCanAttack(const battle::Unit * stack, const battle::Unit * target, const BattleHex & dest) const
+bool CBattleInfoCallback::battleCanAttackHex(const BattleHexArray & availableHexes, const battle::Unit * attacker, const BattleHex & position) const
 {
-	RETURN_IF_NOT_BATTLE(false);
+	for (auto direction = 0; direction < 8; direction++)
+	{
+		if (battleCanAttackHex(availableHexes, attacker, position, BattleHex::EDir(direction)))
+			return true;
+	}
+	return false;
+}
 
-	if(battleTacticDist())
-		return false;
+bool CBattleInfoCallback::battleCanAttackHex(const battle::Unit * attacker, const BattleHex & position, const BattleHex::EDir & direction) const
+{
+	return battleCanAttackHex(battleGetAvailableHexes(attacker, false), attacker, position, direction);
+}
 
-	if (!stack || !target)
-		return false;
+bool CBattleInfoCallback::battleCanAttackHex(const BattleHexArray & availableHexes, const battle::Unit * attacker, const BattleHex & position, const BattleHex::EDir & direction) const
+{
+	RETURN_IF_NOT_BATTLE(false);
 
-	if(target->isInvincible())
-		return false;
+	if (!attacker)
+		throw std::runtime_error("Undefined attacker in battleCanAttackHex!");
 
-	if(!battleMatchOwner(stack, target))
+	if (!position.isValid() || direction == BattleHex::NONE)
 		return false;
 
-	if (!stack->isMeleeAttacker())
+	BattleHex fromHex = fromWhichHexAttack(attacker, position, direction);
+
+	//check if the attack is performed from an available hex
+	if (!fromHex.isValid() || !availableHexes.contains(fromHex))
 		return false;
 
-	if (stack->getPosition() != dest)
+	//if the movement ends in an obstacle, check if the obstacle allows attacking from that position
+	if (attacker->getPosition() != fromHex)
 	{
 		for (const auto & obstacle : battleGetAllObstacles())
 		{
-			if (obstacle->getStoppingTile().contains(dest))
+			if (obstacle->getStoppingTile().contains(fromHex))
 				return false;
-
-			if (stack->doubleWide() && obstacle->getStoppingTile().contains(stack->occupiedHex(dest)))
+			if (attacker->doubleWide() && obstacle->getStoppingTile().contains(attacker->occupiedHex(fromHex)))
 				return false;
 		}
 	}
 
+	return true;
+}
+
+bool CBattleInfoCallback::battleCanAttackUnit(const battle::Unit * attacker, const battle::Unit * target) const
+{
+	RETURN_IF_NOT_BATTLE(false);
+
+	if(battleTacticDist())
+		return false;
+
+	if (!attacker)
+		throw std::runtime_error("Undefined attacker in battleCanAttackUnit!");
+
+	if(!target || target->isInvincible())
+		return false;
+
+	if(attacker == target || !battleMatchOwner(attacker, target))
+		return false;
+
+	if (!attacker->isMeleeAttacker())
+		return false;
+
 	return target->alive();
 }
 

+ 17 - 5
lib/battle/CBattleInfoCallback.h

@@ -79,14 +79,26 @@ public:
 
 	void battleGetTurnOrder(std::vector<battle::Units> & out, const size_t maxUnits, const int maxTurns, const int turn = 0, BattleSide lastMoved = BattleSide::NONE) const;
 
-	///returns reachable hexes (valid movement destinations), DOES contain stack current position
-	BattleHexArray battleGetAvailableHexes(const battle::Unit * unit, bool obtainMovementRange, bool addOccupiable, BattleHexArray * attackable) const;
-
 	///returns reachable hexes (valid movement destinations), DOES contain stack current position (lite version)
 	BattleHexArray battleGetAvailableHexes(const battle::Unit * unit, bool obtainMovementRange) const;
-
 	BattleHexArray battleGetAvailableHexes(const ReachabilityInfo & cache, const battle::Unit * unit, bool obtainMovementRange) const;
 
+	//returns hexes the unit can occupy, obtainMovementRange ignores tactics mode (for double-wide units includes both head and tail)
+	BattleHexArray battleGetOccupiableHexes(const battle::Unit * unit, bool obtainMovementRange) const;
+	BattleHexArray battleGetOccupiableHexes(const BattleHexArray & availableHexes, const battle::Unit * unit) const;
+	//returns from which hex the attacker would attack the target from given direction; INVALID if not possible; the hex may be inccessible
+	BattleHex fromWhichHexAttack(const battle::Unit * attacker, const BattleHex & target, const BattleHex::EDir & direction) const;
+
+	//returns to which hex the (head of) unit would move to occupy position (possibly by tail)
+	BattleHex toWhichHexMove(const battle::Unit * unit, const BattleHex & position) const;
+	BattleHex toWhichHexMove(const BattleHexArray & availableHexes, const battle::Unit * unit, const BattleHex & position) const;
+
+	//return true iff attacker move towards and attack position from direction (spatial reasoning only)
+	bool battleCanAttackHex(const battle::Unit * attacker, const BattleHex & position, const BattleHex::EDir & direction) const;
+	bool battleCanAttackHex(const BattleHexArray & availableHexes, const battle::Unit * attacker, const BattleHex & position, const BattleHex::EDir & direction) const; //reuse availableHexes on multiple calls
+	bool battleCanAttackHex(const battle::Unit * attacker, const BattleHex & position) const; //check all directions
+	bool battleCanAttackHex(const BattleHexArray & availableHexes, const battle::Unit * attacker, const BattleHex & position) const; //reuse availableHexes on multiple calls
+
 	int battleGetSurrenderCost(const PlayerColor & Player) const; //returns cost of surrendering battle, -1 if surrendering is not possible
 	ReachabilityInfo::TDistances battleGetDistances(const battle::Unit * unit, const BattleHex & assumedPosition) const;
 	BattleHexArray battleGetAttackedHexes(const battle::Unit * attacker, const BattleHex & destinationTile, const BattleHex & attackerPos = BattleHex::INVALID) const;
@@ -96,7 +108,7 @@ public:
 	std::pair< BattleHexArray, int > getPath(const BattleHex & start, const BattleHex & dest, const battle::Unit * stack) const;
 
 	bool battleCanTargetEmptyHex(const battle::Unit * attacker) const; //determines of stack with given ID can target empty hex to attack - currently used only for SPELL_LIKE_ATTACK shooting
-	bool battleCanAttack(const battle::Unit * stack, const battle::Unit * target, const BattleHex & dest) const; //determines if stack with given ID can attack target at the selected destination
+	bool battleCanAttackUnit(const battle::Unit * attacker, const battle::Unit * target) const; //determines if attacker can attack target (no spatial reasoning)
 	bool battleCanShoot(const battle::Unit * attacker, const BattleHex & dest) const; //determines if stack with given ID shoot at the selected destination
 	bool battleCanShoot(const battle::Unit * attacker) const; //determines if stack with given ID shoot in principle
 	bool battleIsUnitBlocked(const battle::Unit * unit) const; //returns true if there is neighboring enemy stack

+ 16 - 3
lib/battle/Unit.cpp

@@ -91,9 +91,7 @@ BattleHexArray Unit::getAttackableHexes(const Unit * attacker) const
 			if (!coversPos(attacker->occupiedHex(attackOrigin)) && attackOrigin.isAvailable())
 				result.insert(attackOrigin);
 
-			bool isAttacker = attacker->unitSide() == BattleSide::ATTACKER;
-			BattleHex::EDir headDirection = isAttacker ? BattleHex::RIGHT : BattleHex::LEFT;
-			BattleHex headHex = attackOrigin.cloneInDirection(headDirection);
+			BattleHex headHex = attackOrigin.cloneInDirection(attacker->headDirection());
 
 			if (!coversPos(headHex) && headHex.isAvailable())
 				result.insert(headHex);
@@ -107,6 +105,21 @@ bool Unit::coversPos(const BattleHex & pos) const
 	return getPosition() == pos || (doubleWide() && (occupiedHex() == pos));
 }
 
+BattleHex::EDir Unit::headDirection() const
+{
+	if(doubleWide())
+	{
+		if(unitSide() == BattleSide::ATTACKER)
+			return BattleHex::EDir::RIGHT;
+		else
+			return BattleHex::EDir::LEFT;
+	}
+	else
+	{
+		return BattleHex::EDir::NONE;
+	}
+}
+
 const BattleHexArray & Unit::getHexes() const
 {
 	return getHexes(getPosition(), doubleWide(), unitSide());

+ 3 - 0
lib/battle/Unit.h

@@ -140,6 +140,9 @@ public:
 
 	bool coversPos(const BattleHex & position) const; //checks also if unit is double-wide
 
+	/// Returns the direction the double-wide unit is facing; returns NONE for single-hex units
+	BattleHex::EDir headDirection() const;
+
 	const BattleHexArray & getHexes() const; //up to two occupied hexes, starting from front
 	const BattleHexArray & getHexes(const BattleHex & assumedPos) const; //up to two occupied hexes, starting from front
 	static const BattleHexArray & getHexes(const BattleHex & assumedPos, bool twoHex, BattleSide side);

+ 1 - 1
server/battles/BattleActionProcessor.cpp

@@ -649,7 +649,7 @@ BattleActionProcessor::MovementResult BattleActionProcessor::moveStack(const CBa
 	//shifting destination (if we have double wide stack and we can occupy dest but not be exactly there)
 	if(!stackAtEnd && curStack->doubleWide() && !accessibility.accessible(dest, curStack))
 	{
-		BattleHex shifted = dest.cloneInDirection(curStack->destShiftDir(), false);
+		BattleHex shifted = dest.cloneInDirection(curStack->headDirection(), false);
 
 		if(accessibility.accessible(shifted, curStack))
 			dest = shifted;

+ 13 - 3
server/battles/BattleFlowProcessor.cpp

@@ -473,9 +473,19 @@ bool BattleFlowProcessor::tryMakeAutomaticActionOfBallistaOrTowers(const CBattle
 			if (battle.battleCanShoot(unit))
 				return true;
 
-			BattleHexArray attackableHexes;
-			battle.battleGetAvailableHexes(unit, true, false, &attackableHexes);
-			return !attackableHexes.empty();
+			auto units = battle.battleAliveUnits();
+			auto availableHexes = battle.battleGetAvailableHexes(unit, true);
+
+			for (auto otherUnit : units)
+			{
+				if (battle.battleCanAttackUnit(unit, otherUnit))
+					for (auto position : otherUnit->getHexes())
+					{
+						if (battle.battleCanAttackHex(availableHexes, unit, position))
+							return true;
+					}
+			}
+			return false;
 		};
 
 		const auto & getTowerAttackValue = [&battle, &next] (const battle::Unit * unit)