diff --git a/AI/BattleAI/AttackPossibility.cpp b/AI/BattleAI/AttackPossibility.cpp index 1d31c9725..949ebdf30 100644 --- a/AI/BattleAI/AttackPossibility.cpp +++ b/AI/BattleAI/AttackPossibility.cpp @@ -268,11 +268,11 @@ AttackPossibility AttackPossibility::evaluate( std::vector affectedUnits; if (attackInfo.shooting) - defenderUnits = state->getAttackedBattleUnits(attacker, defHex, true, BattleHex::INVALID); + defenderUnits = state->getAttackedBattleUnits(attacker, defender, defHex, true, hex, defender->getPosition()); else { - defenderUnits = state->getAttackedBattleUnits(attacker, defHex, false, hex); - retaliatedUnits = state->getAttackedBattleUnits(defender, hex, false, defender->getPosition()); + defenderUnits = state->getAttackedBattleUnits(attacker, defender, defHex, false, hex, defender->getPosition()); + retaliatedUnits = state->getAttackedBattleUnits(defender, attacker, hex, false, defender->getPosition(), hex); // attacker can not melle-attack itself but still can hit that place where it was before moving vstd::erase_if(defenderUnits, [attacker](const battle::Unit * u) -> bool { return u->unitId() == attacker->unitId(); }); diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index 2048aa367..cccf767ba 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -1235,19 +1235,40 @@ ReachabilityInfo CBattleInfoCallback::getFlyingReachability(const ReachabilityIn return ret; } -AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes(const battle::Unit* attacker, BattleHex destinationTile, BattleHex attackerPos) const +AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes( + const battle::Unit * attacker, + BattleHex destinationTile, + BattleHex attackerPos) const +{ + const auto * defender = battleGetUnitByPos(destinationTile, true); + + if(!defender) + return AttackableTiles(); // can't attack thin air + + return getPotentiallyAttackableHexes( + attacker, + defender, + destinationTile, + attackerPos, + defender->getPosition()); +} + +AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes( + const battle::Unit* attacker, + const battle::Unit * defender, + BattleHex destinationTile, + BattleHex attackerPos, + BattleHex defenderPos) const { //does not return hex attacked directly AttackableTiles at; RETURN_IF_NOT_BATTLE(at); BattleHex attackOriginHex = (attackerPos != BattleHex::INVALID) ? attackerPos : attacker->getPosition(); //real or hypothetical (cursor) position - - const auto * defender = battleGetUnitByPos(destinationTile, true); - if (!defender) - return at; // can't attack thin air - - bool reverse = isToReverse(attacker, defender, attackOriginHex, destinationTile); + + defenderPos = (defenderPos != BattleHex::INVALID) ? defenderPos : defender->getPosition(); //real or hypothetical (cursor) position + + bool reverse = isToReverse(attacker, defender, attackerPos, defenderPos); if(reverse && attacker->doubleWide()) { attackOriginHex = attacker->occupiedHex(attackOriginHex); //the other hex stack stands on @@ -1291,20 +1312,27 @@ AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes(const battle: else if(attacker->hasBonusOfType(BonusType::TWO_HEX_ATTACK_BREATH)) { auto direction = BattleHex::mutualPosition(attackOriginHex, destinationTile); + + if(direction == BattleHex::NONE + && defender->doubleWide() + && attacker->doubleWide() + && defenderPos == destinationTile) + { + direction = BattleHex::mutualPosition(attackOriginHex, defender->occupiedHex(defenderPos)); + } + if(direction != BattleHex::NONE) //only adjacent hexes are subject of dragon breath calculation { BattleHex nextHex = destinationTile.cloneInDirection(direction, false); if ( defender->doubleWide() ) { - auto secondHex = destinationTile == defender->getPosition() ? - defender->occupiedHex(): - defender->getPosition(); + auto secondHex = destinationTile == defenderPos ? defender->occupiedHex(defenderPos) : defenderPos; // if targeted double-wide creature is attacked from above or below ( -> second hex is also adjacent to attack origin) // then dragon breath should target tile on the opposite side of targeted creature - /*if(BattleHex::mutualPosition(attackOriginHex, secondHex) != BattleHex::NONE) - nextHex = secondHex.cloneInDirection(direction, false);*/ + if(BattleHex::mutualPosition(attackOriginHex, secondHex) != BattleHex::NONE) + nextHex = secondHex.cloneInDirection(direction, false); } if (nextHex.isValid()) @@ -1335,17 +1363,29 @@ AttackableTiles CBattleInfoCallback::getPotentiallyShootableHexes(const battle:: return at; } -std::vector CBattleInfoCallback::getAttackedBattleUnits(const battle::Unit* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos) const +std::vector CBattleInfoCallback::getAttackedBattleUnits( + const battle::Unit * attacker, + const battle::Unit * defender, + BattleHex destinationTile, + bool rangedAttack, + BattleHex attackerPos, + BattleHex defenderPos) const { std::vector units; RETURN_IF_NOT_BATTLE(units); + if(attackerPos == BattleHex::INVALID) + attackerPos = attacker->getPosition(); + + if(defenderPos == BattleHex::INVALID) + defenderPos = defender->getPosition(); + AttackableTiles at; if (rangedAttack) at = getPotentiallyShootableHexes(attacker, destinationTile, attackerPos); else - at = getPotentiallyAttackableHexes(attacker, destinationTile, attackerPos); + at = getPotentiallyAttackableHexes(attacker, defender, destinationTile, attackerPos, defenderPos); units = battleGetUnitsIf([=](const battle::Unit * unit) { @@ -1371,7 +1411,7 @@ std::set CBattleInfoCallback::getAttackedCreatures(const CStack* RETURN_IF_NOT_BATTLE(attackedCres); AttackableTiles at; - + if(rangedAttack) at = getPotentiallyShootableHexes(attacker, destinationTile, attackerPos); else @@ -1424,15 +1464,22 @@ bool CBattleInfoCallback::isToReverse(const battle::Unit * attacker, const battl if(isHexInFront(attackerHex, defenderHex, static_cast(attacker->unitSide()))) return false; + auto defenderOtherHex = defenderHex; + auto attackerOtherHex = defenderHex; + if (defender->doubleWide()) { - if(isHexInFront(attackerHex, defender->occupiedHex(), static_cast(attacker->unitSide()))) + defenderOtherHex = battle::Unit::occupiedHex(defenderHex, true, defender->unitSide()); + + if(isHexInFront(attackerHex, defenderOtherHex, static_cast(attacker->unitSide()))) return false; } if (attacker->doubleWide()) { - if(isHexInFront(attacker->occupiedHex(), defenderHex, static_cast(attacker->unitSide()))) + attackerOtherHex = battle::Unit::occupiedHex(attackerHex, true, attacker->unitSide()); + + if(isHexInFront(attackerOtherHex, defenderHex, static_cast(attacker->unitSide()))) return false; } @@ -1440,7 +1487,7 @@ bool CBattleInfoCallback::isToReverse(const battle::Unit * attacker, const battl // but this is how H3 handles it which is important, e.g. for direction of dragon breath attacks if (attacker->doubleWide() && defender->doubleWide()) { - if(isHexInFront(attacker->occupiedHex(), defender->occupiedHex(), static_cast(attacker->unitSide()))) + if(isHexInFront(attackerOtherHex, defenderOtherHex, static_cast(attacker->unitSide()))) return false; } return true; diff --git a/lib/battle/CBattleInfoCallback.h b/lib/battle/CBattleInfoCallback.h index bd3c9bcd6..01f4bd5b8 100644 --- a/lib/battle/CBattleInfoCallback.h +++ b/lib/battle/CBattleInfoCallback.h @@ -130,9 +130,28 @@ public: bool isInTacticRange(BattleHex dest) const; si8 battleGetTacticDist() const; //returns tactic distance for calling player or 0 if this player is not in tactic phase (for ALL_KNOWING actual distance for tactic side) - AttackableTiles getPotentiallyAttackableHexes(const battle::Unit* attacker, BattleHex destinationTile, BattleHex attackerPos) const; //TODO: apply rotation to two-hex attacker + AttackableTiles getPotentiallyAttackableHexes( + const battle::Unit* attacker, + const battle::Unit* defender, + BattleHex destinationTile, + BattleHex attackerPos, + BattleHex defenderPos) const; //TODO: apply rotation to two-hex attacker + + AttackableTiles getPotentiallyAttackableHexes( + const battle::Unit * attacker, + BattleHex destinationTile, + BattleHex attackerPos) const; + AttackableTiles getPotentiallyShootableHexes(const battle::Unit* attacker, BattleHex destinationTile, BattleHex attackerPos) const; - std::vector getAttackedBattleUnits(const battle::Unit* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos = BattleHex::INVALID) const; //calculates range of multi-hex attacks + + std::vector getAttackedBattleUnits( + const battle::Unit* attacker, + const battle::Unit * defender, + BattleHex destinationTile, + bool rangedAttack, + BattleHex attackerPos = BattleHex::INVALID, + BattleHex defenderPos = BattleHex::INVALID) const; //calculates range of multi-hex attacks + std::set getAttackedCreatures(const CStack* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos = BattleHex::INVALID) const; //calculates range of multi-hex attacks bool isToReverse(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerHex = BattleHex::INVALID, BattleHex defenderHex = BattleHex::INVALID) const; //determines if attacker standing at attackerHex should reverse in order to attack defender diff --git a/test/battle/CBattleInfoCallbackTest.cpp b/test/battle/CBattleInfoCallbackTest.cpp index d7b3ae386..8a443ef48 100644 --- a/test/battle/CBattleInfoCallbackTest.cpp +++ b/test/battle/CBattleInfoCallbackTest.cpp @@ -40,11 +40,32 @@ public: bonusFake.addNewBonus(b); } + void addCreatureAbility(BonusType bonusType) + { + addNewBonus( + std::make_shared( + BonusDuration::PERMANENT, + bonusType, + BonusSource::CREATURE_ABILITY, + 0, + CreatureID(0))); + } + void makeAlive() { EXPECT_CALL(*this, alive()).WillRepeatedly(Return(true)); } + void setupPoisition(BattleHex pos) + { + EXPECT_CALL(*this, getPosition()).WillRepeatedly(Return(pos)); + } + + void makeDoubleWide() + { + EXPECT_CALL(*this, doubleWide()).WillRepeatedly(Return(true)); + } + void makeWarMachine() { addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SIEGE_WEAPON, BonusSource::CREATURE_ABILITY, 1, BonusSourceID())); @@ -183,6 +204,190 @@ public: } }; +class AttackableHexesTest : public CBattleInfoCallbackTest +{ +public: + UnitFake & addRegularMelee(BattleHex hex, uint8_t side) + { + auto & unit = unitsFake.add(side); + + unit.makeAlive(); + unit.setDefaultState(); + unit.setupPoisition(hex); + unit.redirectBonusesToFake(); + + return unit; + } + + UnitFake & addDragon(BattleHex hex, uint8_t side) + { + auto & unit = addRegularMelee(hex, side); + + unit.addCreatureAbility(BonusType::TWO_HEX_ATTACK_BREATH); + unit.makeDoubleWide(); + + return unit; + } + + Units getAttackedUnits(UnitFake & attacker, UnitFake & defender, BattleHex defenderHex) + { + startBattle(); + redirectUnitsToFake(); + + return subject.getAttackedBattleUnits( + &attacker, &defender, + defenderHex, false, + attacker.getPosition(), + defender.getPosition()); + } +}; + +TEST_F(AttackableHexesTest, DragonRightRegular_RightHorithontalBreath) +{ + // X A D # + UnitFake & attacker = addDragon(35, 0); + UnitFake & defender = addRegularMelee(36, 1); + UnitFake & next = addRegularMelee(37, 1); + + auto attacked = getAttackedUnits(attacker, defender, defender.getPosition()); + + EXPECT_TRUE(vstd::contains(attacked, &next)); +} + +TEST_F(AttackableHexesTest, DragonDragonBottomRightHead_BottomRightBreathFromHead) +{ + // X A + // D X target D + // # + UnitFake & attacker = addDragon(35, 0); + UnitFake & defender = addDragon(attacker.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), 1); + UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), 1); + + auto attacked = getAttackedUnits(attacker, defender, defender.getPosition()); + + EXPECT_TRUE(vstd::contains(attacked, &next)); +} + +TEST_F(AttackableHexesTest, DragonDragonVerticalDownHead_VerticalDownBreathFromHead) +{ + // X A + // D X target D + // # + UnitFake & attacker = addDragon(35, 0); + UnitFake & defender = addDragon(attacker.getPosition().cloneInDirection(BattleHex::BOTTOM_LEFT), 1); + UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), 1); + + auto attacked = getAttackedUnits(attacker, defender, defender.getPosition()); + + EXPECT_TRUE(vstd::contains(attacked, &next)); +} + +TEST_F(AttackableHexesTest, DragonDragonVerticalDownHeadReverse_VerticalDownBreathFromHead) +{ + // A X + // X D target D + // # + UnitFake & attacker = addDragon(36, 1); + UnitFake & defender = addDragon(attacker.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), 0); + UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_LEFT), 0); + + auto attacked = getAttackedUnits(attacker, defender, defender.getPosition()); + + EXPECT_TRUE(vstd::contains(attacked, &next)); +} + +TEST_F(AttackableHexesTest, DragonDragonVerticalDownBack_VerticalDownBreath) +{ + // X A + // D X target X + // # + UnitFake & attacker = addDragon(37, 0); + UnitFake & defender = addDragon(attacker.occupiedHex().cloneInDirection(BattleHex::BOTTOM_LEFT), 1); + UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), 1); + + auto attacked = getAttackedUnits(attacker, defender, defender.occupiedHex()); + + EXPECT_TRUE(vstd::contains(attacked, &next)); +} + +TEST_F(AttackableHexesTest, DragonDragonHeadBottomRight_BottomRightBreathFromHead) +{ + // X A + // D X target D + // # + UnitFake & attacker = addDragon(37, 0); + UnitFake & defender = addDragon(attacker.occupiedHex().cloneInDirection(BattleHex::BOTTOM_LEFT), 1); + UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), 1); + + auto attacked = getAttackedUnits(attacker, defender, defender.getPosition()); + + EXPECT_TRUE(vstd::contains(attacked, &next)); +} + +TEST_F(AttackableHexesTest, DragonVerticalDownDragonBackReverse_VerticalDownBreath) +{ + // A X + // X D target X + // # + UnitFake & attacker = addDragon(36, 1); + UnitFake & defender = addDragon(attacker.occupiedHex().cloneInDirection(BattleHex::BOTTOM_RIGHT), 0); + UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_LEFT), 0); + + auto attacked = getAttackedUnits(attacker, defender, defender.occupiedHex()); + + EXPECT_TRUE(vstd::contains(attacked, &next)); +} + +TEST_F(AttackableHexesTest, DragonRightBottomDragonHeadReverse_RightBottomBreathFromHeadHex) +{ + // A X + // X D target D + UnitFake & attacker = addDragon(36, 1); + UnitFake & defender = addDragon(attacker.occupiedHex().cloneInDirection(BattleHex::BOTTOM_RIGHT), 0); + UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_LEFT), 0); + + auto attacked = getAttackedUnits(attacker, defender, defender.getPosition()); + + EXPECT_TRUE(vstd::contains(attacked, &next)); +} + +TEST_F(AttackableHexesTest, DragonLeftBottomDragonBackToBack_LeftBottomBreathFromBackHex) +{ + // X A + // D X target X + // # + UnitFake & attacker = addDragon(8, 0); + UnitFake & defender = addDragon(attacker.occupiedHex().cloneInDirection(BattleHex::BOTTOM_LEFT).cloneInDirection(BattleHex::LEFT), 1); + UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), 1); + + auto attacked = getAttackedUnits(attacker, defender, defender.occupiedHex()); + + EXPECT_TRUE(vstd::contains(attacked, &next)); +} + +TEST_F(AttackableHexesTest, DefenderPositionOverride_BreathCountsHypoteticDefenderPosition) +{ + // # N + // X D target D + // A X + UnitFake & attacker = addDragon(35, 1); + UnitFake & defender = addDragon(8, 0); + UnitFake & next = addDragon(2, 0); + + startBattle(); + redirectUnitsToFake(); + + auto attacked = subject.getAttackedBattleUnits( + &attacker, + &defender, + 19, + false, + attacker.getPosition(), + 19); + + EXPECT_TRUE(vstd::contains(attacked, &next)); +} + class BattleFinishedTest : public CBattleInfoCallbackTest { public: