From 7283a4861e7186777ee7a07f7c814fd6f921ec24 Mon Sep 17 00:00:00 2001 From: Dydzio Date: Thu, 4 Jan 2024 22:27:51 +0100 Subject: [PATCH] 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;