2017-07-20 06:08:49 +02:00
|
|
|
/*
|
|
|
|
* Catapult.cpp, part of VCMI engine
|
|
|
|
*
|
|
|
|
* Authors: listed in file AUTHORS in main folder
|
|
|
|
*
|
|
|
|
* License: GNU General Public License v2.0 or later
|
|
|
|
* Full text of license available in license.txt file, in main folder
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
#include "StdInc.h"
|
|
|
|
|
|
|
|
#include "Catapult.h"
|
|
|
|
|
|
|
|
#include "Registry.h"
|
|
|
|
#include "../ISpellMechanics.h"
|
|
|
|
|
|
|
|
#include "../../battle/IBattleState.h"
|
|
|
|
#include "../../battle/CBattleInfoCallback.h"
|
|
|
|
#include "../../battle/Unit.h"
|
|
|
|
#include "../../mapObjects/CGTownInstance.h"
|
2024-08-28 21:33:56 +02:00
|
|
|
#include "../../entities/building/TownFortifications.h"
|
2023-10-23 12:59:15 +02:00
|
|
|
#include "../../networkPacks/PacksForClientBattle.h"
|
2017-07-20 06:08:49 +02:00
|
|
|
#include "../../serializer/JsonSerializeFormat.h"
|
2024-06-01 17:28:17 +02:00
|
|
|
|
|
|
|
#include <vstd/RNG.h>
|
2017-07-20 06:08:49 +02:00
|
|
|
|
2022-07-26 15:07:42 +02:00
|
|
|
VCMI_LIB_NAMESPACE_BEGIN
|
|
|
|
|
2017-07-20 06:08:49 +02:00
|
|
|
namespace spells
|
|
|
|
{
|
|
|
|
namespace effects
|
|
|
|
{
|
|
|
|
|
|
|
|
bool Catapult::applicable(Problem & problem, const Mechanics * m) const
|
|
|
|
{
|
2023-02-07 00:40:01 +02:00
|
|
|
const auto *town = m->battle()->battleGetDefendedTown();
|
2017-07-20 06:08:49 +02:00
|
|
|
|
|
|
|
if(nullptr == town)
|
|
|
|
{
|
|
|
|
return m->adaptProblem(ESpellCastProblem::NO_APPROPRIATE_TARGET, problem);
|
|
|
|
}
|
|
|
|
|
2024-08-28 21:33:56 +02:00
|
|
|
if(town->fortificationsLevel().wallsHealth == 0)
|
2017-07-20 06:08:49 +02:00
|
|
|
{
|
|
|
|
return m->adaptProblem(ESpellCastProblem::NO_APPROPRIATE_TARGET, problem);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(m->isSmart() && m->casterSide != BattleSide::ATTACKER)
|
|
|
|
{
|
|
|
|
//if spell targeting is smart, then only attacker can use it
|
|
|
|
return m->adaptProblem(ESpellCastProblem::NO_APPROPRIATE_TARGET, problem);
|
|
|
|
}
|
|
|
|
|
Entities redesign and a few ERM features
* Made most Handlers derived from CHandlerBase and moved service API there.
* Declared existing Entity APIs.
* Added basic script context caching
* Started Lua script module
* Started Lua spell effect API
* Started script state persistence
* Started battle info callback binding
* CommitPackage removed
* Extracted spells::Caster to own header; Expanded Spell API.
* implemented !!MC:S, !!FU:E, !!FU:P, !!MA, !!VR:H, !!VR:C
* !!BU:C, !!BU:E, !!BU:G, !!BU:M implemented
* Allow use of "MC:S@varName@" to declare normal variable (technically v-variable with string key)
* Re-enabled VERM macros.
* !?GM0 added
* !?TM implemented
* Added !!MF:N
* Started !?OB, !!BM, !!HE, !!OW, !!UN
* Added basic support of w-variables
* Added support for ERM indirect variables
* Made !?FU regular trigger
* !!re (ERA loop receiver) implemented
* Fixed ERM receivers with zero args.
2018-03-17 16:58:30 +02:00
|
|
|
const auto attackableBattleHexes = m->battle()->getAttackableBattleHexes();
|
|
|
|
|
2019-03-20 21:58:15 +02:00
|
|
|
return !attackableBattleHexes.empty() || m->adaptProblem(ESpellCastProblem::NO_APPROPRIATE_TARGET, problem);
|
2017-07-20 06:08:49 +02:00
|
|
|
}
|
|
|
|
|
2023-02-21 01:47:40 +02:00
|
|
|
void Catapult::apply(ServerCallback * server, const Mechanics * m, const EffectTarget & eTarget) const
|
2017-07-20 06:08:49 +02:00
|
|
|
{
|
2023-02-21 01:47:40 +02:00
|
|
|
if(m->isMassive())
|
|
|
|
applyMassive(server, m); // Like earthquake
|
|
|
|
else
|
|
|
|
applyTargeted(server, m, eTarget); // Like catapult shots
|
|
|
|
}
|
2022-12-08 18:48:08 +02:00
|
|
|
|
|
|
|
|
2023-02-21 01:47:40 +02:00
|
|
|
void Catapult::applyMassive(ServerCallback * server, const Mechanics * m) const
|
|
|
|
{
|
|
|
|
//start with all destructible parts
|
|
|
|
std::vector<EWallPart> allowedTargets = getPotentialTargets(m, true, true);
|
2022-12-08 18:48:08 +02:00
|
|
|
|
|
|
|
assert(!allowedTargets.empty());
|
|
|
|
if (allowedTargets.empty())
|
|
|
|
return;
|
2017-07-20 06:08:49 +02:00
|
|
|
|
|
|
|
CatapultAttack ca;
|
2023-11-27 13:25:49 +02:00
|
|
|
ca.battleID = m->battle()->getBattle()->getBattleID();
|
2023-04-12 22:56:12 +02:00
|
|
|
ca.attacker = m->caster->getHeroCaster() ? -1 : m->caster->getCasterUnitId();
|
2017-07-20 06:08:49 +02:00
|
|
|
|
|
|
|
for(int i = 0; i < targetsToAttack; i++)
|
|
|
|
{
|
2022-12-08 18:48:08 +02:00
|
|
|
// Hit on any existing, not destroyed targets are allowed
|
|
|
|
// Multiple hit on same target are allowed.
|
|
|
|
// Potential overshots (more hits on same targets than remaining HP) are allowed
|
2023-01-13 00:35:58 +02:00
|
|
|
EWallPart target = *RandomGeneratorUtil::nextItem(allowedTargets, *server->getRNG());
|
2017-07-20 06:08:49 +02:00
|
|
|
|
2022-12-08 18:48:08 +02:00
|
|
|
auto attackInfo = ca.attackedParts.begin();
|
|
|
|
for ( ; attackInfo != ca.attackedParts.end(); ++attackInfo)
|
|
|
|
if ( attackInfo->attackedPart == target )
|
|
|
|
break;
|
2017-07-20 06:08:49 +02:00
|
|
|
|
2022-12-08 18:48:08 +02:00
|
|
|
if (attackInfo == ca.attackedParts.end()) // new part
|
|
|
|
{
|
2023-02-21 01:47:40 +02:00
|
|
|
CatapultAttack::AttackInfo newInfo;
|
|
|
|
newInfo.damageDealt = getRandomDamage(server);
|
2022-12-08 18:48:08 +02:00
|
|
|
newInfo.attackedPart = target;
|
|
|
|
newInfo.destinationTile = m->battle()->wallPartToBattleHex(target);
|
|
|
|
ca.attackedParts.push_back(newInfo);
|
|
|
|
attackInfo = ca.attackedParts.end() - 1;
|
|
|
|
}
|
|
|
|
else // already damaged before, update damage
|
|
|
|
{
|
2023-02-21 01:47:40 +02:00
|
|
|
attackInfo->damageDealt += getRandomDamage(server);
|
2022-12-08 18:48:08 +02:00
|
|
|
}
|
2023-01-13 00:35:58 +02:00
|
|
|
}
|
2024-10-04 20:59:51 +02:00
|
|
|
server->apply(ca);
|
2023-01-13 00:35:58 +02:00
|
|
|
|
2023-02-21 01:47:40 +02:00
|
|
|
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) &&
|
2024-06-01 15:36:38 +02:00
|
|
|
server->getRNG()->nextInt(0, 99) < getCatapultHitChance(desiredTarget))
|
2023-02-21 01:47:40 +02:00
|
|
|
{
|
|
|
|
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
|
2023-09-05 16:22:11 +02:00
|
|
|
ca.battleID = m->battle()->getBattle()->getBattleID();
|
2023-04-12 22:56:12 +02:00
|
|
|
ca.attacker = m->caster->getHeroCaster() ? -1 : m->caster->getCasterUnitId();
|
2023-02-21 01:47:40 +02:00
|
|
|
ca.attackedParts.push_back(attack);
|
2024-10-04 20:59:51 +02:00
|
|
|
server->apply(ca);
|
2023-02-21 01:47:40 +02:00
|
|
|
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);
|
2024-06-01 15:36:38 +02:00
|
|
|
int damageRandom = server->getRNG()->nextInt(0, totalChance - 1);
|
2023-02-21 01:47:40 +02:00
|
|
|
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
|
|
|
|
{
|
2023-01-13 00:35:58 +02:00
|
|
|
BattleUnitsChanged removeUnits;
|
2023-08-31 17:45:52 +02:00
|
|
|
removeUnits.battleID = m->battle()->getBattle()->getBattleID();
|
2023-01-13 00:35:58 +02:00
|
|
|
|
|
|
|
for (auto const wallPart : { EWallPart::KEEP, EWallPart::BOTTOM_TOWER, EWallPart::UPPER_TOWER })
|
|
|
|
{
|
2017-07-20 06:08:49 +02:00
|
|
|
//removing creatures in turrets / keep if one is destroyed
|
|
|
|
BattleHex posRemove;
|
2023-01-13 00:35:58 +02:00
|
|
|
auto state = m->battle()->battleGetWallState(wallPart);
|
2017-07-20 06:08:49 +02:00
|
|
|
|
2023-01-13 00:35:58 +02:00
|
|
|
switch(wallPart)
|
2017-07-20 06:08:49 +02:00
|
|
|
{
|
|
|
|
case EWallPart::KEEP:
|
2022-12-08 18:48:08 +02:00
|
|
|
posRemove = BattleHex::CASTLE_CENTRAL_TOWER;
|
2017-07-20 06:08:49 +02:00
|
|
|
break;
|
|
|
|
case EWallPart::BOTTOM_TOWER:
|
2022-12-08 18:48:08 +02:00
|
|
|
posRemove = BattleHex::CASTLE_BOTTOM_TOWER;
|
2017-07-20 06:08:49 +02:00
|
|
|
break;
|
|
|
|
case EWallPart::UPPER_TOWER:
|
2022-12-08 18:48:08 +02:00
|
|
|
posRemove = BattleHex::CASTLE_UPPER_TOWER;
|
2017-07-20 06:08:49 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2023-01-13 00:35:58 +02:00
|
|
|
if(state == EWallState::DESTROYED) //HP enum subtraction not intuitive, consider using SiegeInfo::applyDamage
|
2017-07-20 06:08:49 +02:00
|
|
|
{
|
Entities redesign and a few ERM features
* Made most Handlers derived from CHandlerBase and moved service API there.
* Declared existing Entity APIs.
* Added basic script context caching
* Started Lua script module
* Started Lua spell effect API
* Started script state persistence
* Started battle info callback binding
* CommitPackage removed
* Extracted spells::Caster to own header; Expanded Spell API.
* implemented !!MC:S, !!FU:E, !!FU:P, !!MA, !!VR:H, !!VR:C
* !!BU:C, !!BU:E, !!BU:G, !!BU:M implemented
* Allow use of "MC:S@varName@" to declare normal variable (technically v-variable with string key)
* Re-enabled VERM macros.
* !?GM0 added
* !?TM implemented
* Added !!MF:N
* Started !?OB, !!BM, !!HE, !!OW, !!UN
* Added basic support of w-variables
* Added support for ERM indirect variables
* Made !?FU regular trigger
* !!re (ERA loop receiver) implemented
* Fixed ERM receivers with zero args.
2018-03-17 16:58:30 +02:00
|
|
|
auto all = m->battle()->battleGetUnitsIf([=](const battle::Unit * unit)
|
2017-07-20 06:08:49 +02:00
|
|
|
{
|
2023-01-13 00:35:58 +02:00
|
|
|
return !unit->isGhost() && unit->getPosition() == posRemove;
|
2017-07-20 06:08:49 +02:00
|
|
|
});
|
|
|
|
|
2023-01-13 00:35:58 +02:00
|
|
|
assert(all.size() == 0 || all.size() == 1);
|
2017-07-20 06:08:49 +02:00
|
|
|
for(auto & elem : all)
|
2023-01-13 00:35:58 +02:00
|
|
|
removeUnits.changedStacks.emplace_back(elem->unitId(), UnitChanges::EOperation::REMOVE);
|
2017-07-20 06:08:49 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-05 13:55:44 +02:00
|
|
|
if(!removeUnits.changedStacks.empty())
|
2024-10-04 20:59:51 +02:00
|
|
|
server->apply(removeUnits);
|
2017-07-20 06:08:49 +02:00
|
|
|
}
|
|
|
|
|
2023-02-21 01:47:40 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2017-07-20 06:08:49 +02:00
|
|
|
void Catapult::serializeJsonEffect(JsonSerializeFormat & handler)
|
|
|
|
{
|
|
|
|
handler.serializeInt("targetsToAttack", targetsToAttack);
|
2023-02-21 01:47:40 +02:00
|
|
|
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();
|
2017-07-20 06:08:49 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
2022-07-26 15:07:42 +02:00
|
|
|
|
|
|
|
VCMI_LIB_NAMESPACE_END
|