From 898b8f3c711090b42241e6f23174044309a7c0d2 Mon Sep 17 00:00:00 2001 From: Dydzio Date: Mon, 1 Jan 2024 19:58:32 +0100 Subject: [PATCH 01/20] Add initial version of Ferocity ability (for Ayssids) --- Mods/vcmi/config/vcmi/english.json | 2 ++ config/bonuses.json | 8 ++++++++ lib/bonuses/BonusEnum.h | 3 ++- server/battles/BattleActionProcessor.cpp | 9 +++++++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index b4537f306..e187f931b 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -379,6 +379,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": "DESCRIPTION TO BE ADDED", "core.bonus.FLYING.name": "Fly", "core.bonus.FLYING.description": "Flies when moving (ignores obstacles)", "core.bonus.FREE_SHOOTING.name": "Shoot Close", diff --git a/config/bonuses.json b/config/bonuses.json index 877161cf8..2b6aa7205 100644 --- a/config/bonuses.json +++ b/config/bonuses.json @@ -185,6 +185,14 @@ } }, + "FEROCITY": + { + "graphics": + { + "icon": "" + } + }, + "FLYING": { "graphics": diff --git a/lib/bonuses/BonusEnum.h b/lib/bonuses/BonusEnum.h index 0451cbba8..2b694a5d4 100644 --- a/lib/bonuses/BonusEnum.h +++ b/lib/bonuses/BonusEnum.h @@ -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*/ \ @@ -173,6 +173,7 @@ class JsonNode; BONUS_NAME(UNLIMITED_MOVEMENT) /*cheat bonus*/ \ BONUS_NAME(MAX_MORALE) /*cheat bonus*/ \ BONUS_NAME(MAX_LUCK) /*cheat bonus*/ \ + BONUS_NAME(FEROCITY) /*extra attack, only if at least 1 creature killed in opponent target unit*/ \ /* end of list */ diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index a55e42db3..dd82fe62e 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -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 defenderCreatureQuantity = destinationStack->getCount(); + for (int i = 0; i < totalAttacks; ++i) { //first strike @@ -282,6 +285,12 @@ 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) && destinationStack->getCount() < defenderCreatureQuantity) + { + ferocityApplied = true; + ++totalAttacks; + } } //counterattack From 7cf7543747c1be81e20a522e9ebc9e1aa6b3dd28 Mon Sep 17 00:00:00 2001 From: Dydzio Date: Mon, 1 Jan 2024 21:16:38 +0100 Subject: [PATCH 02/20] Configurable ferocity bonus --- Mods/vcmi/config/vcmi/english.json | 2 +- docs/modders/Bonus/Bonus_Types.md | 7 +++++++ lib/bonuses/BonusEnum.h | 2 +- server/battles/BattleActionProcessor.cpp | 14 ++++++++++---- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index e187f931b..8adc06222 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -380,7 +380,7 @@ "core.bonus.FEARLESS.name": "Fearless", "core.bonus.FEARLESS.description": "Immune to Fear ability", "core.bonus.FEROCITY.name": "Ferocity", - "core.bonus.FEROCITY.description": "DESCRIPTION TO BE ADDED", + "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", diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md index 6836066cc..ca008ae37 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -608,6 +608,13 @@ 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 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) + ## Special abilities ### CATAPULT diff --git a/lib/bonuses/BonusEnum.h b/lib/bonuses/BonusEnum.h index 2b694a5d4..6a8c34fae 100644 --- a/lib/bonuses/BonusEnum.h +++ b/lib/bonuses/BonusEnum.h @@ -173,7 +173,7 @@ class JsonNode; BONUS_NAME(UNLIMITED_MOVEMENT) /*cheat bonus*/ \ BONUS_NAME(MAX_MORALE) /*cheat bonus*/ \ BONUS_NAME(MAX_LUCK) /*cheat bonus*/ \ - BONUS_NAME(FEROCITY) /*extra attack, only if at least 1 creature killed in opponent target unit*/ \ + BONUS_NAME(FEROCITY) /*extra attacks, only if at least some creatures killed in opponent target unit, val = amount of additional attacks, additional info = amount of creatures killed to trigger (default 1)*/ \ /* end of list */ diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index dd82fe62e..eeea4ec19 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -271,7 +271,7 @@ 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 defenderCreatureQuantity = destinationStack->getCount(); + int32_t defenderInitialQuantity = destinationStack->getCount(); for (int i = 0; i < totalAttacks; ++i) { @@ -286,10 +286,16 @@ bool BattleActionProcessor::doAttackAction(const CBattleInfoCallback & battle, c { makeAttack(battle, stack, destinationStack, (i ? 0 : distance), destinationTile, i==0, false, false);//no distance travelled on second attack - if(!ferocityApplied && stack->hasBonusOfType(BonusType::FEROCITY) && destinationStack->getCount() < defenderCreatureQuantity) + if(!ferocityApplied && stack->hasBonusOfType(BonusType::FEROCITY)) { - ferocityApplied = true; - ++totalAttacks; + 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; + } } } From fbd988df428cb2876552b283cfa0b0b5ce3ee1e9 Mon Sep 17 00:00:00 2001 From: Dydzio Date: Mon, 1 Jan 2024 21:19:25 +0100 Subject: [PATCH 03/20] Bit better documentation wording --- docs/modders/Bonus/Bonus_Types.md | 2 +- lib/bonuses/BonusEnum.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md index ca008ae37..cf5ce2642 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -610,7 +610,7 @@ Affected unit can use ranged attacks only within specified range ### FEROCITY -Affected unit will attack additional times if killed creatures during attacking (including ADDITIONAL_ATTACK bonus attacks) +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) diff --git a/lib/bonuses/BonusEnum.h b/lib/bonuses/BonusEnum.h index 6a8c34fae..d12c4a796 100644 --- a/lib/bonuses/BonusEnum.h +++ b/lib/bonuses/BonusEnum.h @@ -173,7 +173,7 @@ 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 in opponent target unit, val = amount of additional attacks, additional info = amount of creatures killed to trigger (default 1)*/ \ + 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)*/ \ /* end of list */ From 7283a4861e7186777ee7a07f7c814fd6f921ec24 Mon Sep 17 00:00:00 2001 From: Dydzio Date: Thu, 4 Jan 2024 22:27:51 +0100 Subject: [PATCH 04/20] Initial version of ACCURATE_SHOT implementation --- Mods/vcmi/config/vcmi/english.json | 6 ++- config/bonuses.json | 7 ++++ docs/modders/Bonus/Bonus_Types.md | 9 +++++ lib/JsonNode.cpp | 1 + lib/bonuses/BonusEnum.h | 1 + lib/spells/effects/Damage.cpp | 16 ++++++++ server/battles/BattleActionProcessor.cpp | 49 +++++++++++++++++++++++- 7 files changed, 87 insertions(+), 2 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index 8adc06222..88b89ed00 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -184,6 +184,8 @@ "vcmi.battleWindow.damageEstimation.kills" : "%d will perish", "vcmi.battleWindow.damageEstimation.kills.1" : "%d will perish", "vcmi.battleWindow.killed" : "Killed", + "vcmi.battleWindow.accurateShot.resultDescription" : "%d %s were killed by accurate shots!", + "vcmi.battleWindow.accurateShot.resultDescription.1" : "1 %s was killed with an accurate shot!", "vcmi.battleResultsWindow.applyResultsLabel" : "Apply battle result", @@ -328,7 +330,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", diff --git a/config/bonuses.json b/config/bonuses.json index 2b6aa7205..e90f40bec 100644 --- a/config/bonuses.json +++ b/config/bonuses.json @@ -3,6 +3,13 @@ // LEVEL_SPELL_IMMUNITY { + "ACCURATE_SHOT": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_DIST" + } + }, "ADDITIONAL_ATTACK": { "graphics": diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md index cf5ce2642..43372cfe0 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -732,6 +732,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 diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index 593cbdf19..eb3ec93b4 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -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) diff --git a/lib/bonuses/BonusEnum.h b/lib/bonuses/BonusEnum.h index d12c4a796..a37b5463f 100644 --- a/lib/bonuses/BonusEnum.h +++ b/lib/bonuses/BonusEnum.h @@ -174,6 +174,7 @@ class JsonNode; 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*/ \ /* end of list */ diff --git a/lib/spells/effects/Damage.cpp b/lib/spells/effects/Damage.cpp index 6a28a5349..2e02d0661 100644 --- a/lib/spells/effects/Damage.cpp +++ b/lib/spells/effects/Damage.cpp @@ -152,6 +152,22 @@ void Damage::describeEffect(std::vector & 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; + if(kills > 1) + { + line.appendTextID("vcmi.battleWindow.accurateShot.resultDescription"); //(number) (unit type) was killed with an accurate shot! + line.replaceNumber(kills); + firstTarget->addNameReplacement(line, true); + } + else + { + line.appendTextID("vcmi.battleWindow.accurateShot.resultDescription.1"); //1 (unit type) were killed by accurate shots! + firstTarget->addNameReplacement(line, false); + } + log.push_back(line); + } else if(m->getSpellIndex() == SpellID::THUNDERBOLT && !multiple) { { diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index eeea4ec19..396ecdaa9 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -1214,7 +1214,7 @@ void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & // 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; + double chanceToKill = attacker->valOfBonuses(BonusType::DEATH_STARE, BonusCustomSubtype::deathStareGorgon) / 100.0; vstd::amin(chanceToKill, 1); //cap at 100% std::binomial_distribution<> distribution(attacker->getCount(), chanceToKill); @@ -1241,6 +1241,53 @@ void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & } } + if(attacker->hasBonusOfType(BonusType::ACCURATE_SHOT)) + { + /* Intended to match HotA Sea Dogs + * The Sea Dog's Accurate Shot is triggered after a shot: + * 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(!ranged || battle.battleHasWallPenalty(attacker, attacker->getPosition(), defender->getPosition())) + return; + + int singleCreatureKillChancePercent = attacker->valOfBonuses(BonusType::ACCURATE_SHOT); + + if(battle.battleHasDistancePenalty(attacker, attacker->getPosition(), defender->getPosition())) + singleCreatureKillChancePercent = (singleCreatureKillChancePercent * 2) / 3; + + 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()); + bool isMaxToKillRounded = attacker->getCount() * singleCreatureKillChancePercent % 100 == 0; + int maxToKill = attacker->getCount() * singleCreatureKillChancePercent / 100 + (isMaxToKillRounded ? 0 : 1); + vstd::amin(killedCreatures, maxToKill); + + if(killedCreatures) + { + //TODO: accurate shot was not originally available for multiple-hex attacks, but... + const auto bonus = attacker->getBonus(Selector::type()(BonusType::ACCURATE_SHOT)); + + auto spellID = bonus->subtype.as(); + if(spellID == SpellID::NONE) + spellID = SpellID(SpellID::DEATH_STARE); //fallback for spell not specified + + 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); + } + } + if(!defender->alive()) return; From b32c7beb0563740bb09f9758efdcfd9fcd450a88 Mon Sep 17 00:00:00 2001 From: Dydzio Date: Sat, 6 Jan 2024 18:26:13 +0100 Subject: [PATCH 05/20] Make one common handler for death stare and accurate shot --- server/battles/BattleActionProcessor.cpp | 82 ++++++++++-------------- 1 file changed, 35 insertions(+), 47 deletions(-) diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index 396ecdaa9..81121de02 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -1208,76 +1208,64 @@ 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.0; - 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((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); - } - } - - if(attacker->hasBonusOfType(BonusType::ACCURATE_SHOT)) - { - /* Intended to match HotA Sea Dogs - * The Sea Dog's Accurate Shot is triggered after a shot: + /* 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(!ranged || battle.battleHasWallPenalty(attacker, attacker->getPosition(), defender->getPosition())) - return; + auto bonus = attacker->getBonus(Selector::type()(BonusType::DEATH_STARE)); + if(bonus == nullptr) + bonus = attacker->getBonus(Selector::type()(BonusType::ACCURATE_SHOT)); - int singleCreatureKillChancePercent = attacker->valOfBonuses(BonusType::ACCURATE_SHOT); + if(bonus->type == BonusType::ACCURATE_SHOT && (!ranged || battle.battleHasWallPenalty(attacker, attacker->getPosition(), defender->getPosition()))) + return; //should not work from behind walls, except being defender or under effect of golden bow etc. - if(battle.battleHasDistancePenalty(attacker, attacker->getPosition(), defender->getPosition())) - singleCreatureKillChancePercent = (singleCreatureKillChancePercent * 2) / 3; + 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()); - bool isMaxToKillRounded = attacker->getCount() * singleCreatureKillChancePercent % 100 == 0; - int maxToKill = attacker->getCount() * singleCreatureKillChancePercent / 100 + (isMaxToKillRounded ? 0 : 1); - vstd::amin(killedCreatures, maxToKill); + + if(bonus->type == BonusType::DEATH_STARE) + { + double cap = 1 / std::max(chanceToKill, (double)(0.01));//don't divide by 0 + int maxToKill = static_cast((attacker->getCount() + cap - 1) / cap); //not much more than chance * count + vstd::amin(killedCreatures, maxToKill); + + killedCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, BonusCustomSubtype::deathStareCommander)) / defender->level(); + } + else //ACCURATE_SHOT + { + bool isMaxToKillRounded = attacker->getCount() * singleCreatureKillChancePercent % 100 == 0; + int maxToKill = attacker->getCount() * singleCreatureKillChancePercent / 100 + (isMaxToKillRounded ? 0 : 1); + vstd::amin(killedCreatures, maxToKill); + } if(killedCreatures) { - //TODO: accurate shot was not originally available for multiple-hex attacks, but... - const auto bonus = attacker->getBonus(Selector::type()(BonusType::ACCURATE_SHOT)); + //TODO: death stare or accurate shot was not originally available for multiple-hex attacks, but... - auto spellID = bonus->subtype.as(); - if(spellID == SpellID::NONE) - spellID = SpellID(SpellID::DEATH_STARE); //fallback for spell not specified + SpellID spellID = SpellID(SpellID::DEATH_STARE); //also used as fallback spell for ACCURATE_SHOT + if(bonus->type == BonusType::ACCURATE_SHOT && bonus->subtype.as() != SpellID::NONE) + spellID = bonus->subtype.as(); const CSpell * spell = spellID.toSpell(); - spells::AbilityCaster caster(attacker, 0); spells::BattleCast parameters(&battle, &caster, spells::Mode::PASSIVE, spell); From bb925e4cb0797f7646cda3bbd4716b7fee37049c Mon Sep 17 00:00:00 2001 From: Dydzio Date: Sun, 7 Jan 2024 19:20:32 +0100 Subject: [PATCH 06/20] First version of sea witch / sorceress ability --- client/battle/BattleInterface.cpp | 2 +- lib/bonuses/BonusEnum.h | 2 +- server/battles/BattleActionProcessor.cpp | 74 ++++++++++++++++++++++-- server/battles/BattleActionProcessor.h | 2 +- 4 files changed, 71 insertions(+), 9 deletions(-) diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index 99920ab2a..16daa5e69 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -791,7 +791,7 @@ void BattleInterface::waitForAnimations() } assert(!hasAnimations()); - assert(awaitingEvents.empty()); + //assert(awaitingEvents.empty()); if (!awaitingEvents.empty()) { diff --git a/lib/bonuses/BonusEnum.h b/lib/bonuses/BonusEnum.h index a37b5463f..0839cfb4b 100644 --- a/lib/bonuses/BonusEnum.h +++ b/lib/bonuses/BonusEnum.h @@ -57,7 +57,7 @@ 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_AFTER_ATTACK) /* subtype - spell id, value - chance %, addInfo[0] - level, addInfo[1] -> [0 - all attacks, 1 - shot only, 2 - melee only], addInfo[2] -> backup spell layer (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] */ \ 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 */ \ diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index 81121de02..25a976d9d 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -1119,19 +1119,81 @@ 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 spellsToCast; + TConstBonusListPtr spells = attacker->getBonuses(Selector::type()(attackMode)); - for(const auto & sf : *spells) + + std::array>, 6> spellsWithBackupLayers = { - if (sf->subtype.as() != SpellID()) - spellsToCast.insert(sf->subtype.as()); - else - logMod->error("Invalid spell to cast during attack!"); + { + std::vector>(), + std::vector>(), + std::vector>(), + std::vector>(), + std::vector>(), + std::vector>() + } + }; + + int lastBackupLayer = -1; + for(int i = 0; i < spells->size(); i++) + { + std::shared_ptr bonus = spells->operator[](i); + int layer = bonus->additionalInfo[2]; + vstd::abetween(layer, -1, 4); + spellsWithBackupLayers[layer+1].push_back(bonus); + + if(layer > lastBackupLayer) + lastBackupLayer = layer; } + + auto addSpellsFromLayer = [&](int layer) -> void + { + if(layer + 1 >= spellsWithBackupLayers.size() ) + { + logMod->error(std::string("Wrong layer in addSpellsFromLayer: ") + std::to_string(layer)); + return; + } + + for(const auto & spell : spellsWithBackupLayers[layer+1]) + { + if (spell->subtype.as() != SpellID()) + spellsToCast.insert(spell->subtype.as()); + else + logMod->error("Invalid spell to cast during attack!"); + } + }; + + addSpellsFromLayer(-1); + + for(int spellLayer = 0; spellLayer <= lastBackupLayer; spellLayer++) + { + if(spellsWithBackupLayers[spellLayer+1].empty()) + continue; + + if(spellLayer < lastBackupLayer) + { + bool areCurrentLayerSpellsApplied = std::all_of(spellsWithBackupLayers[spellLayer+1].begin(), spellsWithBackupLayers[spellLayer+1].end(), + [&](const std::shared_ptr spell) + { + std::vector activeSpells = defender->activeSpells(); + auto spellIterator = vstd::find(activeSpells, spell->subtype.as()); + bool value = spellIterator != activeSpells.end(); + return value; + }); + + if(areCurrentLayerSpellsApplied) + continue; + } + + addSpellsFromLayer(spellLayer); + break; + } + for(SpellID spellID : spellsToCast) { bool castMe = false; diff --git a/server/battles/BattleActionProcessor.h b/server/battles/BattleActionProcessor.h index 34e40aa29..01cf5d307 100644 --- a/server/battles/BattleActionProcessor.h +++ b/server/battles/BattleActionProcessor.h @@ -44,7 +44,7 @@ class BattleActionProcessor : boost::noncopyable void handleAttackBeforeCasting(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); // damage, drain life & fire shield; returns amount of drained life int64_t applyBattleEffects(const CBattleInfoCallback & battle, BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary); From 310802ed87b641c1884f11b680c3c7fa487bc21a Mon Sep 17 00:00:00 2001 From: Dydzio Date: Sun, 7 Jan 2024 20:48:03 +0100 Subject: [PATCH 07/20] Second version of spell layers bonus extension --- server/battles/BattleActionProcessor.cpp | 52 ++++++++---------------- 1 file changed, 17 insertions(+), 35 deletions(-) diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index 25a976d9d..e225d95fc 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -1125,72 +1125,54 @@ void BattleActionProcessor::attackCasting(const CBattleInfoCallback & battle, bo { std::set spellsToCast; + const int unlayeredItemsInternalLayer = -1; TConstBonusListPtr spells = attacker->getBonuses(Selector::type()(attackMode)); - std::array>, 6> spellsWithBackupLayers = - { - { - std::vector>(), - std::vector>(), - std::vector>(), - std::vector>(), - std::vector>(), - std::vector>() - } - }; + std::map>> spellsWithBackupLayers; - int lastBackupLayer = -1; for(int i = 0; i < spells->size(); i++) { std::shared_ptr bonus = spells->operator[](i); int layer = bonus->additionalInfo[2]; - vstd::abetween(layer, -1, 4); - spellsWithBackupLayers[layer+1].push_back(bonus); - - if(layer > lastBackupLayer) - lastBackupLayer = layer; + vstd::amax(layer, -1); + spellsWithBackupLayers[layer].push_back(bonus); } auto addSpellsFromLayer = [&](int layer) -> void { - if(layer + 1 >= spellsWithBackupLayers.size() ) - { - logMod->error(std::string("Wrong layer in addSpellsFromLayer: ") + std::to_string(layer)); - return; - } + assert(spellsWithBackupLayers.find(layer) != spellsWithBackupLayers.end()); - for(const auto & spell : spellsWithBackupLayers[layer+1]) + for(const auto & spell : spellsWithBackupLayers[layer]) { if (spell->subtype.as() != SpellID()) spellsToCast.insert(spell->subtype.as()); else - logMod->error("Invalid spell to cast during attack!"); + logGlobal->error("Invalid spell to cast during attack!"); } }; - addSpellsFromLayer(-1); - - for(int spellLayer = 0; spellLayer <= lastBackupLayer; spellLayer++) + if(spellsWithBackupLayers.find(unlayeredItemsInternalLayer) != spellsWithBackupLayers.end()) { - if(spellsWithBackupLayers[spellLayer+1].empty()) - continue; + addSpellsFromLayer(unlayeredItemsInternalLayer); + spellsWithBackupLayers.erase(unlayeredItemsInternalLayer); + } - if(spellLayer < lastBackupLayer) + for(auto item : spellsWithBackupLayers) + { + if(item.first < spellsWithBackupLayers.rbegin()->first) { - bool areCurrentLayerSpellsApplied = std::all_of(spellsWithBackupLayers[spellLayer+1].begin(), spellsWithBackupLayers[spellLayer+1].end(), + bool areCurrentLayerSpellsApplied = std::all_of(item.second.begin(), item.second.end(), [&](const std::shared_ptr spell) { std::vector activeSpells = defender->activeSpells(); - auto spellIterator = vstd::find(activeSpells, spell->subtype.as()); - bool value = spellIterator != activeSpells.end(); - return value; + return vstd::find(activeSpells, spell->subtype.as()) != activeSpells.end(); }); if(areCurrentLayerSpellsApplied) continue; } - addSpellsFromLayer(spellLayer); + addSpellsFromLayer(item.first); break; } From 5dac8e2bbf21362068ca4fecdd0bbd7da4dd497f Mon Sep 17 00:00:00 2001 From: Dydzio Date: Sun, 7 Jan 2024 21:05:55 +0100 Subject: [PATCH 08/20] Extract spell layers processing into separate method --- server/battles/BattleActionProcessor.cpp | 108 ++++++++++++----------- server/battles/BattleActionProcessor.h | 3 + 2 files changed, 60 insertions(+), 51 deletions(-) diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index e225d95fc..6d268fd6f 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -1123,58 +1123,8 @@ void BattleActionProcessor::attackCasting(const CBattleInfoCallback & battle, bo { if(attacker->hasBonusOfType(attackMode)) { - std::set spellsToCast; - - const int unlayeredItemsInternalLayer = -1; TConstBonusListPtr spells = attacker->getBonuses(Selector::type()(attackMode)); - - std::map>> spellsWithBackupLayers; - - for(int i = 0; i < spells->size(); i++) - { - std::shared_ptr 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()) - spellsToCast.insert(spell->subtype.as()); - else - logGlobal->error("Invalid spell to cast during attack!"); - } - }; - - if(spellsWithBackupLayers.find(unlayeredItemsInternalLayer) != spellsWithBackupLayers.end()) - { - addSpellsFromLayer(unlayeredItemsInternalLayer); - spellsWithBackupLayers.erase(unlayeredItemsInternalLayer); - } - - for(auto item : spellsWithBackupLayers) - { - if(item.first < spellsWithBackupLayers.rbegin()->first) - { - bool areCurrentLayerSpellsApplied = std::all_of(item.second.begin(), item.second.end(), - [&](const std::shared_ptr spell) - { - std::vector activeSpells = defender->activeSpells(); - return vstd::find(activeSpells, spell->subtype.as()) != activeSpells.end(); - }); - - if(areCurrentLayerSpellsApplied) - continue; - } - - addSpellsFromLayer(item.first); - break; - } + std::set spellsToCast = getSpellsForAttackCasting(spells, defender); for(SpellID spellID : spellsToCast) { @@ -1234,6 +1184,62 @@ void BattleActionProcessor::attackCasting(const CBattleInfoCallback & battle, bo } } +std::set BattleActionProcessor::getSpellsForAttackCasting(TConstBonusListPtr spells, const CStack *defender) +{ + std::set spellsToCast; + constexpr int unlayeredItemsInternalLayer = -1; + + std::map>> spellsWithBackupLayers; + + for(int i = 0; i < spells->size(); i++) + { + std::shared_ptr 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()) + spellsToCast.insert(spell->subtype.as()); + else + logGlobal->error("Invalid spell to cast during attack!"); + } + }; + + if(spellsWithBackupLayers.find(unlayeredItemsInternalLayer) != spellsWithBackupLayers.end()) + { + addSpellsFromLayer(unlayeredItemsInternalLayer); + spellsWithBackupLayers.erase(unlayeredItemsInternalLayer); + } + + for(auto item : spellsWithBackupLayers) + { + if(item.first < spellsWithBackupLayers.rbegin()->first) + { + bool areCurrentLayerSpellsApplied = std::all_of(item.second.begin(), item.second.end(), + [&](const std::shared_ptr spell) + { + std::vector activeSpells = defender->activeSpells(); + return vstd::find(activeSpells, spell->subtype.as()) != activeSpells.end(); + }); + + if(areCurrentLayerSpellsApplied) + continue; + } + + 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? diff --git a/server/battles/BattleActionProcessor.h b/server/battles/BattleActionProcessor.h index 01cf5d307..85585b7e7 100644 --- a/server/battles/BattleActionProcessor.h +++ b/server/battles/BattleActionProcessor.h @@ -8,6 +8,7 @@ * */ #pragma once +#include "bonuses/BonusList.h" VCMI_LIB_NAMESPACE_BEGIN @@ -45,6 +46,8 @@ class BattleActionProcessor : boost::noncopyable void handleAttackBeforeCasting(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 CStack * defender); + + std::set 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 attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary); From 3c95f92c59aae6949e10b00603effce238a890ef Mon Sep 17 00:00:00 2001 From: Dydzio Date: Sun, 7 Jan 2024 21:22:10 +0100 Subject: [PATCH 09/20] Update documentation --- docs/modders/Bonus/Bonus_Types.md | 8 ++++++-- lib/bonuses/BonusEnum.h | 4 ++-- server/battles/BattleActionProcessor.h | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md index 43372cfe0..7648bcd1a 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -775,17 +775,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 diff --git a/lib/bonuses/BonusEnum.h b/lib/bonuses/BonusEnum.h index 0839cfb4b..c5f9ad74c 100644 --- a/lib/bonuses/BonusEnum.h +++ b/lib/bonuses/BonusEnum.h @@ -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], addInfo[2] -> backup spell layer (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] */ \ + 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 */ \ diff --git a/server/battles/BattleActionProcessor.h b/server/battles/BattleActionProcessor.h index 85585b7e7..39d41eccd 100644 --- a/server/battles/BattleActionProcessor.h +++ b/server/battles/BattleActionProcessor.h @@ -46,7 +46,7 @@ class BattleActionProcessor : boost::noncopyable void handleAttackBeforeCasting(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 CStack * defender); - + std::set getSpellsForAttackCasting(TConstBonusListPtr spells, const CStack *defender); // damage, drain life & fire shield; returns amount of drained life From d309a0002554e94e2e431a2c89c69815e52ba246 Mon Sep 17 00:00:00 2001 From: Dydzio Date: Sun, 7 Jan 2024 21:45:39 +0100 Subject: [PATCH 10/20] Extra fix: fix amount position in battle creature labels --- client/battle/BattleStacksController.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index 8e629d143..70532320f 100644 --- a/client/battle/BattleStacksController.cpp +++ b/client/battle/BattleStacksController.cpp @@ -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)); From 675f9b11faf3dec0624cc1c1a5951e4b399f2a52 Mon Sep 17 00:00:00 2001 From: Dydzio Date: Mon, 8 Jan 2024 19:37:04 +0100 Subject: [PATCH 11/20] Add ENEMY_ATTACK_REDUCTION bonus - fixes HotA Nix --- Mods/vcmi/config/vcmi/english.json | 2 ++ config/bonuses.json | 8 ++++++++ docs/modders/Bonus/Bonus_Types.md | 6 ++++++ lib/battle/DamageCalculator.cpp | 14 +++++++++++++- lib/battle/DamageCalculator.h | 1 + lib/bonuses/BonusEnum.h | 1 + 6 files changed, 31 insertions(+), 1 deletion(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index 88b89ed00..97244f82c 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -371,6 +371,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", diff --git a/config/bonuses.json b/config/bonuses.json index e90f40bec..b9a410848 100644 --- a/config/bonuses.json +++ b/config/bonuses.json @@ -152,6 +152,14 @@ } }, + "ENEMY_ATTACK_REDUCTION": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_RDEF" + } + }, + "ENEMY_DEFENCE_REDUCTION": { "graphics": diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md index 7648bcd1a..b92c101cf 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -615,6 +615,12 @@ Affected unit will attack additional times if killed creatures in target unit du - 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 + ## Special abilities ### CATAPULT diff --git a/lib/battle/DamageCalculator.cpp b/lib/battle/DamageCalculator.cpp index d735222f4..b3cb953bc 100644 --- a/lib/battle/DamageCalculator.cpp +++ b/lib/battle/DamageCalculator.cpp @@ -124,7 +124,19 @@ int DamageCalculator::getActorAttackBase() const int DamageCalculator::getActorAttackEffective() const { - return getActorAttackBase() + getActorAttackSlayer(); + return getActorAttackBase() + getActorAttackSlayer() + getActorAttackIgnored(); +} + +int DamageCalculator::getActorAttackIgnored() const +{ + double multAttackReduction = battleBonusValue(info.defender, Selector::type()(BonusType::ENEMY_ATTACK_REDUCTION)) / 100.0; + + if(multAttackReduction > 0) + { + int reduction = std::round(multAttackReduction * getActorAttackBase()); + return -std::min(reduction,getActorAttackBase()); + } + return 0; } int DamageCalculator::getActorAttackSlayer() const diff --git a/lib/battle/DamageCalculator.h b/lib/battle/DamageCalculator.h index 9548f950f..a91c006fd 100644 --- a/lib/battle/DamageCalculator.h +++ b/lib/battle/DamageCalculator.h @@ -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; diff --git a/lib/bonuses/BonusEnum.h b/lib/bonuses/BonusEnum.h index c5f9ad74c..e1304c54f 100644 --- a/lib/bonuses/BonusEnum.h +++ b/lib/bonuses/BonusEnum.h @@ -175,6 +175,7 @@ class JsonNode; 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)*/ \ /* end of list */ From dbba1164ef9e1bfae3fc2f25293e91b3db48480a Mon Sep 17 00:00:00 2001 From: Dydzio Date: Mon, 8 Jan 2024 20:50:37 +0100 Subject: [PATCH 12/20] Fix floating point rounding for 5 attack points and unupgraded nix --- lib/battle/DamageCalculator.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/battle/DamageCalculator.cpp b/lib/battle/DamageCalculator.cpp index b3cb953bc..e00318206 100644 --- a/lib/battle/DamageCalculator.cpp +++ b/lib/battle/DamageCalculator.cpp @@ -9,6 +9,9 @@ */ #include "StdInc.h" + +#include + #include "DamageCalculator.h" #include "CBattleInfoCallback.h" #include "Unit.h" @@ -133,7 +136,10 @@ int DamageCalculator::getActorAttackIgnored() const if(multAttackReduction > 0) { - int reduction = std::round(multAttackReduction * getActorAttackBase()); + int defaultRoundingMode = std::fegetround(); + std::fesetround(FE_TOWARDZERO); + int reduction = std::nearbyint(multAttackReduction * getActorAttackBase()); + std::fesetround(defaultRoundingMode); return -std::min(reduction,getActorAttackBase()); } return 0; From 67f18729fa863b91d40590320077865db66c986c Mon Sep 17 00:00:00 2001 From: M Date: Tue, 9 Jan 2024 19:10:43 +0100 Subject: [PATCH 13/20] REVENGE bonus that matches HotA haspid ability --- Mods/vcmi/config/vcmi/english.json | 2 ++ config/bonuses.json | 8 ++++++++ docs/modders/Bonus/Bonus_Types.md | 6 ++++++ lib/battle/DamageCalculator.cpp | 18 +++++++++++++++--- lib/bonuses/BonusEnum.h | 1 + 5 files changed, 32 insertions(+), 3 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index 97244f82c..d6ea0beca 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -441,6 +441,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", diff --git a/config/bonuses.json b/config/bonuses.json index b9a410848..8a3ecca59 100644 --- a/config/bonuses.json +++ b/config/bonuses.json @@ -451,6 +451,14 @@ } }, + "REVENGE": + { + "graphics": + { + "icon": "" + } + }, + "SHOOTER": { "graphics": diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md index b92c101cf..6ac0cc942 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -621,6 +621,12 @@ 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 diff --git a/lib/battle/DamageCalculator.cpp b/lib/battle/DamageCalculator.cpp index e00318206..a8b355e24 100644 --- a/lib/battle/DamageCalculator.cpp +++ b/lib/battle/DamageCalculator.cpp @@ -526,11 +526,23 @@ DamageEstimation DamageCalculator::calculateDmgRange() const double resultingFactor = std::min(8.0, attackFactorTotal) * std::max( 0.01, defenseFactorTotal); - info.defender->getTotalHealth(); + double revengeAdditionalMinDamage = 0.0; + double revengeAdditionalMaxDamage = 0.0; + if(info.attacker->hasBonusOfType(BonusType::REVENGE)) //HotA Haspid ability + { + int totalStackCount = info.attacker->unitBaseAmount(); + int currentStackHealth = info.attacker->getAvailableHealth(); + int creatureHealth = info.attacker->getMaxHealth(); + + double revengeFactor = sqrt(static_cast((totalStackCount + 1) * creatureHealth) / (currentStackHealth + creatureHealth) - 1); + + revengeAdditionalMinDamage = std::round(damageBase.min * revengeFactor); + revengeAdditionalMaxDamage = std::round(damageBase.max * revengeFactor); + } DamageRange damageDealt { - std::max( 1.0, std::floor(damageBase.min * resultingFactor)), - std::max( 1.0, std::floor(damageBase.max * resultingFactor)) + std::max( 1.0, std::floor(damageBase.min * resultingFactor + revengeAdditionalMinDamage)), + std::max( 1.0, std::floor(damageBase.max * resultingFactor + revengeAdditionalMaxDamage)) }; DamageRange killsDealt = getCasualties(damageDealt); diff --git a/lib/bonuses/BonusEnum.h b/lib/bonuses/BonusEnum.h index e1304c54f..92068d7be 100644 --- a/lib/bonuses/BonusEnum.h +++ b/lib/bonuses/BonusEnum.h @@ -176,6 +176,7 @@ class JsonNode; 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 */ From 56165818b4a76949109cce3c2cdd82d242de7b24 Mon Sep 17 00:00:00 2001 From: M Date: Tue, 9 Jan 2024 20:06:11 +0100 Subject: [PATCH 14/20] Fix wrong english word --- docs/modders/Bonus/Bonus_Types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md index 6ac0cc942..c3262d469 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -810,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 From 815fa26fb34ec8d4c35c6126a9351f077e5119a8 Mon Sep 17 00:00:00 2001 From: M Date: Wed, 10 Jan 2024 22:18:53 +0100 Subject: [PATCH 15/20] Change nix rounding, revert assert comment --- client/battle/BattleInterface.cpp | 2 +- lib/battle/DamageCalculator.cpp | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index 16daa5e69..99920ab2a 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -791,7 +791,7 @@ void BattleInterface::waitForAnimations() } assert(!hasAnimations()); - //assert(awaitingEvents.empty()); + assert(awaitingEvents.empty()); if (!awaitingEvents.empty()) { diff --git a/lib/battle/DamageCalculator.cpp b/lib/battle/DamageCalculator.cpp index a8b355e24..2f5e65426 100644 --- a/lib/battle/DamageCalculator.cpp +++ b/lib/battle/DamageCalculator.cpp @@ -132,15 +132,12 @@ int DamageCalculator::getActorAttackEffective() const int DamageCalculator::getActorAttackIgnored() const { - double multAttackReduction = battleBonusValue(info.defender, Selector::type()(BonusType::ENEMY_ATTACK_REDUCTION)) / 100.0; + int multAttackReductionPercent = battleBonusValue(info.defender, Selector::type()(BonusType::ENEMY_ATTACK_REDUCTION)); - if(multAttackReduction > 0) + if(multAttackReductionPercent > 0) { - int defaultRoundingMode = std::fegetround(); - std::fesetround(FE_TOWARDZERO); - int reduction = std::nearbyint(multAttackReduction * getActorAttackBase()); - std::fesetround(defaultRoundingMode); - return -std::min(reduction,getActorAttackBase()); + 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; } From 7bf273e01ca45ac5c2afcf6a59fb9730472a73c9 Mon Sep 17 00:00:00 2001 From: M Date: Wed, 10 Jan 2024 22:56:26 +0100 Subject: [PATCH 16/20] Extract revenge calculation to separate method --- lib/battle/DamageCalculator.cpp | 31 ++++++++++++++++++------------- lib/battle/DamageCalculator.h | 1 + 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/lib/battle/DamageCalculator.cpp b/lib/battle/DamageCalculator.cpp index 2f5e65426..31fe70d5c 100644 --- a/lib/battle/DamageCalculator.cpp +++ b/lib/battle/DamageCalculator.cpp @@ -281,6 +281,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((totalStackCount + 1) * creatureHealth) / (currentStackHealth + creatureHealth) - 1); + } + + return 0.0; +} + double DamageCalculator::getDefenseSkillFactor() const { int defenseAdvantage = getTargetDefenseEffective() - getActorAttackEffective(); @@ -523,19 +537,10 @@ DamageEstimation DamageCalculator::calculateDmgRange() const double resultingFactor = std::min(8.0, attackFactorTotal) * std::max( 0.01, defenseFactorTotal); - double revengeAdditionalMinDamage = 0.0; - double revengeAdditionalMaxDamage = 0.0; - if(info.attacker->hasBonusOfType(BonusType::REVENGE)) //HotA Haspid ability - { - int totalStackCount = info.attacker->unitBaseAmount(); - int currentStackHealth = info.attacker->getAvailableHealth(); - int creatureHealth = info.attacker->getMaxHealth(); - - double revengeFactor = sqrt(static_cast((totalStackCount + 1) * creatureHealth) / (currentStackHealth + creatureHealth) - 1); - - revengeAdditionalMinDamage = std::round(damageBase.min * revengeFactor); - revengeAdditionalMaxDamage = std::round(damageBase.max * revengeFactor); - } + //calculated separately since it bypasses cap on bonus damage + double revengeFactor = getAttackRevengeFactor(); + double revengeAdditionalMinDamage = std::round(damageBase.min * revengeFactor); + double revengeAdditionalMaxDamage = std::round(damageBase.max * revengeFactor); DamageRange damageDealt { std::max( 1.0, std::floor(damageBase.min * resultingFactor + revengeAdditionalMinDamage)), diff --git a/lib/battle/DamageCalculator.h b/lib/battle/DamageCalculator.h index a91c006fd..d7f91e0b5 100644 --- a/lib/battle/DamageCalculator.h +++ b/lib/battle/DamageCalculator.h @@ -51,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; From bf7c6a4c3f755258d433f5dcb1a66af68ff7fd56 Mon Sep 17 00:00:00 2001 From: M Date: Wed, 10 Jan 2024 23:23:10 +0100 Subject: [PATCH 17/20] Extract method for death stare / accurate shot, fix translation key --- Mods/vcmi/config/vcmi/english.json | 3 +- server/battles/BattleActionProcessor.cpp | 134 ++++++++++++----------- server/battles/BattleActionProcessor.h | 3 + 3 files changed, 75 insertions(+), 65 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index d6ea0beca..e8edfa889 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -184,8 +184,9 @@ "vcmi.battleWindow.damageEstimation.kills" : "%d will perish", "vcmi.battleWindow.damageEstimation.kills.1" : "%d will perish", "vcmi.battleWindow.killed" : "Killed", - "vcmi.battleWindow.accurateShot.resultDescription" : "%d %s were killed by accurate shots!", + "vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s were killed by accurate shots!", "vcmi.battleWindow.accurateShot.resultDescription.1" : "1 %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", diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index 6d268fd6f..11ecaa58e 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -1245,6 +1245,75 @@ void BattleActionProcessor::handleAttackBeforeCasting(const CBattleInfoCallback 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 && (!ranged || battle.battleHasWallPenalty(attacker, attacker->getPosition(), defender->getPosition()))) + return; //should not work from behind walls, except being defender or under effect of golden bow etc. + + + 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()); + + if(bonus->type == BonusType::DEATH_STARE) + { + double cap = 1 / std::max(chanceToKill, (double)(0.01));//don't divide by 0 + int maxToKill = static_cast((attacker->getCount() + cap - 1) / cap); //not much more than chance * count + vstd::amin(killedCreatures, maxToKill); + + killedCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, BonusCustomSubtype::deathStareCommander)) / defender->level(); + } + else //ACCURATE_SHOT + { + bool isMaxToKillRounded = attacker->getCount() * singleCreatureKillChancePercent % 100 == 0; + int maxToKill = attacker->getCount() * singleCreatureKillChancePercent / 100 + (isMaxToKillRounded ? 0 : 1); + vstd::amin(killedCreatures, maxToKill); + } + + 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::NONE) + spellID = bonus->subtype.as(); + + 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 @@ -1260,70 +1329,7 @@ void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & 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); - - /* 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 && (!ranged || battle.battleHasWallPenalty(attacker, attacker->getPosition(), defender->getPosition()))) - return; //should not work from behind walls, except being defender or under effect of golden bow etc. - - 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()); - - if(bonus->type == BonusType::DEATH_STARE) - { - double cap = 1 / std::max(chanceToKill, (double)(0.01));//don't divide by 0 - int maxToKill = static_cast((attacker->getCount() + cap - 1) / cap); //not much more than chance * count - vstd::amin(killedCreatures, maxToKill); - - killedCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, BonusCustomSubtype::deathStareCommander)) / defender->level(); - } - else //ACCURATE_SHOT - { - bool isMaxToKillRounded = attacker->getCount() * singleCreatureKillChancePercent % 100 == 0; - int maxToKill = attacker->getCount() * singleCreatureKillChancePercent / 100 + (isMaxToKillRounded ? 0 : 1); - vstd::amin(killedCreatures, maxToKill); - } - - 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::NONE) - spellID = bonus->subtype.as(); - - 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); - } + HandleDeathStareAndPirateShot(battle, ranged, attacker, defender); } if(!defender->alive()) diff --git a/server/battles/BattleActionProcessor.h b/server/battles/BattleActionProcessor.h index 39d41eccd..b8d4ede5e 100644 --- a/server/battles/BattleActionProcessor.h +++ b/server/battles/BattleActionProcessor.h @@ -44,6 +44,9 @@ 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 CStack * defender); From 3a83de5e70cd9c7994aa0533034ae920b7909cff Mon Sep 17 00:00:00 2001 From: M Date: Wed, 10 Jan 2024 23:56:32 +0100 Subject: [PATCH 18/20] Simplified code a bit --- server/battles/BattleActionProcessor.cpp | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index 11ecaa58e..2ea776260 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -1220,21 +1220,18 @@ std::set BattleActionProcessor::getSpellsForAttackCasting(TConstBonusLi for(auto item : spellsWithBackupLayers) { - if(item.first < spellsWithBackupLayers.rbegin()->first) + bool areCurrentLayerSpellsApplied = std::all_of(item.second.begin(), item.second.end(), + [&](const std::shared_ptr spell) + { + std::vector activeSpells = defender->activeSpells(); + return vstd::find(activeSpells, spell->subtype.as()) != activeSpells.end(); + }); + + if(!areCurrentLayerSpellsApplied || item.first == spellsWithBackupLayers.rbegin()->first) { - bool areCurrentLayerSpellsApplied = std::all_of(item.second.begin(), item.second.end(), - [&](const std::shared_ptr spell) - { - std::vector activeSpells = defender->activeSpells(); - return vstd::find(activeSpells, spell->subtype.as()) != activeSpells.end(); - }); - - if(areCurrentLayerSpellsApplied) - continue; + addSpellsFromLayer(item.first); + break; } - - addSpellsFromLayer(item.first); - break; } return spellsToCast; From 250b1b69a804914eba39cec4cffaa837f779de46 Mon Sep 17 00:00:00 2001 From: Dydzio Date: Wed, 10 Jan 2024 23:57:29 +0100 Subject: [PATCH 19/20] Update accurate shot translation handling Co-authored-by: Ivan Savenko --- lib/spells/effects/Damage.cpp | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/lib/spells/effects/Damage.cpp b/lib/spells/effects/Damage.cpp index 2e02d0661..76203741a 100644 --- a/lib/spells/effects/Damage.cpp +++ b/lib/spells/effects/Damage.cpp @@ -155,17 +155,11 @@ void Damage::describeEffect(std::vector & log, const Mechanics * m, else if(m->getSpell()->getJsonKey().find("accurateShot") != std::string::npos && !multiple) { MetaString line; - if(kills > 1) - { - line.appendTextID("vcmi.battleWindow.accurateShot.resultDescription"); //(number) (unit type) was killed with an accurate shot! - line.replaceNumber(kills); - firstTarget->addNameReplacement(line, true); - } - else - { - line.appendTextID("vcmi.battleWindow.accurateShot.resultDescription.1"); //1 (unit type) were killed by accurate shots! - firstTarget->addNameReplacement(line, false); - } + std::string preferredLanguage = VLC->generaltexth->getPreferredLanguage(); + std::string textID = "vcmi.battleWindow.accurateShot.resultDescription"; + line.appendTextID(Languages::getPluralFormTextID( preferredLanguage, kills, text)); + line.replaceNumber(kills); + firstTarget->addNameReplacement(line, true); log.push_back(line); } else if(m->getSpellIndex() == SpellID::THUNDERBOLT && !multiple) From 9ee526d2025cbde1b89c77775203f102e7583934 Mon Sep 17 00:00:00 2001 From: M Date: Thu, 11 Jan 2024 21:10:22 +0100 Subject: [PATCH 20/20] Fixes from code review --- Mods/vcmi/config/vcmi/english.json | 2 +- lib/battle/DamageCalculator.cpp | 18 ++++++----------- lib/spells/effects/Damage.cpp | 6 ++++-- server/battles/BattleActionProcessor.cpp | 25 ++++++++++-------------- 4 files changed, 21 insertions(+), 30 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index e8edfa889..798d429c2 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -185,7 +185,7 @@ "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" : "1 %s was killed with an accurate shot!", + "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", diff --git a/lib/battle/DamageCalculator.cpp b/lib/battle/DamageCalculator.cpp index 31fe70d5c..58279e118 100644 --- a/lib/battle/DamageCalculator.cpp +++ b/lib/battle/DamageCalculator.cpp @@ -10,8 +10,6 @@ #include "StdInc.h" -#include - #include "DamageCalculator.h" #include "CBattleInfoCallback.h" #include "Unit.h" @@ -462,7 +460,8 @@ std::vector DamageCalculator::getAttackFactors() const getAttackJoustingFactor(), getAttackDeathBlowFactor(), getAttackDoubleDamageFactor(), - getAttackHateFactor() + getAttackHateFactor(), + getAttackRevengeFactor() }; } @@ -532,19 +531,14 @@ 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); - - //calculated separately since it bypasses cap on bonus damage - double revengeFactor = getAttackRevengeFactor(); - double revengeAdditionalMinDamage = std::round(damageBase.min * revengeFactor); - double revengeAdditionalMaxDamage = std::round(damageBase.max * revengeFactor); + double resultingFactor = attackFactorTotal * defenseFactorTotal; DamageRange damageDealt { - std::max( 1.0, std::floor(damageBase.min * resultingFactor + revengeAdditionalMinDamage)), - std::max( 1.0, std::floor(damageBase.max * resultingFactor + revengeAdditionalMaxDamage)) + std::max( 1.0, std::floor(damageBase.min * resultingFactor)), + std::max( 1.0, std::floor(damageBase.max * resultingFactor)) }; DamageRange killsDealt = getCasualties(damageDealt); diff --git a/lib/spells/effects/Damage.cpp b/lib/spells/effects/Damage.cpp index 76203741a..425607008 100644 --- a/lib/spells/effects/Damage.cpp +++ b/lib/spells/effects/Damage.cpp @@ -20,10 +20,12 @@ #include "../../battle/CBattleInfoCallback.h" #include "../../networkPacks/PacksForClientBattle.h" #include "../../CGeneralTextHandler.h" +#include "../../Languages.h" #include "../../serializer/JsonSerializeFormat.h" #include + VCMI_LIB_NAMESPACE_BEGIN namespace spells @@ -157,9 +159,9 @@ void Damage::describeEffect(std::vector & log, const Mechanics * m, MetaString line; std::string preferredLanguage = VLC->generaltexth->getPreferredLanguage(); std::string textID = "vcmi.battleWindow.accurateShot.resultDescription"; - line.appendTextID(Languages::getPluralFormTextID( preferredLanguage, kills, text)); + line.appendTextID(Languages::getPluralFormTextID( preferredLanguage, kills, textID)); line.replaceNumber(kills); - firstTarget->addNameReplacement(line, true); + firstTarget->addNameReplacement(line, kills != 1); log.push_back(line); } else if(m->getSpellIndex() == SpellID::THUNDERBOLT && !multiple) diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index 2ea776260..2529842eb 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -1258,9 +1258,13 @@ void BattleActionProcessor::HandleDeathStareAndPirateShot(const CBattleInfoCallb if(bonus == nullptr) bonus = attacker->getBonus(Selector::type()(BonusType::ACCURATE_SHOT)); - if(bonus->type == BonusType::ACCURATE_SHOT && (!ranged || battle.battleHasWallPenalty(attacker, attacker->getPosition(), defender->getPosition()))) - return; //should not work from behind walls, except being defender or under effect of golden bow etc. - + 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) @@ -1277,20 +1281,11 @@ void BattleActionProcessor::HandleDeathStareAndPirateShot(const CBattleInfoCallb std::binomial_distribution<> distribution(attacker->getCount(), chanceToKill); int killedCreatures = distribution(gameHandler->getRandomGenerator().getStdGenerator()); - if(bonus->type == BonusType::DEATH_STARE) - { - double cap = 1 / std::max(chanceToKill, (double)(0.01));//don't divide by 0 - int maxToKill = static_cast((attacker->getCount() + cap - 1) / cap); //not much more than chance * count - vstd::amin(killedCreatures, maxToKill); + 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(); - } - else //ACCURATE_SHOT - { - bool isMaxToKillRounded = attacker->getCount() * singleCreatureKillChancePercent % 100 == 0; - int maxToKill = attacker->getCount() * singleCreatureKillChancePercent / 100 + (isMaxToKillRounded ? 0 : 1); - vstd::amin(killedCreatures, maxToKill); - } if(killedCreatures) {