1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-11-23 22:37:55 +02:00

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
This commit is contained in:
Andrej Dudenhefner
2025-11-09 22:12:49 +01:00
parent 81db5c7cf4
commit e72dcce8ba
12 changed files with 251 additions and 328 deletions

View File

@@ -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;
const CStack * currentStack = owner.stacksController->getActiveStack();
return currentStack &&
owner.getBattle()->battleCanAttackUnit(currentStack, targetStack) &&
owner.getBattle()->battleCanAttackHex(currentStack, targetHex);
}
return false;
}
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();

View File

@@ -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);

View File

@@ -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,12 +275,15 @@ 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++)
{
//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))
{
BattleHex attackFromHex = fromWhichHexAttack(hoveredHex);
if(owner.getBattle()->battleCanAttack(stack, hoveredStack, attackFromHex))
if(owner.getBattle()->battleCanAttackUnit(stack, hoveredStack) && owner.getBattle()->battleCanAttackHex(availableHexes, stack, hoveredHex))
{
BattleHex fromHex = owner.getBattle()->fromWhichHexAttack(stack, hoveredHex, selectAttackDirection(hoveredHex));
assert(fromHex.isValid());
if(stack->doubleWide())
return {attackFromHex, stack->occupiedHex(attackFromHex)};
return {fromHex, stack->occupiedHex(fromHex)};
return {attackFromHex};
}
return {fromHex};
}
auto toHex = owner.getBattle()->toWhichHexMove(availableHexes, stack, hoveredHex);
if (!toHex.isValid())
return {};
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)};
return {};
}
return {toHex, stack->occupiedHex(toHex)};
else
{
if (availableHexes.contains(hoveredHex))
return {hoveredHex};
else
return {};
}
return {toHex};
}
// Range limit highlight helpers
@@ -667,164 +649,47 @@ 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]) )
{
if (testPoint[i].isValid())
{
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;
}
}
}
assert(nearest != -1);
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);
}
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
{
// 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)
{
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;
}
}
}
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;
}
void BattleFieldController::updateAccessibleHexes()
{
auto accessibility = owner.getBattle()->getAccessibility();

View File

@@ -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);
};

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;
for(const auto & hex : ret)
occupiable.insert(unit->occupiedHex(hex));
ret.insert(occupiable);
}
if(attackable)
{
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())))
{
if(!otherSt->isValidTarget(false))
continue;
const BattleHexArray & occupied = otherSt->getHexes();
if(battleCanShoot(unit, otherSt->getPosition()))
{
attackable->insert(occupied);
continue;
}
for(const BattleHex & he : occupied)
{
if(meleeAttackable(he))
attackable->insert(he);
}
}
}
return ret;
return battleGetOccupiableHexes(battleGetAvailableHexes(unit, obtainMovementRange), unit);
}
bool CBattleInfoCallback::battleCanAttack(const battle::Unit * stack, const battle::Unit * target, const BattleHex & dest) const
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!");
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;
bool isAttacker = attacker->unitSide() == BattleSide::ATTACKER;
if (attacker->doubleWide())
{
// 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)
{
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);
}
BattleHex CBattleInfoCallback::toWhichHexMove(const battle::Unit * unit, const BattleHex & position) const
{
return toWhichHexMove(battleGetAvailableHexes(unit, false), unit, position);
}
BattleHex CBattleInfoCallback::toWhichHexMove(const BattleHexArray & availableHexes, const battle::Unit * unit, const BattleHex & position) const
{
RETURN_IF_NOT_BATTLE(false);
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;
}
bool CBattleInfoCallback::battleCanAttackHex(const battle::Unit * attacker, const BattleHex & position) const
{
return battleCanAttackHex(battleGetAvailableHexes(attacker, false), attacker, position);
}
bool CBattleInfoCallback::battleCanAttackHex(const BattleHexArray & availableHexes, const battle::Unit * attacker, const BattleHex & position) const
{
for (auto direction = 0; direction < 8; direction++)
{
if (battleCanAttackHex(availableHexes, attacker, position, BattleHex::EDir(direction)))
return true;
}
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);
}
bool CBattleInfoCallback::battleCanAttackHex(const BattleHexArray & availableHexes, const battle::Unit * attacker, const BattleHex & position, const BattleHex::EDir & direction) const
{
RETURN_IF_NOT_BATTLE(false);
if (!attacker)
throw std::runtime_error("Undefined attacker in battleCanAttackHex!");
if (!position.isValid() || direction == BattleHex::NONE)
return false;
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 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(fromHex))
return false;
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 (!stack || !target)
if (!attacker)
throw std::runtime_error("Undefined attacker in battleCanAttackUnit!");
if(!target || target->isInvincible())
return false;
if(target->isInvincible())
if(attacker == target || !battleMatchOwner(attacker, target))
return false;
if(!battleMatchOwner(stack, target))
if (!attacker->isMeleeAttacker())
return false;
if (!stack->isMeleeAttacker())
return false;
if (stack->getPosition() != dest)
{
for (const auto & obstacle : battleGetAllObstacles())
{
if (obstacle->getStoppingTile().contains(dest))
return false;
if (stack->doubleWide() && obstacle->getStoppingTile().contains(stack->occupiedHex(dest)))
return false;
}
}
return target->alive();
}

View File

@@ -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

View File

@@ -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());

View File

@@ -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);

View File

@@ -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;

View File

@@ -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)