1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-08-13 19:54:17 +02:00

Merge pull request #3467 from dydzio0614/hota-fixes

Fix HotA creature abilities (add bonuses allowing implementing them)
This commit is contained in:
Ivan Savenko
2024-01-12 21:55:18 +02:00
committed by GitHub
11 changed files with 277 additions and 54 deletions

View File

@@ -185,6 +185,9 @@
"vcmi.battleWindow.damageEstimation.kills" : "%d will perish",
"vcmi.battleWindow.damageEstimation.kills.1" : "%d will perish",
"vcmi.battleWindow.killed" : "Killed",
"vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s were killed by accurate shots!",
"vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s was killed with an accurate shot!",
"vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s were killed by accurate shots!",
"vcmi.battleResultsWindow.applyResultsLabel" : "Apply battle result",
@@ -329,7 +332,9 @@
"vcmi.stackExperience.rank.8" : "Elite",
"vcmi.stackExperience.rank.9" : "Master",
"vcmi.stackExperience.rank.10" : "Ace",
"core.bonus.ACCURATE_SHOT.name": "Accurate Shot",
"core.bonus.ACCURATE_SHOT.description": "Has (${val}% - penalties) extra kills chance",
"core.bonus.ADDITIONAL_ATTACK.name": "Double Strike",
"core.bonus.ADDITIONAL_ATTACK.description": "Attacks twice",
"core.bonus.ADDITIONAL_RETALIATION.name": "Additional retaliations",
@@ -368,6 +373,8 @@
"core.bonus.ENCHANTER.description": "Can cast mass ${subtype.spell} every turn",
"core.bonus.ENCHANTED.name": "Enchanted",
"core.bonus.ENCHANTED.description": "Affected by permanent ${subtype.spell}",
"core.bonus.ENEMY_ATTACK_REDUCTION.name": "Ignore Attack (${val}%)",
"core.bonus.ENEMY_ATTACK_REDUCTION.description": "When being attacked, ${val}% of the attacker's attack is ignored",
"core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Ignore Defense (${val}%)",
"core.bonus.ENEMY_DEFENCE_REDUCTION.description": "When attacking, ${val}% of the defender's defense is ignored",
"core.bonus.FIRE_IMMUNITY.name": "Fire immunity",
@@ -380,6 +387,8 @@
"core.bonus.FEAR.description": "Causes Fear on an enemy stack",
"core.bonus.FEARLESS.name": "Fearless",
"core.bonus.FEARLESS.description": "Immune to Fear ability",
"core.bonus.FEROCITY.name": "Ferocity",
"core.bonus.FEROCITY.description": "Attacks ${val} additional times if killed anybody",
"core.bonus.FLYING.name": "Fly",
"core.bonus.FLYING.description": "Flies when moving (ignores obstacles)",
"core.bonus.FREE_SHOOTING.name": "Shoot Close",
@@ -434,6 +443,8 @@
"core.bonus.REBIRTH.description": "${val}% of stack will rise after death",
"core.bonus.RETURN_AFTER_STRIKE.name": "Attack and Return",
"core.bonus.RETURN_AFTER_STRIKE.description": "Returns after melee attack",
"core.bonus.REVENGE.name": "Revenge",
"core.bonus.REVENGE.description": "Deals extra damage based on attacker's lost health in battle",
"core.bonus.SHOOTER.name": "Ranged",
"core.bonus.SHOOTER.description": "Creature can shoot",
"core.bonus.SHOOTS_ALL_ADJACENT.name": "Shoot all around",

View File

@@ -327,7 +327,7 @@ void BattleStacksController::showStackAmountBox(Canvas & canvas, const CStack *
boxPosition = owner.fieldController->hexPositionLocal(frontPos).center() + Point(-8, -14);
}
Point textPosition = amountBG->dimensions()/2 + boxPosition;
Point textPosition = amountBG->dimensions()/2 + boxPosition + Point(0, 1);
canvas.draw(amountBG, boxPosition);
canvas.drawText(textPosition, EFonts::FONT_TINY, Colors::WHITE, ETextAlignment::CENTER, TextOperations::formatMetric(stack->getCount(), 4));

View File

@@ -3,6 +3,13 @@
// LEVEL_SPELL_IMMUNITY
{
"ACCURATE_SHOT":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_DIST"
}
},
"ADDITIONAL_ATTACK":
{
"graphics":
@@ -145,6 +152,14 @@
}
},
"ENEMY_ATTACK_REDUCTION":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_RDEF"
}
},
"ENEMY_DEFENCE_REDUCTION":
{
"graphics":
@@ -185,6 +200,14 @@
}
},
"FEROCITY":
{
"graphics":
{
"icon": ""
}
},
"FLYING":
{
"graphics":
@@ -428,6 +451,14 @@
}
},
"REVENGE":
{
"graphics":
{
"icon": ""
}
},
"SHOOTER":
{
"graphics":

View File

@@ -608,6 +608,25 @@ Affected unit can use ranged attacks only within specified range
- val: max shooting range in hexes
- addInfo: optional, range at which ranged penalty will trigger (default is 10)
### FEROCITY
Affected unit will attack additional times if killed creatures in target unit during attacking (including ADDITIONAL_ATTACK bonus attacks)
- val: amount of additional attacks (negative number will reduce number of unperformed attacks if any left)
- addInfo: optional, amount of creatures needed to kill (default is 1)
### ENEMY_ATTACK_REDUCTION
Affected unit will ignore specified percentage of attacked unit attack (Nix)
- val: amount of attack points to ignore, percentage
### REVENGE
Affected unit will deal more damage based on percentage of self health lost compared to amount on start of battle
(formula: `square_root((total_unit_count + 1) * 1_creature_max_health / (current_whole_unit_health + 1_creature_max_health) - 1)`.
Result is then multiplied separately by min and max base damage of unit and result is additive bonus to total damage at end of calculation)
## Special abilities
### CATAPULT
@@ -725,6 +744,15 @@ If player has affected unit under his control in any army, he will receive addit
Affected unit will not use spellcast as default attack option
### ACCURATE_SHOT
Affected unit will kill additional units after attack, similar to death stare - works only for ranged attack
- subtype:
spell identifier for spell that receives value that should be killed on input, spell.deathStare is used by default, use 'accurateShot' as part of spell name to allow detection for proper battle log description
- val:
chance to kill, counted separately for each unit in attacking stack, percentage. Chance gets lessened by 2/3 with range penalty and effect won't trigger with wall penalty. At most (stack size \* chance / 100 **[rounded up]**) units can be killed at once. TODO: recheck formula
## Creature spellcasting and activated abilities
### SPELLCASTER
@@ -759,17 +787,21 @@ 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\]
- additional info - \[X, Y, Z\]
- X - spell level
- 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.
When enabled - spells from specific layer will not be cast until target has all spells from previous layer on him. Spell from last layer is on repeat if none of spells on lower layers expired.
### SPELL_BEFORE_ATTACK
- subtype - spell id
- value - chance %
- additional info - \[X, Y\]
- additional info - \[X, Y, Z\]
- X - spell level
- 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.
When enabled - spells from specific layer will not be cast until target has all spells from previous layer on him. Spell from last layer is on repeat if none of spells on lower layers expired.
### SPECIFIC_SPELL_POWER
@@ -778,7 +810,7 @@ Determines how many times per combat affected creature can cast its targeted spe
### CREATURE_SPELL_POWER
- value: Spell Power of offensive spell cast unit, divided by 100. ie. Faerie Dragons have value fo 500, which gives them 5 Spell Power for each unit in the stack.
- value: Spell Power of offensive spell cast unit, multiplied by 100. ie. Faerie Dragons have value fo 500, which gives them 5 Spell Power for each unit in the stack.
### CREATURE_ENCHANT_POWER

View File

@@ -515,6 +515,7 @@ static void loadBonusSubtype(BonusSubtypeID & subtype, BonusType type, const Jso
case BonusType::SPECIFIC_SPELL_POWER:
case BonusType::ENCHANTED:
case BonusType::MORE_DAMAGE_FROM_SPELL:
case BonusType::ACCURATE_SHOT:
case BonusType::NOT_ACTIVE:
{
VLC->identifiers()->requestIdentifier( "spell", node, [&subtype](int32_t identifier)

View File

@@ -9,6 +9,7 @@
*/
#include "StdInc.h"
#include "DamageCalculator.h"
#include "CBattleInfoCallback.h"
#include "Unit.h"
@@ -124,7 +125,19 @@ int DamageCalculator::getActorAttackBase() const
int DamageCalculator::getActorAttackEffective() const
{
return getActorAttackBase() + getActorAttackSlayer();
return getActorAttackBase() + getActorAttackSlayer() + getActorAttackIgnored();
}
int DamageCalculator::getActorAttackIgnored() const
{
int multAttackReductionPercent = battleBonusValue(info.defender, Selector::type()(BonusType::ENEMY_ATTACK_REDUCTION));
if(multAttackReductionPercent > 0)
{
int reduction = (getActorAttackBase() * multAttackReductionPercent + 49) / 100; //using ints so 1.5 for 5 attack is rounded down as in HotA / h3assist etc. (keep in mind h3assist 1.2 shows wrong value for 15 attack points and unupg. nix)
return -std::min(reduction, getActorAttackBase());
}
return 0;
}
int DamageCalculator::getActorAttackSlayer() const
@@ -266,6 +279,20 @@ double DamageCalculator::getAttackHateFactor() const
return allHateEffects->valOfBonuses(Selector::subtype()(BonusSubtypeID(info.defender->creatureId()))) / 100.0;
}
double DamageCalculator::getAttackRevengeFactor() const
{
if(info.attacker->hasBonusOfType(BonusType::REVENGE)) //HotA Haspid ability
{
int totalStackCount = info.attacker->unitBaseAmount();
int currentStackHealth = info.attacker->getAvailableHealth();
int creatureHealth = info.attacker->getMaxHealth();
return sqrt(static_cast<double>((totalStackCount + 1) * creatureHealth) / (currentStackHealth + creatureHealth) - 1);
}
return 0.0;
}
double DamageCalculator::getDefenseSkillFactor() const
{
int defenseAdvantage = getTargetDefenseEffective() - getActorAttackEffective();
@@ -433,7 +460,8 @@ std::vector<double> DamageCalculator::getAttackFactors() const
getAttackJoustingFactor(),
getAttackDeathBlowFactor(),
getAttackDoubleDamageFactor(),
getAttackHateFactor()
getAttackHateFactor(),
getAttackRevengeFactor()
};
}
@@ -503,12 +531,10 @@ DamageEstimation DamageCalculator::calculateDmgRange() const
for (auto & factor : defenseFactors)
{
assert(factor >= 0.0);
defenseFactorTotal *= ( 1 - std::min(1.0, factor));
defenseFactorTotal *= (1 - std::min(1.0, factor));
}
double resultingFactor = std::min(8.0, attackFactorTotal) * std::max( 0.01, defenseFactorTotal);
info.defender->getTotalHealth();
double resultingFactor = attackFactorTotal * defenseFactorTotal;
DamageRange damageDealt {
std::max<int64_t>( 1.0, std::floor(damageBase.min * resultingFactor)),

View File

@@ -38,6 +38,7 @@ class DLL_LINKAGE DamageCalculator
int getActorAttackBase() const;
int getActorAttackEffective() const;
int getActorAttackSlayer() const;
int getActorAttackIgnored() const;
int getTargetDefenseBase() const;
int getTargetDefenseEffective() const;
int getTargetDefenseIgnored() const;
@@ -50,6 +51,7 @@ class DLL_LINKAGE DamageCalculator
double getAttackDeathBlowFactor() const;
double getAttackDoubleDamageFactor() const;
double getAttackHateFactor() const;
double getAttackRevengeFactor() const;
double getDefenseSkillFactor() const;
double getDefenseArmorerFactor() const;

View File

@@ -48,7 +48,7 @@ class JsonNode;
BONUS_NAME(FLYING) \
BONUS_NAME(SHOOTER) \
BONUS_NAME(CHARGE_IMMUNITY) \
BONUS_NAME(ADDITIONAL_ATTACK) \
BONUS_NAME(ADDITIONAL_ATTACK) /*val: number of additional attacks to perform*/ \
BONUS_NAME(UNLIMITED_RETALIATIONS) \
BONUS_NAME(NO_MELEE_PENALTY) \
BONUS_NAME(JOUSTING) /*for champions*/ \
@@ -57,8 +57,8 @@ class JsonNode;
BONUS_NAME(MAGIC_RESISTANCE) /*in % (value)*/ \
BONUS_NAME(CHANGES_SPELL_COST_FOR_ALLY) /*in mana points (value) , eg. mage*/ \
BONUS_NAME(CHANGES_SPELL_COST_FOR_ENEMY) /*in mana points (value) , eg. pegasus */ \
BONUS_NAME(SPELL_AFTER_ATTACK) /* subtype - spell id, value - chance %, addInfo[0] - level, addInfo[1] -> [0 - all attacks, 1 - shot only, 2 - melee only] */ \
BONUS_NAME(SPELL_BEFORE_ATTACK) /* subtype - spell id, value - chance %, addInfo[0] - level, addInfo[1] -> [0 - all attacks, 1 - shot only, 2 - melee only] */ \
BONUS_NAME(SPELL_AFTER_ATTACK) /* subtype - spell id, value - chance %, addInfo[0] - level, addInfo[1] -> [0 - all attacks, 1 - shot only, 2 - melee only], addInfo[2] -> spell layer for multiple SPELL_AFTER_ATTACK bonuses (default none [-1]) */ \
BONUS_NAME(SPELL_BEFORE_ATTACK) /* subtype - spell id, value - chance %, addInfo[0] - level, addInfo[1] -> [0 - all attacks, 1 - shot only, 2 - melee only], addInfo[2] -> spell layer for multiple SPELL_BEFORE_ATTACK bonuses (default none [-1]) */ \
BONUS_NAME(SPELL_RESISTANCE_AURA) /*eg. unicorns, value - resistance bonus in % for adjacent creatures*/ \
BONUS_NAME(LEVEL_SPELL_IMMUNITY) /*creature is immune to all spell with level below or equal to value of this bonus */ \
BONUS_NAME(BLOCK_MAGIC_ABOVE) /*blocks casting spells of the level > value */ \
@@ -173,6 +173,10 @@ class JsonNode;
BONUS_NAME(UNLIMITED_MOVEMENT) /*cheat bonus*/ \
BONUS_NAME(MAX_MORALE) /*cheat bonus*/ \
BONUS_NAME(MAX_LUCK) /*cheat bonus*/ \
BONUS_NAME(FEROCITY) /*extra attacks, only if at least some creatures killed while attacking target unit, val = amount of additional attacks, additional info = amount of creatures killed to trigger (default 1)*/ \
BONUS_NAME(ACCURATE_SHOT) /*HotA Sea Dog-like ability - ranged only, val = full arrow trigger percent, subtype = spell identifier that killed value goes through (death stare by default) - use 'accurateShot' as part of spell name for proper battle log description*/ \
BONUS_NAME(ENEMY_ATTACK_REDUCTION) /*in % (value) eg. Nix (HotA)*/ \
BONUS_NAME(REVENGE) /*additional damage based on how many units in stack died - formula: sqrt((number of creatures at battle start + 1) * creature health) / (total health now + 1 creature health) - 1) * 100% */ \
/* end of list */

View File

@@ -20,10 +20,12 @@
#include "../../battle/CBattleInfoCallback.h"
#include "../../networkPacks/PacksForClientBattle.h"
#include "../../CGeneralTextHandler.h"
#include "../../Languages.h"
#include "../../serializer/JsonSerializeFormat.h"
#include <vcmi/spells/Spell.h>
VCMI_LIB_NAMESPACE_BEGIN
namespace spells
@@ -152,6 +154,16 @@ void Damage::describeEffect(std::vector<MetaString> & log, const Mechanics * m,
m->caster->getCasterName(line);
log.push_back(line);
}
else if(m->getSpell()->getJsonKey().find("accurateShot") != std::string::npos && !multiple)
{
MetaString line;
std::string preferredLanguage = VLC->generaltexth->getPreferredLanguage();
std::string textID = "vcmi.battleWindow.accurateShot.resultDescription";
line.appendTextID(Languages::getPluralFormTextID( preferredLanguage, kills, textID));
line.replaceNumber(kills);
firstTarget->addNameReplacement(line, kills != 1);
log.push_back(line);
}
else if(m->getSpellIndex() == SpellID::THUNDERBOLT && !multiple)
{
{

View File

@@ -270,6 +270,9 @@ bool BattleActionProcessor::doAttackAction(const CBattleInfoCallback & battle, c
const bool firstStrike = destinationStack->hasBonusOfType(BonusType::FIRST_STRIKE);
const bool retaliation = destinationStack->ableToRetaliate();
bool ferocityApplied = false;
int32_t defenderInitialQuantity = destinationStack->getCount();
for (int i = 0; i < totalAttacks; ++i)
{
//first strike
@@ -282,6 +285,18 @@ bool BattleActionProcessor::doAttackAction(const CBattleInfoCallback & battle, c
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
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
@@ -1104,19 +1119,13 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const
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))
{
std::set<SpellID> spellsToCast;
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)
{
bool castMe = false;
@@ -1175,11 +1184,128 @@ 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)
{
attackCasting(battle, ranged, BonusType::SPELL_BEFORE_ATTACK, attacker, defender); //no death stare / acid breath needed?
}
void BattleActionProcessor::HandleDeathStareAndPirateShot(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender)
{
// 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);
/* 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.
*/
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.
{
if(!ranged)
return;
if(battle.battleHasWallPenalty(attacker, attacker->getPosition(), defender->getPosition()))
return;
}
int singleCreatureKillChancePercent;
if(bonus->type == BonusType::ACCURATE_SHOT)
{
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 = singleCreatureKillChancePercent / 100.0;
vstd::amin(chanceToKill, 1); //cap at 100%
std::binomial_distribution<> distribution(attacker->getCount(), chanceToKill);
int killedCreatures = distribution(gameHandler->getRandomGenerator().getStdGenerator());
int maxToKill = (attacker->getCount() * singleCreatureKillChancePercent + 99) / 100;
vstd::amin(killedCreatures, maxToKill);
if(bonus->type == BonusType::DEATH_STARE)
killedCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, BonusCustomSubtype::deathStareCommander)) / defender->level();
if(killedCreatures)
{
//TODO: death stare or accurate shot was not originally available for multiple-hex attacks, but...
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>();
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(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
@@ -1193,37 +1319,9 @@ void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback &
return;
}
if(attacker->hasBonusOfType(BonusType::DEATH_STARE))
if(attacker->hasBonusOfType(BonusType::DEATH_STARE) || attacker->hasBonusOfType(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);
double chanceToKill = attacker->valOfBonuses(BonusType::DEATH_STARE, BonusCustomSubtype::deathStareGorgon) / 100.0f;
vstd::amin(chanceToKill, 1); //cap at 100%
std::binomial_distribution<> distribution(attacker->getCount(), chanceToKill);
int staredCreatures = distribution(gameHandler->getRandomGenerator().getStdGenerator());
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);
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();
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);
}
HandleDeathStareAndPirateShot(battle, ranged, attacker, defender);
}
if(!defender->alive())

View File

@@ -8,6 +8,7 @@
*
*/
#pragma once
#include "bonuses/BonusList.h"
VCMI_LIB_NAMESPACE_BEGIN
@@ -43,8 +44,13 @@ class BattleActionProcessor : boost::noncopyable
void makeAttack(const CBattleInfoCallback & battle, const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter);
void handleAttackBeforeCasting(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender);
void HandleDeathStareAndPirateShot(const CBattleInfoCallback &battle, bool ranged, const CStack *attacker, const CStack *defender);
void handleAfterAttackCasting(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender);
void attackCasting(const CBattleInfoCallback & battle, bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender);
void attackCasting(const CBattleInfoCallback & battle, bool ranged, BonusType attackMode, const battle::Unit * attacker, const CStack * defender);
std::set<SpellID> getSpellsForAttackCasting(TConstBonusListPtr spells, const CStack *defender);
// damage, drain life & fire shield; returns amount of drained life
int64_t applyBattleEffects(const CBattleInfoCallback & battle, BattleAttack & bat, std::shared_ptr<battle::CUnitState> attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary);