Browse Source

vcmi: skill-agnostic ballistics

Made ballistics by using spell action and more code is shared now.
Konstantin 2 years ago
parent
commit
9205ef2c91

+ 1 - 5
client/battle/BattleSiegeController.cpp

@@ -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)

+ 3 - 0
config/creatures/special.json

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

+ 14 - 1
config/creatures/stronghold.json

@@ -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
 			}
 		},

+ 2 - 2
config/skills.json

@@ -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" : {

+ 3 - 1
config/spells/other.json

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

+ 131 - 0
config/spells/vcmiAbility.json

@@ -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
+		}
 	}
 }

+ 0 - 1
lib/CCreatureHandler.cpp

@@ -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")}

+ 0 - 30
lib/CHeroHandler.cpp

@@ -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);

+ 0 - 23
lib/CHeroHandler.h

@@ -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;
 	}
 

+ 1 - 1
lib/HeroBonus.h

@@ -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)*/  \

+ 13 - 13
lib/battle/CBattleInfoCallback.cpp

@@ -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;

+ 1 - 0
lib/battle/CBattleInfoCallback.h

@@ -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

+ 141 - 29
lib/spells/effects/Catapult.cpp

@@ -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
 {
-	//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
-	};
-
-	assert(potentialTargets.size() == size_t(EWallPart::PARTS_COUNT));
+	if(m->isMassive())
+		applyMassive(server, m); // Like earthquake
+	else
+		applyTargeted(server, m, eTarget); // Like catapult shots
+}
 
-	std::set<EWallPart> allowedTargets;
 
-	for (auto const & target : potentialTargets)
-	{
-		auto state = m->battle()->battleGetWallState(target);
+void Catapult::applyMassive(ServerCallback * server, const Mechanics * m) const
+{
+	//start with all destructible parts
+	std::vector<EWallPart> allowedTargets = getPotentialTargets(m, true, true);
 
-		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();
 }
 
 

+ 17 - 0
lib/spells/effects/Catapult.h

@@ -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;
 };
 
 }

+ 12 - 142
server/CGameHandler.cpp

@@ -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;