diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp
index 884b31055..6ef6352de 100644
--- a/client/CPlayerInterface.cpp
+++ b/client/CPlayerInterface.cpp
@@ -943,6 +943,7 @@ void CPlayerInterface::battleAttack(const BattleID & battleID, const BattleAttac
info.unlucky = ba->unlucky();
info.deathBlow = ba->deathBlow();
info.lifeDrain = ba->lifeDrain();
+ info.playCustomAnimation = ba->playCustomAnimation();
info.tile = ba->tile;
info.spellEffect = SpellID::NONE;
diff --git a/client/battle/BattleInterface.h b/client/battle/BattleInterface.h
index 1c8aa0cbf..a4aa261b8 100644
--- a/client/battle/BattleInterface.h
+++ b/client/battle/BattleInterface.h
@@ -85,6 +85,7 @@ struct StackAttackInfo
bool unlucky;
bool deathBlow;
bool lifeDrain;
+ bool playCustomAnimation;
};
/// Main class for battles, responsible for relaying information from server to various battle entities
diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp
index 2757d6bb8..555d267cc 100644
--- a/client/battle/BattleStacksController.cpp
+++ b/client/battle/BattleStacksController.cpp
@@ -574,7 +574,6 @@ void BattleStacksController::stackAttacking( const StackAttackInfo & info )
auto defender = info.defender;
auto tile = info.tile;
auto spellEffect = info.spellEffect;
- auto multiAttack = !info.secondaryDefender.empty();
bool needsReverse = false;
if (info.indirectAttack)
@@ -625,7 +624,7 @@ void BattleStacksController::stackAttacking( const StackAttackInfo & info )
}
}
- owner.addToAnimationStage(EAnimationEvents::ATTACK, [this, attacker, tile, defender, multiAttack, info]()
+ owner.addToAnimationStage(EAnimationEvents::ATTACK, [this, attacker, tile, defender, info]()
{
if (info.indirectAttack)
{
@@ -633,7 +632,7 @@ void BattleStacksController::stackAttacking( const StackAttackInfo & info )
}
else
{
- addNewAnim(new MeleeAttackAnimation(owner, attacker, tile, defender, multiAttack));
+ addNewAnim(new MeleeAttackAnimation(owner, attacker, tile, defender, info.playCustomAnimation));
}
});
diff --git a/docs/images/Bonus_Multihex_Attack_Horizontal.svg b/docs/images/Bonus_Multihex_Attack_Horizontal.svg
new file mode 100644
index 000000000..fbe092408
--- /dev/null
+++ b/docs/images/Bonus_Multihex_Attack_Horizontal.svg
@@ -0,0 +1,17 @@
+
diff --git a/docs/images/Bonus_Multihex_Attack_Vertical.svg b/docs/images/Bonus_Multihex_Attack_Vertical.svg
new file mode 100644
index 000000000..b54830d78
--- /dev/null
+++ b/docs/images/Bonus_Multihex_Attack_Vertical.svg
@@ -0,0 +1,17 @@
+
diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md
index a2384fa09..885861697 100644
--- a/docs/modders/Bonus/Bonus_Types.md
+++ b/docs/modders/Bonus/Bonus_Types.md
@@ -91,7 +91,7 @@ Affected units will view any terrain as native. This means army containing these
Changes selected primary skill for affected heroes and units
- subtype: primary skill
-- additional info: 1 - only for melee attacks, 2 - only for ranged attacks
+- addInfo: 1 - only for melee attacks, 2 - only for ranged attacks
### SIGHT_RADIUS
@@ -500,21 +500,61 @@ Affected unit ranged attack will use animation and range of specified spell (Mag
- subtype - spell identifier
- value - spell mastery level
-### THREE_HEADED_ATTACK
-
-Affected unit attacks creatures located on tiles to left and right of targeted tile (Cerberus). Only directly targeted creature will attempt to retaliate
-
### ATTACKS_ALL_ADJACENT
-Affected unit attacks all adjacent creatures (Hydra). Only directly targeted creature will attempt to retaliate
+The affected unit attacks all adjacent units (Hydra). Only the unit that has been directly targeted will attempt to retaliate. If the unit is hypnotised, it will attack its former allies instead.
+
+### THREE_HEADED_ATTACK
+
+The affected unit will attack units located on the hexed to the left and right of the targeted tile (Cerberus). Only the unit that has been directly targeted will attempt to retaliate.
+Potentially deprecated. Consider using the more flexible [MULTIHEX_ENEMY_ATTACK](#multihex_unit_attack) instead with custom icon and description.
### TWO_HEX_ATTACK_BREATH
-Affected unit attacks creature located directly behind targeted tile (Dragons). Only directly targeted creature will attempt to retaliate
+The affected unit will also attack the hex located directly behind the targeted hex (Dragons). Only the unit that has been directly targeted will attempt to retaliate.
+Potentially deprecated. Consider using the more flexible [MULTIHEX_UNIT_ATTACK](#multihex_unit_attack) instead with custom icon and description.
+
+### WIDE_BREATH
+
+The affected unit will attack any units in the hexes surrounding the attacked hex.
+Deprecated. Please use [MULTIHEX_UNIT_ATTACK](#multihex_unit_attack) instead with custom icon and description.
### PRISM_HEX_ATTACK_BREATH
-Like `TWO_HEX_ATTACK_BREATH` but affects also two additional cratures (in triangle form from target tile)
+Similar to `TWO_HEX_ATTACK_BREATH`, but affecting two additional hexes in a triangular formation from the target hex.
+Deprecated. Please use [MULTIHEX_UNIT_ATTACK](#multihex_unit_attack) instead with custom icon and description.
+
+### MULTIHEX_UNIT_ATTACK
+
+The affected unit attacks all units, friendly or not, located on specified hexes in addition to the primary target. Only the unit that has been directly targeted will attempt to retaliate.
+
+- addInfo: A list of strings describing which hexes this unit will attack, computed from the attacker's position. The possible values are: `F` (front), `L` (left), `R` (right), `B` (back). See below for more examples.
+
+Examples:
+
+- H3 Dragon Breath: `[ "FF" ]` – dragons also attack the hex located two hexes in front of the dragon's head.
+- H3 Cerberus three-headed attack: `[ "L", "R" ]` - Cerberus also attacks the hexes one hex to the left and right of itself.
+- Prism Breath (mods): `[ "FL", "FF", "FR" ]` — a more powerful version of Dragon Breath; all units behind the target are attacked.
+
+This is how all tiles can be referenced in the event of a frontal attack (green is the attacker and red is the defender). The hex on which defender is located is always included unconditionally.
+
+
+In the case of a double-wide unit that can attack hexes to the left and right (e.g. Cerberi), the left or right hex may end up inside the attacker in certain attack configurations. To avoid this, the hex that ends up inside the unit will be 'pushed' one hex forward. This does not affect single-wide units. See below for reference:
+
+
+### MULTIHEX_ENEMY_ATTACK
+
+The affected unit will attack all enemies located on the specified hexes, in addition to its primary target. Only the unit that has been directly targeted will attempt to retaliate. If the unit is hypnotised, it will attack its former allies instead.
+
+- addInfo: see [MULTIHEX_UNIT_ATTACK](#multihex_unit_attack) for a detailed description.
+
+### MULTIHEX_ANIMATION
+
+The bonus does not affect the mechanics. If the affected unit hits any non-primary targets located on the specified tiles, the unit will play an alternative attack animation if one is present.
+
+If this bonus is not present, the unit will always use the alternative attack animation whenever its attack hits any unit other than the primary target.
+
+- addInfo: see [MULTIHEX_UNIT_ATTACK](#multihex_unit_attack) for a detailed description.
### RETURN_AFTER_STRIKE
@@ -611,10 +651,6 @@ Affected units will retaliate against ranged attacks, if able
Affected unit will never receive counterattack in ranged attacks. Counters RANGED_RETALIATION bonus
-### WIDE_BREATH
-
-Affected unit will attack units on all hexes that surround attacked hex
-
### FIRST_STRIKE
Affected unit will retaliate before enemy attacks, if able
@@ -761,7 +797,7 @@ If affected unit is targeted by a spell it will reflect spell to a random enemy
Affected unit will deal additional damage after attack
- val - additional damage to deal, multiplied by unit stack size
-- additional info: chance to trigger, percentage
+- addInfo: chance to trigger, percentage
### DEATH_STARE
@@ -796,7 +832,7 @@ Affected units can cast a spell as targeted action (Archangel, Faerie Dragon). U
- subtype: spell identifier
- value: spell mastery level
-- additional info: weighted chance to select this spell. Can be omitted for always available spells
+- addInfo: weighted chance to select this spell. Can be omitted for always available spells
### ENCHANTER
@@ -822,7 +858,7 @@ Determines how many times per combat affected creature can cast its targeted spe
- subtype - spell id, eg. spell.iceBolt
- value - chance (percent)
-- additional info - \[X, Y, Z\]
+- addInfo - \[X, Y, Z\]
- X - spell mastery level (1 - Basic, 3 - Expert)
- Y = 0 - all attacks, 1 - shot only, 2 - melee only
- Z (optional) - layer for multiple SPELL_AFTER_ATTACK bonuses and multi-turn casting. Empty or value less than 0 = not participating in layering.
@@ -832,7 +868,7 @@ Determines how many times per combat affected creature can cast its targeted spe
- subtype - spell id
- value - chance %
-- additional info - \[X, Y, Z\]
+- addInfo - \[X, Y, Z\]
- X - spell mastery level (1 - Basic, 3 - Expert)
- Y = 0 - all attacks, 1 - shot only, 2 - melee only
- Z (optional) - layer for multiple SPELL_BEFORE_ATTACK bonuses and multi-turn casting. Empty or value less than 0 = not participating in layering.
diff --git a/lib/battle/BattleHex.h b/lib/battle/BattleHex.h
index d13fa534c..33f2a4d5f 100644
--- a/lib/battle/BattleHex.h
+++ b/lib/battle/BattleHex.h
@@ -118,7 +118,7 @@ public:
if(hasToBeValid)
{
if(x < 0 || x >= GameConstants::BFIELD_WIDTH || y < 0 || y >= GameConstants::BFIELD_HEIGHT)
- throw std::runtime_error("Hex at (" + std::to_string(x) + ", " + std::to_string(y) + ") is not valid!");
+ throw std::out_of_range("Hex at (" + std::to_string(x) + ", " + std::to_string(y) + ") is not valid!");
}
hex = x + y * GameConstants::BFIELD_WIDTH;
diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp
index e4faf832e..58669a37a 100644
--- a/lib/battle/CBattleInfoCallback.cpp
+++ b/lib/battle/CBattleInfoCallback.cpp
@@ -1330,89 +1330,82 @@ AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes(
defenderPos = (defenderPos.toInt() != 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
- }
if(attacker->hasBonusOfType(BonusType::ATTACKS_ALL_ADJACENT))
- {
at.hostileCreaturePositions.insert(attacker->getSurroundingHexes(attackerPos));
- }
+
+ // If attacker is double-wide and its head is not adjacent to enemy we need to turn around
+ if(attacker->doubleWide() && !vstd::contains(defender->getSurroundingHexes(defenderPos), attackOriginHex))
+ attackOriginHex = attacker->occupiedHex(attackOriginHex);
+
+ if (!vstd::contains(defender->getSurroundingHexes(defenderPos), attackOriginHex))
+ throw std::runtime_error("!!!");
+
+ auto attackDirection = BattleHex::mutualPosition(attackOriginHex, defenderPos);
+
+ // If defender is double-wide, attacker always prefers targeting its 'tail', if it is reachable
+ if(defender->doubleWide() && BattleHex::mutualPosition(attackOriginHex, defender->occupiedHex(defenderPos)) != BattleHex::NONE)
+ attackDirection = BattleHex::mutualPosition(attackOriginHex, defender->occupiedHex(defenderPos));
+
+ if (attackDirection == BattleHex::NONE)
+ throw std::runtime_error("!!!");
+
+ const auto & processTargets = [&](const std::vector & additionalTargets) -> BattleHexArray
+ {
+ BattleHexArray output;
+
+ for (int targetPath : additionalTargets)
+ {
+ BattleHex target = attackOriginHex;
+ std::vector path;
+
+ for (int targetPathLeft = targetPath; targetPathLeft != 0; targetPathLeft /= 10)
+ path.push_back(static_cast((attackDirection + targetPathLeft % 10 - 1) % 6));
+
+ try
+ {
+ if(attacker->doubleWide() && attacker->coversPos(target.cloneInDirection(path.front())))
+ target.moveInDirection(attackDirection);
+
+ for(BattleHex::EDir nextDirection : path)
+ target.moveInDirection(nextDirection);
+ }
+ catch(const std::out_of_range &)
+ {
+ // Hex out of range, for example outside of battlefield. This is valid situation, so skip this hex
+ continue;
+ }
+
+ if (target.isValid() && !attacker->coversPos(target))
+ output.insert(target);
+ }
+ return output;
+ };
+
+ const auto multihexUnit = attacker->getBonusesOfType(BonusType::MULTIHEX_UNIT_ATTACK);
+ const auto multihexEnemy = attacker->getBonusesOfType(BonusType::MULTIHEX_ENEMY_ATTACK);
+ const auto multihexAnimation = attacker->getBonusesOfType(BonusType::MULTIHEX_ANIMATION);
+
+ for (const auto & bonus : *multihexUnit)
+ at.friendlyCreaturePositions.insert(processTargets(bonus->additionalInfo));
+
+ for (const auto & bonus : *multihexEnemy)
+ at.hostileCreaturePositions.insert(processTargets(bonus->additionalInfo));
+
+ for (const auto & bonus : *multihexAnimation)
+ at.overrideAnimationPositions.insert(processTargets(bonus->additionalInfo));
+
if(attacker->hasBonusOfType(BonusType::THREE_HEADED_ATTACK))
- {
- const BattleHexArray & hexes = attacker->getSurroundingHexes(attackerPos);
- for(const BattleHex & tile : hexes)
- {
- if((BattleHex::mutualPosition(tile, destinationTile) > -1 && BattleHex::mutualPosition(tile, attackOriginHex) > -1)) //adjacent both to attacker's head and attacked tile
- {
- const auto * st = battleGetUnitByPos(tile, true);
- if(st && battleGetOwner(st) != battleGetOwner(attacker)) //only hostile stacks - does it work well with Berserk?
- at.hostileCreaturePositions.insert(tile);
- }
- }
- }
+ at.hostileCreaturePositions.insert(processTargets({2,6}));
+
if(attacker->hasBonusOfType(BonusType::WIDE_BREATH))
- {
- BattleHexArray hexes = destinationTile.getNeighbouringTiles();
- if (hexes.contains(attackOriginHex))
- hexes.erase(attackOriginHex);
+ at.friendlyCreaturePositions.insert(processTargets({ 11, 111, 2, 12, 6, 16 }));
- for(const BattleHex & tile : hexes)
- {
- //friendly stacks can also be damaged by Dragon Breath
- const auto * st = battleGetUnitByPos(tile, true);
- if(st && st != attacker)
- at.friendlyCreaturePositions.insert(tile);
- }
- }
- else if(attacker->hasBonusOfType(BonusType::TWO_HEX_ATTACK_BREATH) || attacker->hasBonusOfType(BonusType::PRISM_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(attacker->hasBonusOfType(BonusType::TWO_HEX_ATTACK_BREATH))
+ at.friendlyCreaturePositions.insert(processTargets({ 11 }));
- for(int i = 0; i < 3; i++)
- {
- if(direction != BattleHex::NONE) //only adjacent hexes are subject of dragon breath calculation
- {
- BattleHex nextHex = destinationTile.cloneInDirection(direction, false);
+ if (attacker->hasBonusOfType(BonusType::PRISM_HEX_ATTACK_BREATH))
+ at.friendlyCreaturePositions.insert(processTargets({ 11, 12, 16 }));
- if ( defender->doubleWide() )
- {
- 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 (nextHex.isValid())
- {
- //friendly stacks can also be damaged by Dragon Breath
- const auto * st = battleGetUnitByPos(nextHex, true);
- if(st != nullptr && st != attacker) //but not unit itself (doublewide + prism attack)
- at.friendlyCreaturePositions.insert(nextHex);
- }
- }
-
- if(!attacker->hasBonusOfType(BonusType::PRISM_HEX_ATTACK_BREATH))
- break;
-
- // only needed for prism
- int tmpDirection = static_cast(direction) + 2;
- if(tmpDirection > static_cast(BattleHex::EDir::LEFT))
- tmpDirection -= static_cast(BattleHex::EDir::TOP);
- direction = static_cast(tmpDirection);
- }
- }
return at;
}
@@ -1473,9 +1466,9 @@ battle::Units CBattleInfoCallback::getAttackedBattleUnits(
return units;
}
-std::set CBattleInfoCallback::getAttackedCreatures(const CStack* attacker, const BattleHex & destinationTile, bool rangedAttack, BattleHex attackerPos) const
+std::pair, bool> CBattleInfoCallback::getAttackedCreatures(const CStack* attacker, const BattleHex & destinationTile, bool rangedAttack, BattleHex attackerPos) const
{
- std::set attackedCres;
+ std::pair, bool> attackedCres;
RETURN_IF_NOT_BATTLE(attackedCres);
AttackableTiles at;
@@ -1490,7 +1483,7 @@ std::set CBattleInfoCallback::getAttackedCreatures(const CStack*
const CStack * st = battleGetStackByPos(tile, true);
if(st && battleGetOwner(st) != battleGetOwner(attacker)) //only hostile stacks - does it work well with Berserk?
{
- attackedCres.insert(st);
+ attackedCres.first.insert(st);
}
}
for (const BattleHex & tile : at.friendlyCreaturePositions)
@@ -1498,9 +1491,22 @@ std::set CBattleInfoCallback::getAttackedCreatures(const CStack*
const CStack * st = battleGetStackByPos(tile, true);
if(st) //friendly stacks can also be damaged by Dragon Breath
{
- attackedCres.insert(st);
+ attackedCres.first.insert(st);
}
}
+
+ if (at.friendlyCreaturePositions.empty())
+ {
+ attackedCres.second = !attackedCres.first.empty();
+ }
+ else
+ {
+ for (const BattleHex & tile : at.friendlyCreaturePositions)
+ for (const auto & st : attackedCres.first)
+ if (st->coversPos(tile))
+ attackedCres.second = true;
+ }
+
return attackedCres;
}
diff --git a/lib/battle/CBattleInfoCallback.h b/lib/battle/CBattleInfoCallback.h
index 720deef9a..c0825eb74 100644
--- a/lib/battle/CBattleInfoCallback.h
+++ b/lib/battle/CBattleInfoCallback.h
@@ -38,13 +38,12 @@ namespace spells
struct DLL_LINKAGE AttackableTiles
{
+ /// Hexes on which only hostile units will be targeted
BattleHexArray hostileCreaturePositions;
- BattleHexArray friendlyCreaturePositions; //for Dragon Breath
- template void serialize(Handler &h)
- {
- h & hostileCreaturePositions;
- h & friendlyCreaturePositions;
- }
+ /// for Dragon Breath, hexes on which both friendly and hostile creatures will be targeted
+ BattleHexArray friendlyCreaturePositions;
+ /// for animation purposes, if any of targets are on specified positions, unit should play alternative animation
+ BattleHexArray overrideAnimationPositions;
};
struct DLL_LINKAGE BattleClientInterfaceData
@@ -155,7 +154,7 @@ public:
BattleHex attackerPos = BattleHex::INVALID,
BattleHex defenderPos = BattleHex::INVALID) const; //calculates range of multi-hex attacks
- std::set getAttackedCreatures(const CStack* attacker, const BattleHex & destinationTile, bool rangedAttack, BattleHex attackerPos = BattleHex::INVALID) const; //calculates range of multi-hex attacks
+ std::pair, bool> getAttackedCreatures(const CStack* attacker, const 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
ReachabilityInfo getReachability(const battle::Unit * unit) const;
diff --git a/lib/bonuses/BonusEnum.h b/lib/bonuses/BonusEnum.h
index 1b68f9cfe..be8113504 100644
--- a/lib/bonuses/BonusEnum.h
+++ b/lib/bonuses/BonusEnum.h
@@ -185,6 +185,9 @@ class JsonNode;
BONUS_NAME(PRISM_HEX_ATTACK_BREATH) /*eg. dragons*/ \
BONUS_NAME(BASE_TILE_MOVEMENT_COST) /*minimal cost for moving offroad*/ \
BONUS_NAME(HERO_SPELL_CASTS_PER_COMBAT_TURN) /**/ \
+ BONUS_NAME(MULTIHEX_UNIT_ATTACK) /*eg. dragons*/ \
+ BONUS_NAME(MULTIHEX_ENEMY_ATTACK) /*eg. dragons*/ \
+ BONUS_NAME(MULTIHEX_ANIMATION) /*eg. dragons*/ \
/* end of list */
diff --git a/lib/json/JsonBonus.cpp b/lib/json/JsonBonus.cpp
index fc4cda9c6..e1abb7f09 100644
--- a/lib/json/JsonBonus.cpp
+++ b/lib/json/JsonBonus.cpp
@@ -192,6 +192,77 @@ static void loadBonusSubtype(BonusSubtypeID & subtype, BonusType type, const Jso
}
}
+static void loadBonusAddInfo(CAddInfo & var, BonusType type, const JsonNode & node)
+{
+ const auto & getFirstValue = [](const JsonNode & jsonNode) -> const JsonNode &
+ {
+ if (jsonNode.isVector())
+ return jsonNode[0];
+ else
+ return jsonNode;
+ };
+
+ const JsonNode & value = node["addInfo"];
+ if (value.isNull())
+ return;
+
+ switch (type)
+ {
+ case BonusType::IMPROVED_NECROMANCY:
+ case BonusType::SPECIAL_ADD_VALUE_ENCHANT:
+ case BonusType::SPECIAL_FIXED_VALUE_ENCHANT:
+ case BonusType::DESTRUCTION:
+ case BonusType::LIMITED_SHOOTING_RANGE:
+ case BonusType::ACID_BREATH:
+ case BonusType::SPELLCASTER:
+ case BonusType::FEROCITY:
+ case BonusType::PRIMARY_SKILL:
+ // 1 number
+ var = getFirstValue(value).Integer();
+ break;
+ case BonusType::SPECIAL_UPGRADE:
+ case BonusType::TRANSMUTATION:
+ // 1 creature ID
+ LIBRARY->identifiers()->requestIdentifier("creature", getFirstValue(value), [&](si32 identifier) { var = identifier; });
+ break;
+ case BonusType::DEATH_STARE:
+ // 1 spell ID
+ LIBRARY->identifiers()->requestIdentifier("spell", getFirstValue(value), [&](si32 identifier) { var = identifier; });
+ break;
+ case BonusType::SPELL_BEFORE_ATTACK:
+ case BonusType::SPELL_AFTER_ATTACK:
+ // 3 numbers
+ var.resize(3);
+ var[0] = value[0].Integer();
+ var[1] = value[1].Integer();
+ var[2] = value[2].Integer();
+ break;
+ case BonusType::MULTIHEX_UNIT_ATTACK:
+ case BonusType::MULTIHEX_ENEMY_ATTACK:
+ case BonusType::MULTIHEX_ANIMATION:
+ for (const auto & sequence : value.Vector())
+ {
+ static const std::map charToDirection = {
+ { 'f', 1 }, { 'l', 6}, {'r', 2}, {'b', 4}
+ };
+ int converted = 0;
+ for (const auto & ch : boost::adaptors::reverse(sequence.String()))
+ {
+ char chLower = std::tolower(ch);
+ if (charToDirection.count(chLower))
+ converted = 10 * converted + charToDirection.at(chLower);
+ }
+ var.push_back(converted);
+ }
+ break;
+ default:
+ for(const auto & i : bonusNameMap)
+ if(i.second == type)
+ logMod->warn("Bonus type %s does not supports addInfo!", i.first );
+
+ }
+}
+
static void loadBonusSourceInstance(BonusSourceID & sourceInstance, BonusSource sourceType, const JsonNode & node)
{
if (node.isNull())
@@ -384,57 +455,6 @@ std::shared_ptr JsonUtils::parseBonus(const JsonVector & ability_vec)
return b;
}
-void JsonUtils::resolveAddInfo(CAddInfo & var, const JsonNode & node)
-{
- const JsonNode & value = node["addInfo"];
- if (!value.isNull())
- {
- switch (value.getType())
- {
- case JsonNode::JsonType::DATA_INTEGER:
- var = static_cast(value.Integer());
- break;
- case JsonNode::JsonType::DATA_FLOAT:
- var = static_cast(value.Float());
- break;
- case JsonNode::JsonType::DATA_STRING:
- LIBRARY->identifiers()->requestIdentifier(value, [&](si32 identifier)
- {
- var = identifier;
- });
- break;
- case JsonNode::JsonType::DATA_VECTOR:
- {
- const JsonVector & vec = value.Vector();
- var.resize(vec.size());
- for(int i = 0; i < vec.size(); i++)
- {
- switch(vec[i].getType())
- {
- case JsonNode::JsonType::DATA_INTEGER:
- var[i] = static_cast(vec[i].Integer());
- break;
- case JsonNode::JsonType::DATA_FLOAT:
- var[i] = static_cast(vec[i].Float());
- break;
- case JsonNode::JsonType::DATA_STRING:
- LIBRARY->identifiers()->requestIdentifier(vec[i], [&var,i](si32 identifier)
- {
- var[i] = identifier;
- });
- break;
- default:
- logMod->error("Error! Wrong identifier used for value of addInfo[%d].", i);
- }
- }
- break;
- }
- default:
- logMod->error("Error! Wrong identifier used for value of addInfo.");
- }
- }
-}
-
std::shared_ptr JsonUtils::parseLimiter(const JsonNode & limiter)
{
switch(limiter.getType())
@@ -665,7 +685,7 @@ bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b)
b->stacking = ability["stacking"].String();
- resolveAddInfo(b->additionalInfo, ability);
+ loadBonusAddInfo(b->additionalInfo, b->type, ability);
b->turnsRemain = static_cast(ability["turns"].Float());
@@ -832,7 +852,7 @@ CSelector JsonUtils::parseSelector(const JsonNode & ability)
value = &ability["addInfo"];
if(!value->isNull())
{
- resolveAddInfo(info, ability["addInfo"]);
+ loadBonusAddInfo(info, type, ability["addInfo"]);
ret = ret.And(Selector::info()(info));
}
value = &ability["effectRange"];
diff --git a/lib/json/JsonBonus.h b/lib/json/JsonBonus.h
index 521a08226..97f50b788 100644
--- a/lib/json/JsonBonus.h
+++ b/lib/json/JsonBonus.h
@@ -26,7 +26,6 @@ namespace JsonUtils
bool parseBonus(const JsonNode & ability, Bonus * placement);
std::shared_ptr parseLimiter(const JsonNode & limiter);
CSelector parseSelector(const JsonNode &ability);
- void resolveAddInfo(CAddInfo & var, const JsonNode & node);
}
VCMI_LIB_NAMESPACE_END
diff --git a/lib/networkPacks/PacksForClientBattle.h b/lib/networkPacks/PacksForClientBattle.h
index 6c50d9953..47260e5b5 100644
--- a/lib/networkPacks/PacksForClientBattle.h
+++ b/lib/networkPacks/PacksForClientBattle.h
@@ -249,7 +249,7 @@ struct DLL_LINKAGE BattleAttack : public CPackForClient
std::vector bsa;
ui32 stackAttacking = 0;
ui32 flags = 0; //uses Eflags (below)
- enum EFlags { SHOT = 1, COUNTER = 2, LUCKY = 4, UNLUCKY = 8, BALLISTA_DOUBLE_DMG = 16, DEATH_BLOW = 32, SPELL_LIKE = 64, LIFE_DRAIN = 128 };
+ enum EFlags { SHOT = 1, COUNTER = 2, LUCKY = 4, UNLUCKY = 8, BALLISTA_DOUBLE_DMG = 16, DEATH_BLOW = 32, SPELL_LIKE = 64, LIFE_DRAIN = 128, CUSTOM_ANIMATION = 256};
BattleHex tile;
SpellID spellID = SpellID::NONE; //for SPELL_LIKE
@@ -286,6 +286,10 @@ struct DLL_LINKAGE BattleAttack : public CPackForClient
{
return flags & LIFE_DRAIN;
}
+ bool playCustomAnimation() const
+ {
+ return flags & CUSTOM_ANIMATION;
+ }
void visitTyped(ICPackVisitor & visitor) override;
diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp
index d14379a7c..cf2726084 100644
--- a/server/battles/BattleActionProcessor.cpp
+++ b/server/battles/BattleActionProcessor.cpp
@@ -965,13 +965,16 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const
applyBattleEffects(battle, bat, attackerState, fireShield, defender, healInfo, distance, false);
//multiple-hex normal attack
- std::set attackedCreatures = battle.getAttackedCreatures(attacker, targetHex, bat.shot()); //creatures other than primary target
+ const auto & [attackedCreatures, useCustomAnimation] = battle.getAttackedCreatures(attacker, targetHex, bat.shot()); //creatures other than primary target
for(const CStack * stack : attackedCreatures)
{
if(stack != defender && stack->alive()) //do not hit same stack twice
applyBattleEffects(battle, bat, attackerState, fireShield, stack, healInfo, distance, true);
}
+ if (useCustomAnimation)
+ bat.flags |= BattleAttack::CUSTOM_ANIMATION;
+
std::shared_ptr bonus = attacker->getFirstBonus(Selector::type()(BonusType::SPELL_LIKE_ATTACK));
if(bonus && ranged && bonus->subtype.hasValue()) //TODO: make it work in melee?
{
diff --git a/test/battle/CBattleInfoCallbackTest.cpp b/test/battle/CBattleInfoCallbackTest.cpp
index d2efc7396..263d382c5 100644
--- a/test/battle/CBattleInfoCallbackTest.cpp
+++ b/test/battle/CBattleInfoCallbackTest.cpp
@@ -229,6 +229,16 @@ public:
return unit;
}
+ UnitFake & addCerberi(BattleHex hex, BattleSide side)
+ {
+ auto & unit = addRegularMelee(hex, side);
+
+ unit.addCreatureAbility(BonusType::THREE_HEADED_ATTACK);
+ unit.makeDoubleWide();
+
+ return unit;
+ }
+
UnitFake & addDragon(BattleHex hex, BattleSide side)
{
auto & unit = addRegularMelee(hex, side);
@@ -252,6 +262,91 @@ public:
}
};
+//// CERBERI 3-HEADED ATTACKS
+
+TEST_F(AttackableHexesTest, CerberiAttackerRight)
+{
+ // #
+ // X A D
+ // #
+ UnitFake & attacker = addCerberi(35, BattleSide::ATTACKER);
+ UnitFake & defender = addRegularMelee(attacker.getPosition().cloneInDirection(BattleHex::RIGHT), BattleSide::DEFENDER);
+ UnitFake & right = addRegularMelee(attacker.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), BattleSide::DEFENDER);
+ UnitFake & left = addRegularMelee(attacker.getPosition().cloneInDirection(BattleHex::TOP_RIGHT), BattleSide::DEFENDER);
+
+ auto attacked = getAttackedUnits(attacker, defender, defender.getPosition());
+
+ EXPECT_TRUE(vstd::contains(attacked, &defender));
+ EXPECT_TRUE(vstd::contains(attacked, &right));
+ EXPECT_TRUE(vstd::contains(attacked, &left));
+}
+
+TEST_F(AttackableHexesTest, CerberiAttackerTopRight)
+{
+ // # D
+ // X A #
+ //
+ UnitFake & attacker = addCerberi(35, BattleSide::ATTACKER);
+ UnitFake & defender = addRegularMelee(attacker.getPosition().cloneInDirection(BattleHex::TOP_RIGHT), BattleSide::DEFENDER);
+ UnitFake & right = addRegularMelee(attacker.getPosition().cloneInDirection(BattleHex::RIGHT), BattleSide::DEFENDER);
+ UnitFake & left = addRegularMelee(attacker.getPosition().cloneInDirection(BattleHex::TOP_LEFT), BattleSide::DEFENDER);
+
+ auto attacked = getAttackedUnits(attacker, defender, defender.getPosition());
+
+ EXPECT_TRUE(vstd::contains(attacked, &right));
+ EXPECT_TRUE(vstd::contains(attacked, &left));
+}
+
+TEST_F(AttackableHexesTest, CerberiAttackerTopMiddle)
+{
+ // # D #
+ // X A
+ //
+ UnitFake & attacker = addCerberi(35, BattleSide::ATTACKER);
+ UnitFake & defender = addRegularMelee(attacker.getPosition().cloneInDirection(BattleHex::TOP_LEFT), BattleSide::DEFENDER);
+ UnitFake & right = addRegularMelee(attacker.getPosition().cloneInDirection(BattleHex::TOP_RIGHT), BattleSide::DEFENDER);
+ UnitFake & left = addRegularMelee(attacker.occupiedHex().cloneInDirection(BattleHex::TOP_LEFT), BattleSide::DEFENDER);
+
+ auto attacked = getAttackedUnits(attacker, defender, defender.getPosition());
+
+ EXPECT_TRUE(vstd::contains(attacked, &right));
+ EXPECT_TRUE(vstd::contains(attacked, &left));
+}
+
+TEST_F(AttackableHexesTest, CerberiAttackerTopLeft)
+{
+ // D #
+ // # X A
+ //
+ UnitFake & attacker = addCerberi(40, BattleSide::ATTACKER);
+ UnitFake & defender = addRegularMelee(attacker.occupiedHex().cloneInDirection(BattleHex::TOP_LEFT), BattleSide::DEFENDER);
+ UnitFake & right = addRegularMelee(attacker.occupiedHex().cloneInDirection(BattleHex::TOP_RIGHT), BattleSide::DEFENDER);
+ UnitFake & left = addRegularMelee(attacker.occupiedHex().cloneInDirection(BattleHex::LEFT), BattleSide::DEFENDER);
+
+ auto attacked = getAttackedUnits(attacker, defender, defender.getPosition());
+
+ EXPECT_TRUE(vstd::contains(attacked, &right));
+ EXPECT_TRUE(vstd::contains(attacked, &left));
+}
+
+TEST_F(AttackableHexesTest, CerberiAttackerLeft)
+{
+ // #
+ // D X A
+ // #
+ UnitFake & attacker = addCerberi(40, BattleSide::ATTACKER);
+ UnitFake & defender = addRegularMelee(attacker.occupiedHex().cloneInDirection(BattleHex::LEFT), BattleSide::DEFENDER);
+ UnitFake & right = addRegularMelee(attacker.occupiedHex().cloneInDirection(BattleHex::TOP_LEFT), BattleSide::DEFENDER);
+ UnitFake & left = addRegularMelee(attacker.occupiedHex().cloneInDirection(BattleHex::BOTTOM_LEFT), BattleSide::DEFENDER);
+
+ auto attacked = getAttackedUnits(attacker, defender, defender.getPosition());
+
+ EXPECT_TRUE(vstd::contains(attacked, &right));
+ EXPECT_TRUE(vstd::contains(attacked, &left));
+}
+
+//// DRAGON BREATH AS ATTACKER
+
TEST_F(AttackableHexesTest, DragonRightRegular_RightHorithontalBreath)
{
// X A D #
@@ -261,6 +356,7 @@ TEST_F(AttackableHexesTest, DragonRightRegular_RightHorithontalBreath)
auto attacked = getAttackedUnits(attacker, defender, defender.getPosition());
+ EXPECT_TRUE(vstd::contains(attacked, &defender));
EXPECT_TRUE(vstd::contains(attacked, &next));
}
@@ -282,26 +378,26 @@ TEST_F(AttackableHexesTest, DragonDragonVerticalDownHead_VerticalDownBreathFromH
{
// X A
// D X target D
- // #
+ // #
UnitFake & attacker = addDragon(35, BattleSide::ATTACKER);
UnitFake & defender = addDragon(attacker.getPosition().cloneInDirection(BattleHex::BOTTOM_LEFT), BattleSide::DEFENDER);
- UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), BattleSide::DEFENDER);
+ UnitFake & next = addRegularMelee(defender.occupiedHex().cloneInDirection(BattleHex::BOTTOM_RIGHT), BattleSide::DEFENDER);
auto attacked = getAttackedUnits(attacker, defender, defender.getPosition());
EXPECT_TRUE(vstd::contains(attacked, &next));
}
-TEST_F(AttackableHexesTest, DragonDragonVerticalDownHeadReverse_VerticalDownBreathFromHead)
+TEST_F(AttackableHexesTest, DragonDragonVerticalDownHead_VerticalRightBreathFromHead)
{
- // A X
- // X D target D
- // #
- UnitFake & attacker = addDragon(36, BattleSide::DEFENDER);
- UnitFake & defender = addDragon(attacker.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), BattleSide::ATTACKER);
- UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_LEFT), BattleSide::ATTACKER);
+ // X A
+ // D X target X
+ // #
+ UnitFake & attacker = addDragon(35, BattleSide::ATTACKER);
+ UnitFake & defender = addDragon(attacker.getPosition().cloneInDirection(BattleHex::BOTTOM_LEFT), BattleSide::DEFENDER);
+ UnitFake & next = addRegularMelee(defender.occupiedHex().cloneInDirection(BattleHex::BOTTOM_RIGHT), BattleSide::DEFENDER);
- auto attacked = getAttackedUnits(attacker, defender, defender.getPosition());
+ auto attacked = getAttackedUnits(attacker, defender, defender.occupiedHex());
EXPECT_TRUE(vstd::contains(attacked, &next));
}
@@ -334,6 +430,36 @@ TEST_F(AttackableHexesTest, DragonDragonHeadBottomRight_BottomRightBreathFromHea
EXPECT_TRUE(vstd::contains(attacked, &next));
}
+TEST_F(AttackableHexesTest, DragonLeftBottomDragonBackToBack_LeftBottomBreathFromBackHex)
+{
+ // X A
+ // D X target X
+ // #
+ UnitFake & attacker = addDragon(8, BattleSide::ATTACKER);
+ UnitFake & defender = addDragon(attacker.occupiedHex().cloneInDirection(BattleHex::BOTTOM_LEFT).cloneInDirection(BattleHex::LEFT), BattleSide::DEFENDER);
+ UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), BattleSide::DEFENDER);
+
+ auto attacked = getAttackedUnits(attacker, defender, defender.occupiedHex());
+
+ EXPECT_TRUE(vstd::contains(attacked, &next));
+}
+
+//// DRAGON BREATH AS DEFENDER
+
+TEST_F(AttackableHexesTest, DragonDragonVerticalDownHeadReverse_VerticalDownBreathFromHead)
+{
+ // A X
+ // X D target D
+ // #
+ UnitFake & attacker = addDragon(36, BattleSide::DEFENDER);
+ UnitFake & defender = addDragon(attacker.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), BattleSide::ATTACKER);
+ UnitFake & next = addRegularMelee(defender.occupiedHex().cloneInDirection(BattleHex::BOTTOM_LEFT), BattleSide::ATTACKER);
+
+ auto attacked = getAttackedUnits(attacker, defender, defender.getPosition());
+
+ EXPECT_TRUE(vstd::contains(attacked, &next));
+}
+
TEST_F(AttackableHexesTest, DragonVerticalDownDragonBackReverse_VerticalDownBreath)
{
// A X
@@ -361,28 +487,14 @@ TEST_F(AttackableHexesTest, DragonRightBottomDragonHeadReverse_RightBottomBreath
EXPECT_TRUE(vstd::contains(attacked, &next));
}
-TEST_F(AttackableHexesTest, DragonLeftBottomDragonBackToBack_LeftBottomBreathFromBackHex)
-{
- // X A
- // D X target X
- // #
- UnitFake & attacker = addDragon(8, BattleSide::ATTACKER);
- UnitFake & defender = addDragon(attacker.occupiedHex().cloneInDirection(BattleHex::BOTTOM_LEFT).cloneInDirection(BattleHex::LEFT), BattleSide::DEFENDER);
- UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), BattleSide::DEFENDER);
-
- 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
+ // # N
+ // X D target D
+ // A X
UnitFake & attacker = addDragon(35, BattleSide::DEFENDER);
UnitFake & defender = addDragon(8, BattleSide::ATTACKER);
- UnitFake & next = addDragon(2, BattleSide::ATTACKER);
+ UnitFake & next = addDragon(1, BattleSide::ATTACKER);
startBattle();
redirectUnitsToFake();