|
@@ -270,6 +270,9 @@ bool BattleActionProcessor::doAttackAction(const CBattleInfoCallback & battle, c
|
|
|
|
|
|
const bool firstStrike = destinationStack->hasBonusOfType(BonusType::FIRST_STRIKE);
|
|
const bool firstStrike = destinationStack->hasBonusOfType(BonusType::FIRST_STRIKE);
|
|
const bool retaliation = destinationStack->ableToRetaliate();
|
|
const bool retaliation = destinationStack->ableToRetaliate();
|
|
|
|
+ bool ferocityApplied = false;
|
|
|
|
+ int32_t defenderInitialQuantity = destinationStack->getCount();
|
|
|
|
+
|
|
for (int i = 0; i < totalAttacks; ++i)
|
|
for (int i = 0; i < totalAttacks; ++i)
|
|
{
|
|
{
|
|
//first strike
|
|
//first strike
|
|
@@ -282,6 +285,18 @@ bool BattleActionProcessor::doAttackAction(const CBattleInfoCallback & battle, c
|
|
if(stack->alive() && !stack->hasBonusOfType(BonusType::NOT_ACTIVE) && destinationStack->alive())
|
|
if(stack->alive() && !stack->hasBonusOfType(BonusType::NOT_ACTIVE) && destinationStack->alive())
|
|
{
|
|
{
|
|
makeAttack(battle, stack, destinationStack, (i ? 0 : distance), destinationTile, i==0, false, false);//no distance travelled on second attack
|
|
makeAttack(battle, stack, destinationStack, (i ? 0 : distance), destinationTile, i==0, false, false);//no distance travelled on second attack
|
|
|
|
+
|
|
|
|
+ if(!ferocityApplied && stack->hasBonusOfType(BonusType::FEROCITY))
|
|
|
|
+ {
|
|
|
|
+ auto ferocityBonus = stack->getBonus(Selector::type()(BonusType::FEROCITY));
|
|
|
|
+ int32_t requiredCreaturesToKill = ferocityBonus->additionalInfo != CAddInfo::NONE ? ferocityBonus->additionalInfo[0] : 1;
|
|
|
|
+ if(defenderInitialQuantity - destinationStack->getCount() >= requiredCreaturesToKill)
|
|
|
|
+ {
|
|
|
|
+ ferocityApplied = true;
|
|
|
|
+ int additionalAttacksCount = stack->valOfBonuses(BonusType::FEROCITY);
|
|
|
|
+ totalAttacks += additionalAttacksCount;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|
|
//counterattack
|
|
//counterattack
|
|
@@ -1104,19 +1119,13 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const
|
|
handleAfterAttackCasting(battle, ranged, attacker, defender);
|
|
handleAfterAttackCasting(battle, ranged, attacker, defender);
|
|
}
|
|
}
|
|
|
|
|
|
-void BattleActionProcessor::attackCasting(const CBattleInfoCallback & battle, bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender)
|
|
|
|
|
|
+void BattleActionProcessor::attackCasting(const CBattleInfoCallback & battle, bool ranged, BonusType attackMode, const battle::Unit * attacker, const CStack * defender)
|
|
{
|
|
{
|
|
if(attacker->hasBonusOfType(attackMode))
|
|
if(attacker->hasBonusOfType(attackMode))
|
|
{
|
|
{
|
|
- std::set<SpellID> spellsToCast;
|
|
|
|
TConstBonusListPtr spells = attacker->getBonuses(Selector::type()(attackMode));
|
|
TConstBonusListPtr spells = attacker->getBonuses(Selector::type()(attackMode));
|
|
- for(const auto & sf : *spells)
|
|
|
|
- {
|
|
|
|
- if (sf->subtype.as<SpellID>() != SpellID())
|
|
|
|
- spellsToCast.insert(sf->subtype.as<SpellID>());
|
|
|
|
- else
|
|
|
|
- logMod->error("Invalid spell to cast during attack!");
|
|
|
|
- }
|
|
|
|
|
|
+ std::set<SpellID> spellsToCast = getSpellsForAttackCasting(spells, defender);
|
|
|
|
+
|
|
for(SpellID spellID : spellsToCast)
|
|
for(SpellID spellID : spellsToCast)
|
|
{
|
|
{
|
|
bool castMe = false;
|
|
bool castMe = false;
|
|
@@ -1175,55 +1184,144 @@ void BattleActionProcessor::attackCasting(const CBattleInfoCallback & battle, bo
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+std::set<SpellID> BattleActionProcessor::getSpellsForAttackCasting(TConstBonusListPtr spells, const CStack *defender)
|
|
|
|
+{
|
|
|
|
+ std::set<SpellID> spellsToCast;
|
|
|
|
+ constexpr int unlayeredItemsInternalLayer = -1;
|
|
|
|
+
|
|
|
|
+ std::map<int, std::vector<std::shared_ptr<Bonus>>> spellsWithBackupLayers;
|
|
|
|
+
|
|
|
|
+ for(int i = 0; i < spells->size(); i++)
|
|
|
|
+ {
|
|
|
|
+ std::shared_ptr<Bonus> bonus = spells->operator[](i);
|
|
|
|
+ int layer = bonus->additionalInfo[2];
|
|
|
|
+ vstd::amax(layer, -1);
|
|
|
|
+ spellsWithBackupLayers[layer].push_back(bonus);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ auto addSpellsFromLayer = [&](int layer) -> void
|
|
|
|
+ {
|
|
|
|
+ assert(spellsWithBackupLayers.find(layer) != spellsWithBackupLayers.end());
|
|
|
|
+
|
|
|
|
+ for(const auto & spell : spellsWithBackupLayers[layer])
|
|
|
|
+ {
|
|
|
|
+ if (spell->subtype.as<SpellID>() != SpellID())
|
|
|
|
+ spellsToCast.insert(spell->subtype.as<SpellID>());
|
|
|
|
+ else
|
|
|
|
+ logGlobal->error("Invalid spell to cast during attack!");
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ if(spellsWithBackupLayers.find(unlayeredItemsInternalLayer) != spellsWithBackupLayers.end())
|
|
|
|
+ {
|
|
|
|
+ addSpellsFromLayer(unlayeredItemsInternalLayer);
|
|
|
|
+ spellsWithBackupLayers.erase(unlayeredItemsInternalLayer);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ for(auto item : spellsWithBackupLayers)
|
|
|
|
+ {
|
|
|
|
+ bool areCurrentLayerSpellsApplied = std::all_of(item.second.begin(), item.second.end(),
|
|
|
|
+ [&](const std::shared_ptr<Bonus> spell)
|
|
|
|
+ {
|
|
|
|
+ std::vector<SpellID> activeSpells = defender->activeSpells();
|
|
|
|
+ return vstd::find(activeSpells, spell->subtype.as<SpellID>()) != activeSpells.end();
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ if(!areCurrentLayerSpellsApplied || item.first == spellsWithBackupLayers.rbegin()->first)
|
|
|
|
+ {
|
|
|
|
+ addSpellsFromLayer(item.first);
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return spellsToCast;
|
|
|
|
+}
|
|
|
|
+
|
|
void BattleActionProcessor::handleAttackBeforeCasting(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender)
|
|
void BattleActionProcessor::handleAttackBeforeCasting(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender)
|
|
{
|
|
{
|
|
attackCasting(battle, ranged, BonusType::SPELL_BEFORE_ATTACK, attacker, defender); //no death stare / acid breath needed?
|
|
attackCasting(battle, ranged, BonusType::SPELL_BEFORE_ATTACK, attacker, defender); //no death stare / acid breath needed?
|
|
}
|
|
}
|
|
|
|
|
|
-void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender)
|
|
|
|
|
|
+void BattleActionProcessor::HandleDeathStareAndPirateShot(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender)
|
|
{
|
|
{
|
|
- if(!attacker->alive() || !defender->alive()) // can be already dead
|
|
|
|
- return;
|
|
|
|
|
|
+ // mechanics of Death Stare as in H3:
|
|
|
|
+ // each gorgon have 10% chance to kill (counted separately in H3) -> binomial distribution
|
|
|
|
+ //original formula x = min(x, (gorgons_count + 9)/10);
|
|
|
|
|
|
- attackCasting(battle, ranged, BonusType::SPELL_AFTER_ATTACK, attacker, defender);
|
|
|
|
|
|
+ /* mechanics of Accurate Shot as in HotA:
|
|
|
|
+ * each creature in an attacking stack has a X% chance of killing a creature in the attacked squad,
|
|
|
|
+ * but the total number of killed creatures cannot be more than (number of creatures in an attacking squad) * X/100 (rounded up).
|
|
|
|
+ * X = 3 multiplier for shooting without penalty and X = 2 if shooting with penalty. Ability doesn't work if shooting at creatures behind walls.
|
|
|
|
+ */
|
|
|
|
|
|
- if(!defender->alive())
|
|
|
|
|
|
+ auto bonus = attacker->getBonus(Selector::type()(BonusType::DEATH_STARE));
|
|
|
|
+ if(bonus == nullptr)
|
|
|
|
+ bonus = attacker->getBonus(Selector::type()(BonusType::ACCURATE_SHOT));
|
|
|
|
+
|
|
|
|
+ if(bonus->type == BonusType::ACCURATE_SHOT) //should not work from behind walls, except when being defender or under effect of golden bow etc.
|
|
{
|
|
{
|
|
- //don't try death stare or acid breath on dead stack (crash!)
|
|
|
|
- return;
|
|
|
|
|
|
+ if(!ranged)
|
|
|
|
+ return;
|
|
|
|
+ if(battle.battleHasWallPenalty(attacker, attacker->getPosition(), defender->getPosition()))
|
|
|
|
+ return;
|
|
}
|
|
}
|
|
|
|
|
|
- if(attacker->hasBonusOfType(BonusType::DEATH_STARE))
|
|
|
|
|
|
+ int singleCreatureKillChancePercent;
|
|
|
|
+ if(bonus->type == BonusType::ACCURATE_SHOT)
|
|
{
|
|
{
|
|
- // mechanics of Death Stare as in H3:
|
|
|
|
- // each gorgon have 10% chance to kill (counted separately in H3) -> binomial distribution
|
|
|
|
- //original formula x = min(x, (gorgons_count + 9)/10);
|
|
|
|
|
|
+ singleCreatureKillChancePercent = attacker->valOfBonuses(BonusType::ACCURATE_SHOT);
|
|
|
|
+ if(battle.battleHasDistancePenalty(attacker, attacker->getPosition(), defender->getPosition()))
|
|
|
|
+ singleCreatureKillChancePercent = (singleCreatureKillChancePercent * 2) / 3;
|
|
|
|
+ }
|
|
|
|
+ else //DEATH_STARE
|
|
|
|
+ singleCreatureKillChancePercent = attacker->valOfBonuses(BonusType::DEATH_STARE, BonusCustomSubtype::deathStareGorgon);
|
|
|
|
|
|
- double chanceToKill = attacker->valOfBonuses(BonusType::DEATH_STARE, BonusCustomSubtype::deathStareGorgon) / 100.0f;
|
|
|
|
- vstd::amin(chanceToKill, 1); //cap at 100%
|
|
|
|
|
|
+ double chanceToKill = singleCreatureKillChancePercent / 100.0;
|
|
|
|
+ vstd::amin(chanceToKill, 1); //cap at 100%
|
|
|
|
+ std::binomial_distribution<> distribution(attacker->getCount(), chanceToKill);
|
|
|
|
+ int killedCreatures = distribution(gameHandler->getRandomGenerator().getStdGenerator());
|
|
|
|
|
|
- std::binomial_distribution<> distribution(attacker->getCount(), chanceToKill);
|
|
|
|
|
|
+ int maxToKill = (attacker->getCount() * singleCreatureKillChancePercent + 99) / 100;
|
|
|
|
+ vstd::amin(killedCreatures, maxToKill);
|
|
|
|
|
|
- int staredCreatures = distribution(gameHandler->getRandomGenerator().getStdGenerator());
|
|
|
|
|
|
+ if(bonus->type == BonusType::DEATH_STARE)
|
|
|
|
+ killedCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, BonusCustomSubtype::deathStareCommander)) / defender->level();
|
|
|
|
|
|
- double cap = 1 / std::max(chanceToKill, (double)(0.01));//don't divide by 0
|
|
|
|
- int maxToKill = static_cast<int>((attacker->getCount() + cap - 1) / cap); //not much more than chance * count
|
|
|
|
- vstd::amin(staredCreatures, maxToKill);
|
|
|
|
|
|
+ if(killedCreatures)
|
|
|
|
+ {
|
|
|
|
+ //TODO: death stare or accurate shot was not originally available for multiple-hex attacks, but...
|
|
|
|
|
|
- staredCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, BonusCustomSubtype::deathStareCommander)) / defender->level();
|
|
|
|
- if(staredCreatures)
|
|
|
|
- {
|
|
|
|
- //TODO: death stare was not originally available for multiple-hex attacks, but...
|
|
|
|
- const CSpell * spell = SpellID(SpellID::DEATH_STARE).toSpell();
|
|
|
|
|
|
+ SpellID spellID = SpellID(SpellID::DEATH_STARE); //also used as fallback spell for ACCURATE_SHOT
|
|
|
|
+ if(bonus->type == BonusType::ACCURATE_SHOT && bonus->subtype.as<SpellID>() != SpellID::NONE)
|
|
|
|
+ spellID = bonus->subtype.as<SpellID>();
|
|
|
|
|
|
- spells::AbilityCaster caster(attacker, 0);
|
|
|
|
|
|
+ const CSpell * spell = spellID.toSpell();
|
|
|
|
+ spells::AbilityCaster caster(attacker, 0);
|
|
|
|
|
|
- spells::BattleCast parameters(&battle, &caster, spells::Mode::PASSIVE, spell);
|
|
|
|
- spells::Target target;
|
|
|
|
- target.emplace_back(defender);
|
|
|
|
- parameters.setEffectValue(staredCreatures);
|
|
|
|
- parameters.cast(gameHandler->spellEnv, target);
|
|
|
|
- }
|
|
|
|
|
|
+ spells::BattleCast parameters(&battle, &caster, spells::Mode::PASSIVE, spell);
|
|
|
|
+ spells::Target target;
|
|
|
|
+ target.emplace_back(defender);
|
|
|
|
+ parameters.setEffectValue(killedCreatures);
|
|
|
|
+ parameters.cast(gameHandler->spellEnv, target);
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender)
|
|
|
|
+{
|
|
|
|
+ if(!attacker->alive() || !defender->alive()) // can be already dead
|
|
|
|
+ return;
|
|
|
|
+
|
|
|
|
+ attackCasting(battle, ranged, BonusType::SPELL_AFTER_ATTACK, attacker, defender);
|
|
|
|
+
|
|
|
|
+ if(!defender->alive())
|
|
|
|
+ {
|
|
|
|
+ //don't try death stare or acid breath on dead stack (crash!)
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if(attacker->hasBonusOfType(BonusType::DEATH_STARE) || attacker->hasBonusOfType(BonusType::ACCURATE_SHOT))
|
|
|
|
+ {
|
|
|
|
+ HandleDeathStareAndPirateShot(battle, ranged, attacker, defender);
|
|
}
|
|
}
|
|
|
|
|
|
if(!defender->alive())
|
|
if(!defender->alive())
|