2
0
Эх сурвалжийг харах

BattleAI: fix dragonbreath retaliation

Andrii Danylchenko 1 жил өмнө
parent
commit
9edb0afff8

+ 3 - 3
AI/BattleAI/AttackPossibility.cpp

@@ -268,11 +268,11 @@ AttackPossibility AttackPossibility::evaluate(
 		std::vector<const battle::Unit *> 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(); });

+ 65 - 18
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<const battle::Unit*> CBattleInfoCallback::getAttackedBattleUnits(const battle::Unit* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos) const
+std::vector<const battle::Unit*> CBattleInfoCallback::getAttackedBattleUnits(
+	const battle::Unit * attacker,
+	const  battle::Unit * defender,
+	BattleHex destinationTile,
+	bool rangedAttack,
+	BattleHex attackerPos,
+	BattleHex defenderPos) const
 {
 	std::vector<const battle::Unit*> 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<const CStack*> 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<BattleSide::Type>(attacker->unitSide())))
 		return false;
 
+	auto defenderOtherHex = defenderHex;
+	auto attackerOtherHex = defenderHex;
+
 	if (defender->doubleWide())
 	{
-		if(isHexInFront(attackerHex, defender->occupiedHex(), static_cast<BattleSide::Type>(attacker->unitSide())))
+		defenderOtherHex = battle::Unit::occupiedHex(defenderHex, true, defender->unitSide());
+
+		if(isHexInFront(attackerHex, defenderOtherHex, static_cast<BattleSide::Type>(attacker->unitSide())))
 			return false;
 	}
 
 	if (attacker->doubleWide())
 	{
-		if(isHexInFront(attacker->occupiedHex(), defenderHex, static_cast<BattleSide::Type>(attacker->unitSide())))
+		attackerOtherHex = battle::Unit::occupiedHex(attackerHex, true, attacker->unitSide());
+
+		if(isHexInFront(attackerOtherHex, defenderHex, static_cast<BattleSide::Type>(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<BattleSide::Type>(attacker->unitSide())))
+		if(isHexInFront(attackerOtherHex, defenderOtherHex, static_cast<BattleSide::Type>(attacker->unitSide())))
 			return false;
 	}
 	return true;

+ 21 - 2
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<const battle::Unit *> getAttackedBattleUnits(const battle::Unit* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos = BattleHex::INVALID) const; //calculates range of multi-hex attacks
+
+	std::vector<const battle::Unit *> 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<const CStack*> 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
 

+ 205 - 0
test/battle/CBattleInfoCallbackTest.cpp

@@ -40,11 +40,32 @@ public:
 		bonusFake.addNewBonus(b);
 	}
 
+	void addCreatureAbility(BonusType bonusType)
+	{
+		addNewBonus(
+			std::make_shared<Bonus>(
+				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<Bonus>(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: