1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-11-28 08:48:48 +02:00

vcmi: skill-agnostic ballistics

Made ballistics by using spell action and more code is shared now.
This commit is contained in:
Konstantin 2023-02-21 02:47:40 +03:00
parent 84f53485e2
commit 9205ef2c91
15 changed files with 340 additions and 249 deletions

View File

@ -325,11 +325,7 @@ bool BattleSiegeController::isAttackableByCatapult(BattleHex hex) const
return false;
auto wallPart = owner.curInt->cb->battleHexToWallPart(hex);
if (!owner.curInt->cb->isWallPartPotentiallyAttackable(wallPart))
return false;
auto state = owner.curInt->cb->battleGetWallState(wallPart);
return state != EWallState::DESTROYED && state != EWallState::NONE;
return owner.curInt->cb->isWallPartAttackable(wallPart);
}
void BattleSiegeController::stackIsCatapulting(const CatapultAttack & ca)

View File

@ -36,6 +36,9 @@
"index": 145,
"level": 0,
"faction": "neutral",
"abilities" : {
"siegeMachine" : { "type" : "CATAPULT", "subtype" : "spell.catapultShot" }
},
"graphics" :
{
"animation": "SMCATA.DEF",

View File

@ -245,6 +245,13 @@
"index": 94,
"level": 6,
"faction": "stronghold",
"abilities" :
{
"siege" : {
"subtype" : "spell.cyclopsShot",
"type" : "CATAPULT"
}
},
"upgrades": ["cyclopKing"],
"graphics" :
{
@ -271,9 +278,15 @@
"faction": "stronghold",
"abilities":
{
"siegeDoubleAttack" :
"siege" : {
"subtype" : "spell.cyclopsShot",
"type" : "CATAPULT"
},
"siegeLevel" :
{
"subtype" : "spell.cyclopsShot",
"type" : "CATAPULT_EXTRA_SHOTS",
"valueType" : "BASE_NUMBER",
"val" : 1
}
},

View File

@ -277,8 +277,8 @@
"base" : {
"effects" : {
"main" : {
"subtype" : "skill.ballistics",
"type" : "SECONDARY_SKILL_PREMY",
"subtype" : "spell.catapultShot",
"type" : "CATAPULT_EXTRA_SHOTS",
"valueType" : "BASE_NUMBER"
},
"ctrl" : {

View File

@ -284,7 +284,9 @@
"battleEffects":{
"catapult":{
"type":"core:catapult",
"targetsToAttack": 2
"targetsToAttack": 2,
"chanceToCrit" : 0,
"chanceToNormalHit" : 100
}
},
"range" : "X"

View File

@ -101,5 +101,136 @@
"bonus.SIEGE_WEAPON" : "absolute"
}
}
},
"catapultShot" : {
"targetType" : "LOCATION",
"type": "ability",
"name": "Catapult shot",
"school" : {},
"level": 1,
"power": 1,
"defaultGainChance": 0,
"gainChance": {},
"levels" : {
"base":{
"description" : "",
"aiValue" : 0,
"power" : 1,
"cost" : 0,
"targetModifier":{"smart":true},
"battleEffects":{
"catapult":{
"type":"core:catapult"
}
},
"range" : "0"
},
"none":{
"battleEffects" : {
"catapult" : {
"targetsToAttack": 1,
"chanceToHitKeep" : 5,
"chanceToHitGate" : 25,
"chanceToHitTower" : 10,
"chanceToHitWall" : 50,
"chanceToNormalHit" : 60,
"chanceToCrit" : 30
}
}
},
"basic":{
"battleEffects" : {
"catapult" : {
"targetsToAttack": 1,
"chanceToHitKeep" : 7,
"chanceToHitGate" : 30,
"chanceToHitTower" : 15,
"chanceToHitWall" : 60,
"chanceToNormalHit" : 50,
"chanceToCrit" : 50
}
}
},
"advanced":{
"battleEffects" : {
"catapult" : {
"targetsToAttack": 2,
"chanceToHitKeep" : 7,
"chanceToHitGate" : 30,
"chanceToHitTower" : 15,
"chanceToHitWall" : 60,
"chanceToNormalHit" : 50,
"chanceToCrit" : 50
}
}
},
"expert":{
"battleEffects" : {
"catapult" : {
"targetsToAttack": 2,
"chanceToHitKeep" : 10,
"chanceToHitGate" : 40,
"chanceToHitTower" : 20,
"chanceToHitWall" : 75,
"chanceToNormalHit" : 0,
"chanceToCrit" : 100
}
}
}
},
"flags" : {
"indifferent": true
},
"targetCondition" : {
"nonMagical" : true
}
},
"cyclopsShot" : {
"targetType" : "LOCATION",
"type": "ability",
"name": "Siege shot",
"school" : {},
"level": 1,
"power": 1,
"defaultGainChance": 0,
"gainChance": {},
"levels" : {
"base":{
"description" : "",
"aiValue" : 0,
"power" : 1,
"cost" : 0,
"targetModifier":{"smart":true},
"battleEffects":{
"catapult":{
"type":"core:catapult",
"targetsToAttack": 1,
"chanceToHitKeep" : 7,
"chanceToHitGate" : 30,
"chanceToHitTower" : 15,
"chanceToHitWall" : 60,
"chanceToNormalHit" : 50,
"chanceToCrit" : 50
}
},
"range" : "0"
},
"none":{},
"basic":{
"battleEffects" : {
"catapult" : {
"targetsToAttack": 2
}
}
},
"advanced":{},
"expert" : {}
},
"flags" : {
"indifferent": true
},
"targetCondition" : {
"nonMagical" : true
}
}
}

View File

@ -490,7 +490,6 @@ void CCreatureHandler::loadBonuses(JsonNode & creature, std::string bonuses)
{"KING_2", makeBonusNode("KING", 2)}, // Advanced Slayer or better
{"KING_3", makeBonusNode("KING", 3)}, // Expert Slayer only
{"const_no_wall_penalty", makeBonusNode("NO_WALL_PENALTY")},
{"CATAPULT", makeBonusNode("CATAPULT")},
{"MULTI_HEADED", makeBonusNode("ATTACKS_ALL_ADJACENT")},
{"IMMUNE_TO_MIND_SPELLS", makeBonusNode("MIND_IMMUNITY")},
{"HAS_EXTENDED_ATTACK", makeBonusNode("TWO_HEX_ATTACK_BREATH")}

View File

@ -399,7 +399,6 @@ CHeroHandler::~CHeroHandler() = default;
CHeroHandler::CHeroHandler()
{
loadBallistics();
loadExperience();
}
@ -773,35 +772,6 @@ static std::string genRefName(std::string input)
return input;
}
void CHeroHandler::loadBallistics()
{
CLegacyConfigParser ballParser("DATA/BALLIST.TXT");
ballParser.endLine(); //header
ballParser.endLine();
do
{
ballParser.readString();
ballParser.readString();
CHeroHandler::SBallisticsLevelInfo bli;
bli.keep = static_cast<ui8>(ballParser.readNumber());
bli.tower = static_cast<ui8>(ballParser.readNumber());
bli.gate = static_cast<ui8>(ballParser.readNumber());
bli.wall = static_cast<ui8>(ballParser.readNumber());
bli.shots = static_cast<ui8>(ballParser.readNumber());
bli.noDmg = static_cast<ui8>(ballParser.readNumber());
bli.oneDmg = static_cast<ui8>(ballParser.readNumber());
bli.twoDmg = static_cast<ui8>(ballParser.readNumber());
bli.sum = static_cast<ui8>(ballParser.readNumber());
ballistics.push_back(bli);
assert(bli.noDmg + bli.oneDmg + bli.twoDmg == 100 && bli.sum == 100);
}
while (ballParser.endLine());
}
std::vector<JsonNode> CHeroHandler::loadLegacyData(size_t dataSize)
{
objects.resize(dataSize);

View File

@ -253,7 +253,6 @@ class DLL_LINKAGE CHeroHandler : public CHandlerBase<HeroTypeID, HeroType, CHero
void loadHeroSpecialty(CHero * hero, const JsonNode & node);
void loadExperience();
void loadBallistics();
public:
CHeroClassHandler classes;
@ -261,27 +260,6 @@ public:
//default costs of going through terrains. -1 means terrain is impassable
std::map<TerrainId, int> terrCosts;
struct SBallisticsLevelInfo
{
ui8 keep, tower, gate, wall; //chance to hit in percent (eg. 87 is 87%)
ui8 shots; //how many shots we have
ui8 noDmg, oneDmg, twoDmg; //chances for shot dealing certain dmg in percent (eg. 87 is 87%); must sum to 100
ui8 sum; //I don't know if it is useful for anything, but it's in config file
template <typename Handler> void serialize(Handler &h, const int version)
{
h & keep;
h & tower;
h & gate;
h & wall;
h & shots;
h & noDmg;
h & oneDmg;
h & twoDmg;
h & sum;
}
};
std::vector<SBallisticsLevelInfo> ballistics; //info about ballistics ability per level; [0] - none; [1] - basic; [2] - adv; [3] - expert
ui32 level(ui64 experience) const; //calculates level corresponding to given experience amount
ui64 reqExp(ui32 level) const; //calculates experience required for given level
@ -302,7 +280,6 @@ public:
h & classes;
h & objects;
h & expPerLevel;
h & ballistics;
h & terrCosts;
}

View File

@ -314,7 +314,7 @@ public:
BONUS_NAME(SOUL_STEAL) /*val - number of units gained per enemy killed, subtype = 0 - gained units survive after battle, 1 - they do not*/ \
BONUS_NAME(TRANSMUTATION) /*val - chance to trigger in %, subtype = 0 - resurrection based on HP, 1 - based on unit count, additional info - target creature ID (attacker default)*/\
BONUS_NAME(SUMMON_GUARDIANS) /*val - amount in % of stack count, subtype = creature ID*/\
BONUS_NAME(CATAPULT_EXTRA_SHOTS) /*val - number of additional shots, requires CATAPULT bonus to work*/\
BONUS_NAME(CATAPULT_EXTRA_SHOTS) /*val - power of catapult effect, requires CATAPULT bonus to work*/\
BONUS_NAME(RANGED_RETALIATION) /*allows shooters to perform ranged retaliation*/\
BONUS_NAME(BLOCKS_RANGED_RETALIATION) /*disallows ranged retaliation for shooter unit, BLOCKS_RETALIATION bonus is for melee retaliation only*/\
BONUS_NAME(SECONDARY_SKILL_VAL2) /*for secondary skills that have multiple effects, like eagle eye (max level and chance)*/ \

View File

@ -142,11 +142,7 @@ bool CBattleInfoCallback::battleHasWallPenalty(const IBonusBearer * shooter, Bat
if (wallPart == EWallPart::INDESTRUCTIBLE_PART)
return true; // always blocks ranged attacks
assert(isWallPartPotentiallyAttackable(wallPart));
EWallState state = battleGetWallState(wallPart);
return state != EWallState::DESTROYED;
return isWallPartAttackable(wallPart);
};
auto needWallPenalty = [&](BattleHex from, BattleHex dest)
@ -1417,6 +1413,16 @@ bool CBattleInfoCallback::isWallPartPotentiallyAttackable(EWallPart wallPart) co
wallPart != EWallPart::INVALID;
}
bool CBattleInfoCallback::isWallPartAttackable(EWallPart wallPart) const
{
RETURN_IF_NOT_BATTLE(false);
auto wallState = battleGetWallState(wallPart);
if(isWallPartPotentiallyAttackable(wallPart))
return (wallState == EWallState::REINFORCED || wallState == EWallState::INTACT || wallState == EWallState::DAMAGED);
return false;
}
std::vector<BattleHex> CBattleInfoCallback::getAttackableBattleHexes() const
{
std::vector<BattleHex> attackableBattleHexes;
@ -1424,14 +1430,8 @@ std::vector<BattleHex> CBattleInfoCallback::getAttackableBattleHexes() const
for(const auto & wallPartPair : wallParts)
{
if(isWallPartPotentiallyAttackable(wallPartPair.second))
{
auto wallState = battleGetWallState(wallPartPair.second);
if(wallState == EWallState::REINFORCED || wallState == EWallState::INTACT || wallState == EWallState::DAMAGED)
{
attackableBattleHexes.emplace_back(wallPartPair.first);
}
}
if(isWallPartAttackable(wallPartPair.second))
attackableBattleHexes.emplace_back(wallPartPair.first);
}
return attackableBattleHexes;

View File

@ -133,6 +133,7 @@ public:
BattleHex wallPartToBattleHex(EWallPart part) const;
EWallPart battleHexToWallPart(BattleHex hex) const; //returns part of destructible wall / gate / keep under given hex or -1 if not found
bool isWallPartPotentiallyAttackable(EWallPart wallPart) const; // returns true if the wall part is potentially attackable (independent of wall state), false if not
bool isWallPartAttackable(EWallPart wallPart) const; // returns true if the wall part is actually attackable, false if not
std::vector<BattleHex> getAttackableBattleHexes() const;
si8 battleMinSpellLevel(ui8 side) const; //calculates maximum spell level possible to be cast on battlefield - takes into account artifacts of both heroes; if no effects are set, 0 is returned

View File

@ -57,38 +57,26 @@ bool Catapult::applicable(Problem & problem, const Mechanics * m) const
return !attackableBattleHexes.empty() || m->adaptProblem(ESpellCastProblem::NO_APPROPRIATE_TARGET, problem);
}
void Catapult::apply(ServerCallback * server, const Mechanics * m, const EffectTarget & /* eTarget */) const
void Catapult::apply(ServerCallback * server, const Mechanics * m, const EffectTarget & eTarget) const
{
if(m->isMassive())
applyMassive(server, m); // Like earthquake
else
applyTargeted(server, m, eTarget); // Like catapult shots
}
void Catapult::applyMassive(ServerCallback * server, const Mechanics * m) const
{
//start with all destructible parts
static const std::set<EWallPart> potentialTargets =
{
EWallPart::KEEP,
EWallPart::BOTTOM_TOWER,
EWallPart::BOTTOM_WALL,
EWallPart::BELOW_GATE,
EWallPart::OVER_GATE,
EWallPart::UPPER_WALL,
EWallPart::UPPER_TOWER,
EWallPart::GATE
};
std::vector<EWallPart> allowedTargets = getPotentialTargets(m, true, true);
assert(potentialTargets.size() == size_t(EWallPart::PARTS_COUNT));
std::set<EWallPart> allowedTargets;
for (auto const & target : potentialTargets)
{
auto state = m->battle()->battleGetWallState(target);
if(state != EWallState::DESTROYED && state != EWallState::NONE)
allowedTargets.insert(target);
}
assert(!allowedTargets.empty());
if (allowedTargets.empty())
return;
CatapultAttack ca;
ca.attacker = -1;
ca.attacker = m->caster->getCasterUnitId();
for(int i = 0; i < targetsToAttack; i++)
{
@ -97,7 +85,6 @@ void Catapult::apply(ServerCallback * server, const Mechanics * m, const EffectT
// Potential overshots (more hits on same targets than remaining HP) are allowed
EWallPart target = *RandomGeneratorUtil::nextItem(allowedTargets, *server->getRNG());
auto attackInfo = ca.attackedParts.begin();
for ( ; attackInfo != ca.attackedParts.end(); ++attackInfo)
if ( attackInfo->attackedPart == target )
@ -105,8 +92,8 @@ void Catapult::apply(ServerCallback * server, const Mechanics * m, const EffectT
if (attackInfo == ca.attackedParts.end()) // new part
{
CatapultAttack::AttackInfo newInfo{};
newInfo.damageDealt = 1;
CatapultAttack::AttackInfo newInfo;
newInfo.damageDealt = getRandomDamage(server);
newInfo.attackedPart = target;
newInfo.destinationTile = m->battle()->wallPartToBattleHex(target);
ca.attackedParts.push_back(newInfo);
@ -114,12 +101,96 @@ void Catapult::apply(ServerCallback * server, const Mechanics * m, const EffectT
}
else // already damaged before, update damage
{
attackInfo->damageDealt += 1;
attackInfo->damageDealt += getRandomDamage(server);
}
}
server->apply(&ca);
removeTowerShooters(server, m);
}
void Catapult::applyTargeted(ServerCallback * server, const Mechanics * m, const EffectTarget & target) const
{
assert(!target.empty());
auto destination = target.at(0).hexValue;
auto desiredTarget = m->battle()->battleHexToWallPart(destination);
for(int i = 0; i < targetsToAttack; i++)
{
auto actualTarget = EWallPart::INVALID;
if ( m->battle()->isWallPartAttackable(desiredTarget) &&
server->getRNG()->getInt64Range(0, 99)() < getCatapultHitChance(desiredTarget))
{
actualTarget = desiredTarget;
}
else
{
std::vector<EWallPart> potentialTargets = getPotentialTargets(m, false, false);
if (potentialTargets.empty())
break; // everything is gone, can't attack anymore
actualTarget = *RandomGeneratorUtil::nextItem(potentialTargets, *server->getRNG());
}
assert(actualTarget != EWallPart::INVALID);
CatapultAttack::AttackInfo attack;
attack.attackedPart = actualTarget;
attack.destinationTile = m->battle()->wallPartToBattleHex(actualTarget);
attack.damageDealt = getRandomDamage(server);
CatapultAttack ca; //package for clients
ca.attacker = m->caster->getCasterUnitId();
ca.attackedParts.push_back(attack);
server->apply(&ca);
removeTowerShooters(server, m);
}
}
int Catapult::getCatapultHitChance(EWallPart part) const
{
switch(part)
{
case EWallPart::GATE:
return gate;
case EWallPart::KEEP:
return keep;
case EWallPart::BOTTOM_TOWER:
case EWallPart::UPPER_TOWER:
return tower;
case EWallPart::BOTTOM_WALL:
case EWallPart::BELOW_GATE:
case EWallPart::OVER_GATE:
case EWallPart::UPPER_WALL:
return wall;
default:
return 0;
}
}
int Catapult::getRandomDamage (ServerCallback * server) const
{
std::array<int, 3> damageChances = { noDmg, hit, crit }; //dmgChance[i] - chance for doing i dmg when hit is successful
int totalChance = std::accumulate(damageChances.begin(), damageChances.end(), 0);
int damageRandom = server->getRNG()->getInt64Range(0, totalChance - 1)();
int dealtDamage = 0;
//calculating dealt damage
for (int damage = 0; damage < damageChances.size(); ++damage)
{
if (damageRandom <= damageChances[damage])
{
dealtDamage = damage;
break;
}
damageRandom -= damageChances[damage];
}
return dealtDamage;
}
void Catapult::removeTowerShooters(ServerCallback * server, const Mechanics * m) const
{
BattleUnitsChanged removeUnits;
for (auto const wallPart : { EWallPart::KEEP, EWallPart::BOTTOM_TOWER, EWallPart::UPPER_TOWER })
@ -158,10 +229,51 @@ void Catapult::apply(ServerCallback * server, const Mechanics * m, const EffectT
server->apply(&removeUnits);
}
std::vector<EWallPart> Catapult::getPotentialTargets(const Mechanics * m, bool bypassGateCheck, bool bypassTowerCheck) const
{
std::vector<EWallPart> potentialTargets;
constexpr std::array<EWallPart, 4> walls = { EWallPart::BOTTOM_WALL, EWallPart::BELOW_GATE, EWallPart::OVER_GATE, EWallPart::UPPER_WALL };
constexpr std::array<EWallPart, 3> towers= { EWallPart::BOTTOM_TOWER, EWallPart::KEEP, EWallPart::UPPER_TOWER };
constexpr EWallPart gates = EWallPart::GATE;
// in H3, catapult under automatic control will attack objects in following order:
// walls, gates, towers
for (auto & part : walls)
if (m->battle()->isWallPartAttackable(part))
potentialTargets.push_back(part);
if ((potentialTargets.empty() || bypassGateCheck) && (m->battle()->isWallPartAttackable(gates)))
potentialTargets.push_back(gates);
if (potentialTargets.empty() || bypassTowerCheck)
for (auto & part : towers)
if (m->battle()->isWallPartAttackable(part))
potentialTargets.push_back(part);
return potentialTargets;
}
void Catapult::adjustHitChance()
{
vstd::abetween(keep, 0, 100);
vstd::abetween(tower, 0, 100);
vstd::abetween(gate, 0, 100);
vstd::abetween(wall, 0, 100);
vstd::abetween(crit, 0, 100);
vstd::abetween(hit, 0, 100 - crit);
vstd::amin(noDmg, 100 - hit - crit);
}
void Catapult::serializeJsonEffect(JsonSerializeFormat & handler)
{
//TODO: add configuration unifying with Catapult ability
handler.serializeInt("targetsToAttack", targetsToAttack);
handler.serializeInt("chanceToHitKeep", keep);
handler.serializeInt("chanceToHitGate", gate);
handler.serializeInt("chanceToHitTower", tower);
handler.serializeInt("chanceToHitWall", wall);
handler.serializeInt("chanceToNormalHit", hit);
handler.serializeInt("chanceToCrit", crit);
adjustHitChance();
}

View File

@ -11,6 +11,7 @@
#pragma once
#include "LocationEffect.h"
#include "../../GameConstants.h"
VCMI_LIB_NAMESPACE_BEGIN
@ -29,6 +30,22 @@ protected:
void serializeJsonEffect(JsonSerializeFormat & handler) override;
private:
int targetsToAttack = 0;
//Ballistics percentage
int gate = 0;
int keep = 0;
int tower = 0;
int wall = 0;
//Damage percentage, used for both ballistics and earthquake
int hit = 0;
int crit = 0;
int noDmg = 0;
int getCatapultHitChance(EWallPart part) const;
int getRandomDamage(ServerCallback * server) const;
void adjustHitChance();
void applyMassive(ServerCallback * server, const Mechanics * m) const;
void applyTargeted(ServerCallback * server, const Mechanics * m, const EffectTarget & target) const;
void removeTowerShooters(ServerCallback * server, const Mechanics * m) const;
std::vector<EWallPart> getPotentialTargets(const Mechanics * m, bool bypassGateCheck, bool bypassTowerCheck) const;
};
}

View File

@ -4859,150 +4859,20 @@ bool CGameHandler::makeBattleAction(BattleAction &ba)
}
case EActionType::CATAPULT:
{
//TODO: unify with spells::effects:Catapult
auto getCatapultHitChance = [](EWallPart part, const CHeroHandler::SBallisticsLevelInfo & sbi) -> int
{
switch(part)
{
case EWallPart::GATE:
return sbi.gate;
case EWallPart::KEEP:
return sbi.keep;
case EWallPart::BOTTOM_TOWER:
case EWallPart::UPPER_TOWER:
return sbi.tower;
case EWallPart::BOTTOM_WALL:
case EWallPart::BELOW_GATE:
case EWallPart::OVER_GATE:
case EWallPart::UPPER_WALL:
return sbi.wall;
default:
return 0;
}
};
auto getBallisticsInfo = [this, &ba] (const CStack * actor)
{
const CGHeroInstance * attackingHero = gs->curB->battleGetFightingHero(ba.side);
if(actor->getCreature()->idNumber == CreatureID::CATAPULT)
return VLC->heroh->ballistics.at(attackingHero->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::BALLISTICS));
else
{
//by design use advanced ballistics parameters with this bonus present, upg. cyclops use advanced ballistics, nonupg. use basic in OH3
int ballisticsLevel = actor->hasBonusOfType(Bonus::CATAPULT_EXTRA_SHOTS) ? 2 : 1;
auto parameters = VLC->heroh->ballistics.at(ballisticsLevel);
parameters.shots = 1 + std::max(actor->valOfBonuses(Bonus::CATAPULT_EXTRA_SHOTS), 0);
return parameters;
}
};
auto isWallPartAttackable = [this] (EWallPart part)
{
return (gs->curB->si.wallState[part] == EWallState::REINFORCED || gs->curB->si.wallState[part] == EWallState::INTACT || gs->curB->si.wallState[part] == EWallState::DAMAGED);
};
CHeroHandler::SBallisticsLevelInfo stackBallisticsParameters = getBallisticsInfo(stack);
auto wrapper = wrapAction(ba);
auto destination = target.empty() ? BattleHex(BattleHex::INVALID) : target.at(0).hexValue;
auto desiredTarget = gs->curB->battleHexToWallPart(destination);
for (int shotNumber=0; shotNumber<stackBallisticsParameters.shots; ++shotNumber)
const CStack * shooter = gs->curB->battleGetStackByID(ba.stackNumber);
std::shared_ptr<const Bonus> catapultAbility = stack->getBonusLocalFirst(Selector::type()(Bonus::CATAPULT));
if(!catapultAbility || catapultAbility->subtype < 0)
{
auto actualTarget = EWallPart::INVALID;
if ( isWallPartAttackable(desiredTarget) &&
getRandomGenerator().nextInt(99) < getCatapultHitChance(desiredTarget, stackBallisticsParameters))
{
actualTarget = desiredTarget;
}
else
{
static const std::array<EWallPart, 4> walls = { EWallPart::BOTTOM_WALL, EWallPart::BELOW_GATE, EWallPart::OVER_GATE, EWallPart::UPPER_WALL };
static const std::array<EWallPart, 3> towers= { EWallPart::BOTTOM_TOWER, EWallPart::KEEP, EWallPart::UPPER_TOWER };
static const EWallPart gates = EWallPart::GATE;
// in H3, catapult under automatic control will attack objects in following order:
// walls, gates, towers
std::vector<EWallPart> potentialTargets;
for (auto & part : walls )
if (isWallPartAttackable(part))
potentialTargets.push_back(part);
if (potentialTargets.empty() && isWallPartAttackable(gates))
potentialTargets.push_back(gates);
if (potentialTargets.empty())
for (auto & part : towers )
if (isWallPartAttackable(part))
potentialTargets.push_back(part);
if (potentialTargets.empty())
break; // everything is gone, can't attack anymore
actualTarget = *RandomGeneratorUtil::nextItem(potentialTargets, getRandomGenerator());
}
assert(actualTarget != EWallPart::INVALID);
std::array<int, 3> damageChances = { stackBallisticsParameters.noDmg, stackBallisticsParameters.oneDmg, stackBallisticsParameters.twoDmg }; //dmgChance[i] - chance for doing i dmg when hit is successful
int totalChance = std::accumulate(damageChances.begin(), damageChances.end(), 0);
int damageRandom = getRandomGenerator().nextInt(totalChance - 1);
int dealtDamage = 0;
//calculating dealt damage
for (int damage = 0; damage < damageChances.size(); ++damage)
{
if (damageRandom <= damageChances[damage])
{
dealtDamage = damage;
break;
}
damageRandom -= damageChances[damage];
}
CatapultAttack::AttackInfo attack;
attack.attackedPart = actualTarget;
attack.destinationTile = gs->curB->wallPartToBattleHex(actualTarget);
attack.damageDealt = dealtDamage;
CatapultAttack ca; //package for clients
ca.attacker = ba.stackNumber;
ca.attackedParts.push_back(attack);
sendAndApply(&ca);
logGlobal->trace("Catapult attacks %d dealing %d damage", (int)attack.attackedPart, (int)attack.damageDealt);
//removing creatures in turrets / keep if one is destroyed
if (gs->curB->si.wallState[actualTarget] == EWallState::DESTROYED && (actualTarget == EWallPart::KEEP || actualTarget == EWallPart::BOTTOM_TOWER || actualTarget == EWallPart::UPPER_TOWER))
{
int posRemove = -1;
switch(actualTarget)
{
case EWallPart::KEEP:
posRemove = BattleHex::CASTLE_CENTRAL_TOWER;
break;
case EWallPart::BOTTOM_TOWER:
posRemove = BattleHex::CASTLE_BOTTOM_TOWER;
break;
case EWallPart::UPPER_TOWER:
posRemove = BattleHex::CASTLE_UPPER_TOWER;
break;
}
for(auto & elem : gs->curB->stacks)
{
if(elem->initialPosition == posRemove)
{
BattleUnitsChanged removeUnits;
removeUnits.changedStacks.emplace_back(elem->unitId(), UnitChanges::EOperation::REMOVE);
sendAndApply(&removeUnits);
break;
}
}
}
complain("We do not know how to shoot :P");
}
else
{
const CSpell * spell = SpellID(catapultAbility->subtype).toSpell();
spells::BattleCast parameters(gs->curB, shooter, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can shot infinitely by catapult
auto shotLevel = stack->valOfBonuses(Selector::typeSubtype(Bonus::CATAPULT_EXTRA_SHOTS, catapultAbility->subtype));
parameters.setSpellLevel(shotLevel);
parameters.cast(spellEnv, target);
}
//finish by scope guard
break;