diff --git a/AI/BattleAI/AttackPossibility.cpp b/AI/BattleAI/AttackPossibility.cpp index 7dd30449b..cfda077f3 100644 --- a/AI/BattleAI/AttackPossibility.cpp +++ b/AI/BattleAI/AttackPossibility.cpp @@ -13,9 +13,9 @@ // Eventually only IBattleInfoCallback and battle::Unit should be used, // CUnitState should be private and CStack should be removed completely -uint64_t averageDmg(const TDmgRange & range) +uint64_t averageDmg(const DamageRange & range) { - return (range.first + range.second) / 2; + return (range.min + range.max) / 2; } AttackPossibility::AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack) @@ -52,7 +52,7 @@ int64_t AttackPossibility::calculateDamageReduce( // FIXME: provide distance info for Jousting bonus auto enemyDamageBeforeAttack = cb.battleEstimateDamage(defender, attacker, 0); auto enemiesKilled = damageDealt / defender->MaxHealth() + (damageDealt % defender->MaxHealth() >= defender->getFirstHPleft() ? 1 : 0); - auto enemyDamage = averageDmg(enemyDamageBeforeAttack); + auto enemyDamage = averageDmg(enemyDamageBeforeAttack.damage); auto damagePerEnemy = enemyDamage / (double)defender->getCount(); return (int64_t)(damagePerEnemy * (enemiesKilled * KILL_BOUNTY + damageDealt * HEALTH_BOUNTY / (double)defender->MaxHealth())); @@ -85,7 +85,7 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(const BattleAttackInfo & a auto rangeDmg = state.battleEstimateDamage(rangeAttackInfo); auto meleeDmg = state.battleEstimateDamage(meleeAttackInfo); - int64_t gain = averageDmg(rangeDmg) - averageDmg(meleeDmg) + 1; + int64_t gain = averageDmg(rangeDmg.damage) - averageDmg(meleeDmg.damage) + 1; res += gain; } @@ -156,16 +156,16 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf { int64_t damageDealt, damageReceived, defenderDamageReduce, attackerDamageReduce; - TDmgRange retaliation(0, 0); + DamageEstimation retaliation; auto attackDmg = state.battleEstimateDamage(ap.attack, &retaliation); - vstd::amin(attackDmg.first, defenderState->getAvailableHealth()); - vstd::amin(attackDmg.second, defenderState->getAvailableHealth()); + vstd::amin(attackDmg.damage.min, defenderState->getAvailableHealth()); + vstd::amin(attackDmg.damage.max, defenderState->getAvailableHealth()); - vstd::amin(retaliation.first, ap.attackerState->getAvailableHealth()); - vstd::amin(retaliation.second, ap.attackerState->getAvailableHealth()); + vstd::amin(retaliation.damage.min, ap.attackerState->getAvailableHealth()); + vstd::amin(retaliation.damage.max, ap.attackerState->getAvailableHealth()); - damageDealt = averageDmg(attackDmg); + damageDealt = averageDmg(attackDmg.damage); defenderDamageReduce = calculateDamageReduce(attacker, defender, damageDealt, state); ap.attackerState->afterAttack(attackInfo.shooting, false); @@ -175,7 +175,7 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf if (!attackInfo.shooting && defenderState->ableToRetaliate() && !counterAttacksBlocked) { - damageReceived = averageDmg(retaliation); + damageReceived = averageDmg(retaliation.damage); attackerDamageReduce = calculateDamageReduce(defender, attacker, damageReceived, state); defenderState->afterAttack(attackInfo.shooting, true); } diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index c426e2186..cbc4a2d7f 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -68,7 +68,7 @@ int64_t BattleExchangeVariant::trackAttack( static const auto selectorBlocksRetaliation = Selector::type()(Bonus::BLOCKS_RETALIATION); const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation); - TDmgRange retaliation; + DamageEstimation retaliation; // FIXME: provide distance info for Jousting bonus BattleAttackInfo bai(attacker.get(), defender.get(), 0, shooting); @@ -78,7 +78,7 @@ int64_t BattleExchangeVariant::trackAttack( } auto attack = cb.battleEstimateDamage(bai, &retaliation); - int64_t attackDamage = (attack.first + attack.second) / 2; + int64_t attackDamage = (attack.damage.min + attack.damage.max) / 2; int64_t defenderDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), defender.get(), attackDamage, cb); int64_t attackerDamageReduce = 0; @@ -108,9 +108,9 @@ int64_t BattleExchangeVariant::trackAttack( if(defender->alive() && defender->ableToRetaliate() && !counterAttacksBlocked && !shooting) { - if(retaliation.second != 0) + if(retaliation.damage.max != 0) { - auto retaliationDamage = (retaliation.first + retaliation.second) / 2; + auto retaliationDamage = (retaliation.damage.min + retaliation.damage.max) / 2; attackerDamageReduce = AttackPossibility::calculateDamageReduce(defender.get(), attacker.get(), retaliationDamage, cb); if(!evaluateOnly) diff --git a/AI/BattleAI/StackWithBonuses.cpp b/AI/BattleAI/StackWithBonuses.cpp index 27d223359..78fcdbb1b 100644 --- a/AI/BattleAI/StackWithBonuses.cpp +++ b/AI/BattleAI/StackWithBonuses.cpp @@ -428,9 +428,9 @@ uint32_t HypotheticBattle::nextUnitId() const return nextId++; } -int64_t HypotheticBattle::getActualDamage(const TDmgRange & damage, int32_t attackerCount, vstd::RNG & rng) const +int64_t HypotheticBattle::getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const { - return (damage.first + damage.second) / 2; + return (damage.min + damage.max) / 2; } int64_t HypotheticBattle::getTreeVersion() const diff --git a/AI/BattleAI/StackWithBonuses.h b/AI/BattleAI/StackWithBonuses.h index 48070ca40..916c7b8c0 100644 --- a/AI/BattleAI/StackWithBonuses.h +++ b/AI/BattleAI/StackWithBonuses.h @@ -138,7 +138,7 @@ public: uint32_t nextUnitId() const override; - int64_t getActualDamage(const TDmgRange & damage, int32_t attackerCount, vstd::RNG & rng) const override; + int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override; int64_t getTreeVersion() const; diff --git a/AI/StupidAI/StupidAI.cpp b/AI/StupidAI/StupidAI.cpp index 446fbb29c..ff0887c94 100644 --- a/AI/StupidAI/StupidAI.cpp +++ b/AI/StupidAI/StupidAI.cpp @@ -56,9 +56,10 @@ public: void calcDmg(const CStack * ourStack) { // FIXME: provide distance info for Jousting bonus - TDmgRange retal, dmg = cbc->battleEstimateDamage(ourStack, s, 0, &retal); - adi = static_cast((dmg.first + dmg.second) / 2); - adr = static_cast((retal.first + retal.second) / 2); + DamageEstimation retal; + DamageEstimation dmg = cbc->battleEstimateDamage(ourStack, s, 0, &retal); + adi = static_cast((dmg.damage.min + dmg.damage.max) / 2); + adr = static_cast((retal.damage.min + retal.damage.max) / 2); } bool operator==(const EnemyInfo& ei) const diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index 5c08b71d3..68b0c81ee 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -84,6 +84,17 @@ "vcmi.battleWindow.pressKeyToSkipIntro" : "Press any key to skip battle intro", + "vcmi.battleWindow.damageEstimation.melee" : "Attack %CREATURE (%DAMAGE).", + "vcmi.battleWindow.damageEstimation.meleeKills" : "Attack %CREATURE (%DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.ranged" : "Shoot %CREATURE (%SHOTS, %DAMAGE).", + "vcmi.battleWindow.damageEstimation.rangedKills" : "Shoot %CREATURE (%SHOTS, %DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.shots" : "%d shots left", + "vcmi.battleWindow.damageEstimation.shots.1" : "%d shot left", + "vcmi.battleWindow.damageEstimation.damage" : "%d damage", + "vcmi.battleWindow.damageEstimation.damage.1" : "%d damage", + "vcmi.battleWindow.damageEstimation.kills" : "%d will perish", + "vcmi.battleWindow.damageEstimation.kills.1" : "%d will perish", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Show Available Creatures", "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Show Available Creatures}\n\n Shows creatures available to purchase instead of their growth in town summary (bottom-left corner).", "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Show Weekly Growth of Creatures", diff --git a/Mods/vcmi/config/vcmi/ukrainian.json b/Mods/vcmi/config/vcmi/ukrainian.json index 13d028381..758bb53b8 100644 --- a/Mods/vcmi/config/vcmi/ukrainian.json +++ b/Mods/vcmi/config/vcmi/ukrainian.json @@ -84,6 +84,17 @@ "vcmi.battleOptions.skipBattleIntroMusic.help": "{Пропускати вступну музику}\n\n Пропускати коротку музику, яка грає на початку кожної битви перед початком дії. Також можна пропустити, натиснувши клавішу ESC.", "vcmi.battleWindow.pressKeyToSkipIntro" : "Натисніть будь-яку клавішу, щоб розпочати бій", + + "vcmi.battleWindow.damageEstimation.melee" : "Атакувати %CREATURE (%DAMAGE).", + "vcmi.battleWindow.damageEstimation.meleeKills" : "Атакувати %CREATURE (%DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.ranged" : "Стріляти в %CREATURE (%SHOTS, %DAMAGE).", + "vcmi.battleWindow.damageEstimation.rangedKills" : "Стріляти в %CREATURE (%SHOTS, %DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.shots" : "%d пострілів залишилось", + "vcmi.battleWindow.damageEstimation.shots.1" : "%d постріл залишився", + "vcmi.battleWindow.damageEstimation.damage" : "%d одиниць пошкоджень", + "vcmi.battleWindow.damageEstimation.damage.1" : "%d одиниця пошкодження", + "vcmi.battleWindow.damageEstimation.kills" : "%d загинуть", + "vcmi.battleWindow.damageEstimation.kills.1" : "%d загине", "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Показувати доступних істот", "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Показувати доступних істот}\n\n Показує істот, яких можна придбати, замість їхнього приросту у зведенні по місту (нижній лівий кут).", diff --git a/client/battle/BattleActionsController.cpp b/client/battle/BattleActionsController.cpp index 4995a78c9..9d51aba2a 100644 --- a/client/battle/BattleActionsController.cpp +++ b/client/battle/BattleActionsController.cpp @@ -25,19 +25,90 @@ #include "../windows/CCreatureWindow.h" #include "../../CCallback.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/CGeneralTextHandler.h" #include "../../lib/CStack.h" #include "../../lib/battle/BattleAction.h" #include "../../lib/spells/CSpellHandler.h" #include "../../lib/spells/ISpellMechanics.h" #include "../../lib/spells/Problem.h" -#include "../../lib/CGeneralTextHandler.h" -static std::string formatDmgRange(std::pair dmgRange) +struct TextReplacement { - if (dmgRange.first != dmgRange.second) - return (boost::format("%d - %d") % dmgRange.first % dmgRange.second).str(); - else - return (boost::format("%d") % dmgRange.first).str(); + std::string placeholder; + std::string replacement; +}; + +using TextReplacementList = std::vector; + +static std::string replacePlaceholders(std::string input, const TextReplacementList & format ) +{ + for(const auto & entry : format) + boost::replace_all(input, entry.placeholder, entry.replacement); + + return input; +} + +static std::string translatePlural(int amount, const std::string& baseTextID) +{ + if(amount == 1) + return CGI->generaltexth->translate(baseTextID + ".1"); + return CGI->generaltexth->translate(baseTextID); +} + +static std::string formatPluralImpl(int amount, const std::string & amountString, const std::string & baseTextID) +{ + std::string baseString = translatePlural(amount, baseTextID); + TextReplacementList replacements { + { "%d", amountString } + }; + + return replacePlaceholders(baseString, replacements); +} + +static std::string formatPlural(int amount, const std::string & baseTextID) +{ + return formatPluralImpl(amount, std::to_string(amount), baseTextID); +} + +static std::string formatPlural(DamageRange range, const std::string & baseTextID) +{ + if (range.min == range.max) + return formatPlural(range.min, baseTextID); + + std::string rangeString = std::to_string(range.min) + " - " + std::to_string(range.max); + + return formatPluralImpl(range.max, rangeString, baseTextID); +} + +static std::string formatAttack(const DamageEstimation & estimation, const std::string & creatureName, const std::string & baseTextID, int shotsLeft) +{ + TextReplacementList replacements = { + { "%CREATURE", creatureName }, + { "%DAMAGE", formatPlural(estimation.damage, "vcmi.battleWindow.damageEstimation.damage") }, + { "%SHOTS", formatPlural(shotsLeft, "vcmi.battleWindow.damageEstimation.shots") }, + { "%KILLS", formatPlural(estimation.kills, "vcmi.battleWindow.damageEstimation.kills") }, + }; + + return replacePlaceholders(CGI->generaltexth->translate(baseTextID), replacements); +} + +static std::string formatMeleeAttack(const DamageEstimation & estimation, const std::string & creatureName) +{ + std::string baseTextID = estimation.kills.max == 0 ? + "vcmi.battleWindow.damageEstimation.melee" : + "vcmi.battleWindow.damageEstimation.meleeKills"; + + return formatAttack(estimation, creatureName, baseTextID, 0); +} + +static std::string formatRangedAttack(const DamageEstimation & estimation, const std::string & creatureName, int shotsLeft) +{ + std::string baseTextID = estimation.kills.max == 0 ? + "vcmi.battleWindow.damageEstimation.ranged" : + "vcmi.battleWindow.damageEstimation.rangedKills"; + + return formatAttack(estimation, creatureName, baseTextID, shotsLeft); } BattleActionsController::BattleActionsController(BattleInterface & owner): @@ -356,19 +427,17 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return { BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex); - TDmgRange damage = owner.curInt->cb->battleEstimateDamage(owner.stacksController->getActiveStack(), targetStack, attackFromHex); - std::string estDmgText = formatDmgRange(std::make_pair((ui32)damage.first, (ui32)damage.second)); //calculating estimated dmg - return (boost::format(CGI->generaltexth->allTexts[36]) % targetStack->getName() % estDmgText).str(); //Attack %s (%s damage) + DamageEstimation estimation = owner.curInt->cb->battleEstimateDamage(owner.stacksController->getActiveStack(), targetStack, attackFromHex); + return formatMeleeAttack(estimation, targetStack->getName()); } case PossiblePlayerBattleAction::SHOOT: { - auto const * shooter = owner.stacksController->getActiveStack(); + const auto * shooter = owner.stacksController->getActiveStack(); - TDmgRange damage = owner.curInt->cb->battleEstimateDamage(shooter, targetStack, shooter->getPosition()); - std::string estDmgText = formatDmgRange(std::make_pair((ui32)damage.first, (ui32)damage.second)); //calculating estimated dmg - //printing - Shoot %s (%d shots left, %s damage) - return (boost::format(CGI->generaltexth->allTexts[296]) % targetStack->getName() % shooter->shots.available() % estDmgText).str(); + DamageEstimation estimation = owner.curInt->cb->battleEstimateDamage(shooter, targetStack, shooter->getPosition()); + + return formatRangedAttack(estimation, targetStack->getName(), shooter->shots.available()); } case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: diff --git a/lib/GameConstants.h b/lib/GameConstants.h index d9265ffbf..d005af570 100644 --- a/lib/GameConstants.h +++ b/lib/GameConstants.h @@ -1295,7 +1295,6 @@ enum class EHealPower : ui8 // Typedef declarations typedef ui8 TFaction; typedef si64 TExpType; -typedef std::pair TDmgRange; typedef si32 TBonusSubtype; typedef si32 TQuantity; diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index 311c9fb34..bb473ef48 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -663,14 +663,14 @@ const IBonusBearer * BattleInfo::asBearer() const return this; } -int64_t BattleInfo::getActualDamage(const TDmgRange & damage, int32_t attackerCount, vstd::RNG & rng) const +int64_t BattleInfo::getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const { - if(damage.first != damage.second) + if(damage.min != damage.max) { int64_t sum = 0; auto howManyToAv = std::min(10, attackerCount); - auto rangeGen = rng.getInt64Range(damage.first, damage.second); + auto rangeGen = rng.getInt64Range(damage.min, damage.max); for(int32_t g = 0; g < howManyToAv; ++g) sum += rangeGen(); @@ -679,7 +679,7 @@ int64_t BattleInfo::getActualDamage(const TDmgRange & damage, int32_t attackerCo } else { - return damage.first; + return damage.min; } } diff --git a/lib/battle/BattleInfo.h b/lib/battle/BattleInfo.h index ab53fed25..02dcafe45 100644 --- a/lib/battle/BattleInfo.h +++ b/lib/battle/BattleInfo.h @@ -97,7 +97,7 @@ public: uint32_t nextUnitId() const override; - int64_t getActualDamage(const TDmgRange & damage, int32_t attackerCount, vstd::RNG & rng) const override; + int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override; ////////////////////////////////////////////////////////////////////////// // IBattleState diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index adb20351c..06a28cbba 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -715,57 +715,64 @@ bool CBattleInfoCallback::battleCanShoot(const battle::Unit * attacker, BattleHe return false; } -TDmgRange CBattleInfoCallback::calculateDmgRange(const BattleAttackInfo & info) const +DamageEstimation CBattleInfoCallback::calculateDmgRange(const BattleAttackInfo & info) const { DamageCalculator calculator(*this, info); return calculator.calculateDmgRange(); } -TDmgRange CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPosition, TDmgRange * retaliationDmg) const +DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPosition, DamageEstimation * retaliationDmg) const { - RETURN_IF_NOT_BATTLE(std::make_pair(0, 0)); + RETURN_IF_NOT_BATTLE({}); auto reachability = battleGetDistances(attacker, attacker->getPosition()); int movementDistance = reachability[attackerPosition]; return battleEstimateDamage(attacker, defender, movementDistance, retaliationDmg); } -TDmgRange CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int movementDistance, TDmgRange * retaliationDmg) const +DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int movementDistance, DamageEstimation * retaliationDmg) const { - RETURN_IF_NOT_BATTLE(std::make_pair(0, 0)); + RETURN_IF_NOT_BATTLE({}); const bool shooting = battleCanShoot(attacker, defender->getPosition()); const BattleAttackInfo bai(attacker, defender, movementDistance, shooting); return battleEstimateDamage(bai, retaliationDmg); } -TDmgRange CBattleInfoCallback::battleEstimateDamage(const BattleAttackInfo & bai, TDmgRange * retaliationDmg) const +DamageEstimation CBattleInfoCallback::battleEstimateDamage(const BattleAttackInfo & bai, DamageEstimation * retaliationDmg) const { - RETURN_IF_NOT_BATTLE(std::make_pair(0, 0)); + RETURN_IF_NOT_BATTLE({}); - TDmgRange ret = calculateDmgRange(bai); + DamageEstimation ret = calculateDmgRange(bai); if(retaliationDmg) { if(bai.shooting) { //FIXME: handle RANGED_RETALIATION - retaliationDmg->first = retaliationDmg->second = 0; + *retaliationDmg = DamageEstimation(); } else { //TODO: rewrite using boost::numeric::interval //TODO: rewire once more using interval-based fuzzy arithmetic - int64_t TDmgRange::* pairElems[] = {&TDmgRange::first, &TDmgRange::second}; - for (int i=0; i<2; ++i) + auto const & estimateRetaliation = [&]( int64_t damage) { auto retaliationAttack = bai.reverse(); - int64_t dmg = ret.*pairElems[i]; auto state = retaliationAttack.attacker->acquireState(); - state->damage(dmg); + state->damage(damage); retaliationAttack.attacker = state.get(); - retaliationDmg->*pairElems[!i] = calculateDmgRange(retaliationAttack).*pairElems[!i]; - } + return calculateDmgRange(retaliationAttack); + }; + + DamageEstimation retaliationMin = estimateRetaliation(ret.damage.min); + DamageEstimation retaliationMax = estimateRetaliation(ret.damage.min); + + retaliationDmg->damage.min = std::min(retaliationMin.damage.min, retaliationMax.damage.min); + retaliationDmg->damage.max = std::max(retaliationMin.damage.max, retaliationMax.damage.max); + + retaliationDmg->kills.min = std::min(retaliationMin.kills.min, retaliationMax.kills.min); + retaliationDmg->kills.max = std::max(retaliationMin.kills.max, retaliationMax.kills.max); } } diff --git a/lib/battle/CBattleInfoCallback.h b/lib/battle/CBattleInfoCallback.h index 5cc2c637d..104e31c01 100644 --- a/lib/battle/CBattleInfoCallback.h +++ b/lib/battle/CBattleInfoCallback.h @@ -117,14 +117,14 @@ public: bool battleIsUnitBlocked(const battle::Unit * unit) const; //returns true if there is neighboring enemy stack std::set battleAdjacentUnits(const battle::Unit * unit) const; - TDmgRange calculateDmgRange(const BattleAttackInfo & info) const; //charge - number of hexes travelled before attack (for champion's jousting); returns pair + DamageEstimation calculateDmgRange(const BattleAttackInfo & info) const; /// estimates damage dealt by attacker to defender; /// only non-random bonuses are considered in estimation /// returns pair - TDmgRange battleEstimateDamage(const BattleAttackInfo & bai, TDmgRange * retaliationDmg = nullptr) const; - TDmgRange battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPosition, TDmgRange * retaliationDmg = nullptr) const; - TDmgRange battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int movementDistance, TDmgRange * retaliationDmg = nullptr) const; + DamageEstimation battleEstimateDamage(const BattleAttackInfo & bai, DamageEstimation * retaliationDmg = nullptr) const; + DamageEstimation battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPosition, DamageEstimation * retaliationDmg = nullptr) const; + DamageEstimation battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int movementDistance, DamageEstimation * retaliationDmg = nullptr) const; bool battleHasDistancePenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const; bool battleHasWallPenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const; diff --git a/lib/battle/CUnitState.h b/lib/battle/CUnitState.h index e4c03ee97..ee130edb6 100644 --- a/lib/battle/CUnitState.h +++ b/lib/battle/CUnitState.h @@ -112,7 +112,10 @@ public: int32_t getFirstHPleft() const; int32_t getResurrected() const; + /// returns total remaining health int64_t available() const; + + /// returns total initial health int64_t total() const; void takeResurrected(); diff --git a/lib/battle/DamageCalculator.cpp b/lib/battle/DamageCalculator.cpp index 6b6e490b9..80d31bf48 100644 --- a/lib/battle/DamageCalculator.cpp +++ b/lib/battle/DamageCalculator.cpp @@ -20,10 +20,10 @@ VCMI_LIB_NAMESPACE_BEGIN -TDmgRange DamageCalculator::getBaseDamageSingle() const +DamageRange DamageCalculator::getBaseDamageSingle() const { - double minDmg = 0.0; - double maxDmg = 0.0; + int64_t minDmg = 0.0; + int64_t maxDmg = 0.0; minDmg = info.attacker->getMinDamage(info.shooting); maxDmg = info.attacker->getMaxDamage(info.shooting); @@ -63,7 +63,7 @@ TDmgRange DamageCalculator::getBaseDamageSingle() const return { minDmg, maxDmg }; } -TDmgRange DamageCalculator::getBaseDamageBlessCurse() const +DamageRange DamageCalculator::getBaseDamageBlessCurse() const { const std::string cachingStrForcedMinDamage = "type_ALWAYS_MINIMUM_DAMAGE"; static const auto selectorForcedMinDamage = Selector::type()(Bonus::ALWAYS_MINIMUM_DAMAGE); @@ -76,10 +76,10 @@ TDmgRange DamageCalculator::getBaseDamageBlessCurse() const int curseBlessAdditiveModifier = blessEffects->totalValue() - curseEffects->totalValue(); - TDmgRange baseDamage = getBaseDamageSingle(); - TDmgRange modifiedDamage = { - std::max(static_cast(1), baseDamage.first + curseBlessAdditiveModifier), - std::max(static_cast(1), baseDamage.second + curseBlessAdditiveModifier) + DamageRange baseDamage = getBaseDamageSingle(); + DamageRange modifiedDamage = { + std::max(static_cast(1), baseDamage.min + curseBlessAdditiveModifier), + std::max(static_cast(1), baseDamage.max + curseBlessAdditiveModifier) }; if(curseEffects->size() && blessEffects->size() ) @@ -91,29 +91,29 @@ TDmgRange DamageCalculator::getBaseDamageBlessCurse() const if(curseEffects->size()) { return { - modifiedDamage.first, - modifiedDamage.first + modifiedDamage.min, + modifiedDamage.min }; } if(blessEffects->size()) { return { - modifiedDamage.second, - modifiedDamage.second + modifiedDamage.max, + modifiedDamage.max }; } return modifiedDamage; } -TDmgRange DamageCalculator::getBaseDamageStack() const +DamageRange DamageCalculator::getBaseDamageStack() const { auto stackSize = info.attacker->getCount(); auto baseDamage = getBaseDamageBlessCurse(); return { - baseDamage.first * stackSize, - baseDamage.second * stackSize + baseDamage.min * stackSize, + baseDamage.max * stackSize }; } @@ -450,6 +450,25 @@ std::vector DamageCalculator::getDefenseFactors() const }; } +DamageRange DamageCalculator::getCasualties(const DamageRange & damageDealt) const +{ + return { + getCasualties(damageDealt.min), + getCasualties(damageDealt.max), + }; +} + +int64_t DamageCalculator::getCasualties(int64_t damageDealt) const +{ + if (damageDealt < info.defender->getFirstHPleft()) + return 0; + + int64_t damageLeft = damageDealt - info.defender->getFirstHPleft(); + int64_t killsLeft = damageLeft / info.defender->MaxHealth(); + + return 1 + killsLeft; +} + int DamageCalculator::battleBonusValue(const IBonusBearer * bearer, const CSelector & selector) const { auto noLimit = Selector::effectRange()(Bonus::NO_LIMIT); @@ -461,9 +480,9 @@ int DamageCalculator::battleBonusValue(const IBonusBearer * bearer, const CSelec return bearer->getBonuses(selector, noLimit.Or(limitMatches))->totalValue(); }; -TDmgRange DamageCalculator::calculateDmgRange() const +DamageEstimation DamageCalculator::calculateDmgRange() const { - TDmgRange result = getBaseDamageStack(); + DamageRange damageBase = getBaseDamageStack(); auto attackFactors = getAttackFactors(); auto defenseFactors = getDefenseFactors(); @@ -485,10 +504,16 @@ TDmgRange DamageCalculator::calculateDmgRange() const double resultingFactor = std::min(8.0, attackFactorTotal) * std::max( 0.01, defenseFactorTotal); - return { - std::max( 1.0, std::floor(result.first * resultingFactor)), - std::max( 1.0, std::floor(result.second * resultingFactor)) + info.defender->getTotalHealth(); + + DamageRange damageDealt { + std::max( 1.0, std::floor(damageBase.min * resultingFactor)), + std::max( 1.0, std::floor(damageBase.max * resultingFactor)) }; + + DamageRange killsDealt = getCasualties(damageDealt); + + return DamageEstimation{damageDealt, killsDealt}; } VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/DamageCalculator.h b/lib/battle/DamageCalculator.h index ad8111dc2..9548f950f 100644 --- a/lib/battle/DamageCalculator.h +++ b/lib/battle/DamageCalculator.h @@ -18,6 +18,8 @@ class CBattleInfoCallback; class IBonusBearer; class CSelector; struct BattleAttackInfo; +struct DamageRange; +struct DamageEstimation; class DLL_LINKAGE DamageCalculator { @@ -26,9 +28,12 @@ class DLL_LINKAGE DamageCalculator int battleBonusValue(const IBonusBearer * bearer, const CSelector & selector) const; - TDmgRange getBaseDamageSingle() const; - TDmgRange getBaseDamageBlessCurse() const; - TDmgRange getBaseDamageStack() const; + DamageRange getCasualties(const DamageRange & damageDealt) const; + int64_t getCasualties(int64_t damageDealt) const; + + DamageRange getBaseDamageSingle() const; + DamageRange getBaseDamageBlessCurse() const; + DamageRange getBaseDamageStack() const; int getActorAttackBase() const; int getActorAttackEffective() const; @@ -66,7 +71,7 @@ public: info(info) {} - TDmgRange calculateDmgRange() const; + DamageEstimation calculateDmgRange() const; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/IBattleInfoCallback.h b/lib/battle/IBattleInfoCallback.h index 38e57f0dc..8f8ab8c80 100644 --- a/lib/battle/IBattleInfoCallback.h +++ b/lib/battle/IBattleInfoCallback.h @@ -26,6 +26,18 @@ namespace battle using UnitFilter = std::function; } +struct DamageRange +{ + int64_t min = 0; + int64_t max = 0; +}; + +struct DamageEstimation +{ + DamageRange damage; + DamageRange kills; +}; + #if SCRIPTING_ENABLED namespace scripting { diff --git a/lib/battle/IBattleState.h b/lib/battle/IBattleState.h index 7165ed5b2..ab98a6d89 100644 --- a/lib/battle/IBattleState.h +++ b/lib/battle/IBattleState.h @@ -66,7 +66,7 @@ public: virtual uint32_t nextUnitId() const = 0; - virtual int64_t getActualDamage(const TDmgRange & damage, int32_t attackerCount, vstd::RNG & rng) const = 0; + virtual int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const = 0; }; class DLL_LINKAGE IBattleState : public IBattleInfo diff --git a/lib/battle/Unit.h b/lib/battle/Unit.h index 5c0c75904..424e4f628 100644 --- a/lib/battle/Unit.h +++ b/lib/battle/Unit.h @@ -70,10 +70,19 @@ public: virtual bool canShoot() const = 0; virtual bool isShooter() const = 0; + /// returns initial size of this unit virtual int32_t getCount() const = 0; + + /// returns remaining health of first unit virtual int32_t getFirstHPleft() const = 0; + + /// returns total amount of killed in this unit virtual int32_t getKilled() const = 0; + + /// returns total health that unit still has virtual int64_t getAvailableHealth() const = 0; + + /// returns total health that unit had initially virtual int64_t getTotalHealth() const = 0; virtual int getTotalAttacks(bool ranged) const = 0; diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index bb12ba10d..a41e8d1e7 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -12,7 +12,7 @@ #include "CGTownInstance.h" #include "CObjectClassesHandler.h" #include "../spells/CSpellHandler.h" - +#include "../battle/IBattleInfoCallback.h" #include "../NetPacks.h" #include "../CConfigHandler.h" #include "../CGeneralTextHandler.h" @@ -807,7 +807,7 @@ void CGTownInstance::addTownBonuses() } } -TDmgRange CGTownInstance::getTowerDamageRange() const +DamageRange CGTownInstance::getTowerDamageRange() const { assert(hasBuilt(BuildingID::CASTLE)); @@ -825,7 +825,7 @@ TDmgRange CGTownInstance::getTowerDamageRange() const }; } -TDmgRange CGTownInstance::getKeepDamageRange() const +DamageRange CGTownInstance::getKeepDamageRange() const { assert(hasBuilt(BuildingID::CITADEL)); diff --git a/lib/mapObjects/CGTownInstance.h b/lib/mapObjects/CGTownInstance.h index 47b497431..2eb63be11 100644 --- a/lib/mapObjects/CGTownInstance.h +++ b/lib/mapObjects/CGTownInstance.h @@ -20,6 +20,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CCastleEvent; class CGTownInstance; class CGDwelling; +struct DamageRange; class DLL_LINKAGE CSpecObjInfo { @@ -332,10 +333,10 @@ public: void deleteTownBonus(BuildingID::EBuildingID bid); /// Returns damage range for secondary towers of this town - TDmgRange getTowerDamageRange() const; + DamageRange getTowerDamageRange() const; /// Returns damage range for central tower(keep) of this town - TDmgRange getKeepDamageRange() const; + DamageRange getKeepDamageRange() const; const CTown * getTown() const ; diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 9557e7939..3fd426c04 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1233,7 +1233,7 @@ int64_t CGameHandler::applyBattleEffects(BattleAttack & bat, std::shared_ptrcurB->calculateDmgRange(bai); - bsa.damageAmount = gs->curB->getActualDamage(range, attackerState->getCount(), getRandomGenerator()); + bsa.damageAmount = gs->curB->getActualDamage(range.damage, attackerState->getCount(), getRandomGenerator()); CStack::prepareAttacked(bsa, getRandomGenerator(), bai.defender->acquireState()); //calculate casualties }