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:
parent
84f53485e2
commit
9205ef2c91
@ -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)
|
||||
|
@ -36,6 +36,9 @@
|
||||
"index": 145,
|
||||
"level": 0,
|
||||
"faction": "neutral",
|
||||
"abilities" : {
|
||||
"siegeMachine" : { "type" : "CATAPULT", "subtype" : "spell.catapultShot" }
|
||||
},
|
||||
"graphics" :
|
||||
{
|
||||
"animation": "SMCATA.DEF",
|
||||
|
@ -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
|
||||
}
|
||||
},
|
||||
|
@ -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" : {
|
||||
|
@ -284,7 +284,9 @@
|
||||
"battleEffects":{
|
||||
"catapult":{
|
||||
"type":"core:catapult",
|
||||
"targetsToAttack": 2
|
||||
"targetsToAttack": 2,
|
||||
"chanceToCrit" : 0,
|
||||
"chanceToNormalHit" : 100
|
||||
}
|
||||
},
|
||||
"range" : "X"
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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")}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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)*/ \
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user