Browse Source

Support for configurable town fortifications

Removed most of hardcoded checks for fort level or for presence of fort/
citadel/castle buildings.

It is now possible to define which parts of town fortifications are
provided by town buildings

Configuration for H3-like fortifications is provided in
buildingsLibrary.json and will be used automatically by mods as long as
mods have buidings named "fort", "citadel" and "castle".

Alternatively, mods can separately define:
- hitpoints of walls (shared value for all sections)
- hitpoints of central, upper and lower towers (separate values)
- presence of moat
- shooters for each tower (separate values)
Ivan Savenko 1 năm trước cách đây
mục cha
commit
36c1ed670f

+ 1 - 1
AI/BattleAI/BattleAI.cpp

@@ -229,7 +229,7 @@ BattleAction CBattleAI::useCatapult(const BattleID & battleID, const CStack * st
 		{
 			auto wallState = cb->getBattle(battleID)->battleGetWallState(wallPart);
 
-			if(wallState == EWallState::REINFORCED || wallState == EWallState::INTACT || wallState == EWallState::DAMAGED)
+			if(wallState != EWallState::NONE && wallState != EWallState::DESTROYED)
 			{
 				targetHex = cb->getBattle(battleID)->wallPartToBattleHex(wallPart);
 				break;

+ 2 - 1
AI/BattleAI/BattleEvaluator.cpp

@@ -17,6 +17,7 @@
 #include "../../lib/CStopWatch.h"
 #include "../../lib/CThreadHelper.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
+#include "../../lib/entities/building/TownFortifications.h"
 #include "../../lib/spells/CSpellHandler.h"
 #include "../../lib/spells/ISpellMechanics.h"
 #include "../../lib/battle/BattleStateInfoForRetreat.h"
@@ -262,7 +263,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 	if(score <= EvaluationResult::INEFFECTIVE_SCORE
 		&& !stack->hasBonusOfType(BonusType::FLYING)
 		&& stack->unitSide() == BattleSide::ATTACKER
-		&& cb->getBattle(battleID)->battleGetSiegeLevel() >= CGTownInstance::CITADEL)
+	   && cb->getBattle(battleID)->battleGetFortifications().hasMoat)
 	{
 		auto brokenWallMoat = getBrokenWallMoatHexes();
 

+ 1 - 1
client/battle/BattleAnimationClasses.cpp

@@ -161,7 +161,7 @@ ECreatureAnimType AttackAnimation::findValidGroup( const std::vector<ECreatureAn
 const CCreature * AttackAnimation::getCreature() const
 {
 	if (attackingStack->unitType()->getId() == CreatureID::ARROW_TOWERS)
-		return owner.siegeController->getTurretCreature();
+		return owner.siegeController->getTurretCreature(attackingStack->initialPosition);
 	else
 		return attackingStack->unitType();
 }

+ 1 - 1
client/battle/BattleInterface.cpp

@@ -85,7 +85,7 @@ BattleInterface::BattleInterface(const BattleID & battleID, const CCreatureSet *
 	this->army2 = army2;
 
 	const CGTownInstance *town = getBattle()->battleGetDefendedTown();
-	if(town && town->hasFort())
+	if(town && town->fortificationsLevel().wallsHealth > 0)
 		siegeController.reset(new BattleSiegeController(*this, town));
 
 	windowObject = std::make_shared<BattleWindow>(*this);

+ 1 - 1
client/battle/BattleProjectileController.cpp

@@ -160,7 +160,7 @@ const CCreature & BattleProjectileController::getShooter(const CStack * stack) c
 	const CCreature * creature = stack->unitType();
 
 	if(creature->getId() == CreatureID::ARROW_TOWERS)
-		creature = owner.siegeController->getTurretCreature();
+		creature = owner.siegeController->getTurretCreature(stack->initialPosition);
 
 	if(creature->animation.missileFrameAngles.empty())
 	{

+ 37 - 30
client/battle/BattleSiegeController.cpp

@@ -27,6 +27,7 @@
 
 #include "../../CCallback.h"
 #include "../../lib/CStack.h"
+#include "../../lib/entities/building/TownFortifications.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
 #include "../../lib/networkPacks/PacksForClientBattle.h"
 
@@ -34,30 +35,27 @@ ImagePath BattleSiegeController::getWallPieceImageName(EWallVisual::EWallVisual
 {
 	auto getImageIndex = [&]() -> int
 	{
-		bool isTower = (what == EWallVisual::KEEP || what == EWallVisual::BOTTOM_TOWER || what == EWallVisual::UPPER_TOWER);
+		int health = static_cast<int>(state);
 
-		switch (state)
+		switch (what)
 		{
-		case EWallState::REINFORCED :
-			return 1;
-		case EWallState::INTACT :
-			if (town->hasBuilt(BuildingID::CASTLE))
-				return 2; // reinforced walls were damaged
-			else
-				return 1;
-		case EWallState::DAMAGED :
-			// towers don't have separate image here - INTACT and DAMAGED is 1, DESTROYED is 2
-			if (isTower)
-				return 1;
-			else
-				return 2;
-		case EWallState::DESTROYED :
-			if (isTower)
-				return 2;
-			else
+			case EWallVisual::KEEP:
+			case EWallVisual::BOTTOM_TOWER:
+			case EWallVisual::UPPER_TOWER:
+				if (health > 0)
+					return 1;
+				else
+					return 2;
+			default:
+			{
+				int healthTotal = town->fortificationsLevel().wallsHealth;
+				if (healthTotal == health)
+					return 1;
+				if (health > 0)
+					return 2;
 				return 3;
-		}
-		return 1;
+			}
+		};
 	};
 
 	const std::string & prefix = town->town->clientInfo.siegePrefix;
@@ -128,16 +126,15 @@ ImagePath BattleSiegeController::getBattleBackgroundName() const
 
 bool BattleSiegeController::getWallPieceExistence(EWallVisual::EWallVisual what) const
 {
-	//FIXME: use this instead of buildings test?
-	//ui8 siegeLevel = owner.curInt->cb->battleGetSiegeLevel();
+	const auto & fortifications = town->fortificationsLevel();
 
 	switch (what)
 	{
-	case EWallVisual::MOAT:              return town->hasBuilt(BuildingID::CITADEL) && town->town->clientInfo.siegePositions.at(EWallVisual::MOAT).isValid();
-	case EWallVisual::MOAT_BANK:         return town->hasBuilt(BuildingID::CITADEL) && town->town->clientInfo.siegePositions.at(EWallVisual::MOAT_BANK).isValid();
-	case EWallVisual::KEEP_BATTLEMENT:   return town->hasBuilt(BuildingID::CITADEL) && owner.getBattle()->battleGetWallState(EWallPart::KEEP) != EWallState::DESTROYED;
-	case EWallVisual::UPPER_BATTLEMENT:  return town->hasBuilt(BuildingID::CASTLE) && owner.getBattle()->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::DESTROYED;
-	case EWallVisual::BOTTOM_BATTLEMENT: return town->hasBuilt(BuildingID::CASTLE) && owner.getBattle()->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::DESTROYED;
+	case EWallVisual::MOAT:              return fortifications.hasMoat && town->town->clientInfo.siegePositions.at(EWallVisual::MOAT).isValid();
+	case EWallVisual::MOAT_BANK:         return fortifications.hasMoat && town->town->clientInfo.siegePositions.at(EWallVisual::MOAT_BANK).isValid();
+	case EWallVisual::KEEP_BATTLEMENT:   return fortifications.citadelHealth > 0 && owner.getBattle()->battleGetWallState(EWallPart::KEEP) != EWallState::DESTROYED;
+	case EWallVisual::UPPER_BATTLEMENT:  return fortifications.upperTowerHealth > 0 && owner.getBattle()->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::DESTROYED;
+	case EWallVisual::BOTTOM_BATTLEMENT: return fortifications.lowerTowerHealth > 0 && owner.getBattle()->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::DESTROYED;
 	default:                             return true;
 	}
 }
@@ -186,9 +183,19 @@ BattleSiegeController::BattleSiegeController(BattleInterface & owner, const CGTo
 	}
 }
 
-const CCreature *BattleSiegeController::getTurretCreature() const
+const CCreature *BattleSiegeController::getTurretCreature(BattleHex position) const
 {
-	return town->town->clientInfo.siegeShooter.toCreature();
+	switch (position)
+	{
+		case BattleHex::CASTLE_CENTRAL_TOWER:
+			return town->fortificationsLevel().citadelShooter.toCreature();
+		case BattleHex::CASTLE_UPPER_TOWER:
+			return town->fortificationsLevel().upperTowerShooter.toCreature();
+		case BattleHex::CASTLE_BOTTOM_TOWER:
+			return town->fortificationsLevel().lowerTowerShooter.toCreature();
+	}
+
+	throw std::runtime_error("Unable to select shooter for tower at " + std::to_string(position.hex));
 }
 
 Point BattleSiegeController::getTurretCreaturePosition( BattleHex position ) const

+ 1 - 1
client/battle/BattleSiegeController.h

@@ -104,7 +104,7 @@ public:
 	/// queries from other battle controllers
 	bool isAttackableByCatapult(BattleHex hex) const;
 	ImagePath getBattleBackgroundName() const;
-	const CCreature *getTurretCreature() const;
+	const CCreature *getTurretCreature(BattleHex turretPosition) const;
 	Point getTurretCreaturePosition( BattleHex position ) const;
 
 	const CGTownInstance *getSiegedTown() const;

+ 1 - 1
client/battle/BattleStacksController.cpp

@@ -191,7 +191,7 @@ void BattleStacksController::stackAdded(const CStack * stack, bool instant)
 	{
 		assert(owner.siegeController);
 
-		const CCreature *turretCreature = owner.siegeController->getTurretCreature();
+		const CCreature *turretCreature = owner.siegeController->getTurretCreature(stack->initialPosition);
 
 		stackAnimation[stack->unitId()] = AnimationControls::getAnimation(turretCreature);
 		stackAnimation[stack->unitId()]->pos.h = turretCreatureAnimationHeight;

+ 25 - 3
config/buildingsLibrary.json

@@ -19,9 +19,31 @@
 		]
 	},
 	"shipyard":       { "id" : 6 },
-	"fort":           { "id" : 7 },
-	"citadel":        { "id" : 8,  "upgrades" : "fort" },
-	"castle":         { "id" : 9,  "upgrades" : "citadel" },
+	"fort": {
+		"id" : 7,
+		"fortifications" : {
+			"wallsHealth" : 2
+		}
+	},
+	
+	"citadel": {
+		"id" : 8,
+		"upgrades" : "fort",
+		"fortifications" : {
+			"citadelHealth" : 2,
+			"hasMoat" : true
+		}
+	},
+	
+	"castle": {
+		"id" : 9,
+		"upgrades" : "citadel",
+		"fortifications" : {
+			"wallsHealth" : 3,
+			"upperTowerHealth" : 2,
+			"lowerTowerHealth" : 2
+		}
+	},
 	
 	"villageHall": {
 		"id" : 10,

+ 16 - 0
config/schemas/townBuilding.json

@@ -65,6 +65,22 @@
 			"description" : "Optional, configuration of building that can be activated by visiting hero",
 			"$ref" : "rewardable.json"
 		},
+		"firtufications" : {
+			"type" : "object",
+			"additionalProperties" : false,
+			"description" : "Fortifications provided by this buildings, if any",
+			"properties" : {
+				"citadelShooter" :    { "type" : "string", "description" : "Creature ID of shooter located in central keep (citadel). Used only if citadel is present." },
+				"upperTowerShooter" : { "type" : "string", "description" : "Creature ID of shooter located in upper tower. Used only if upper tower is present." },
+				"lowerTowerShooter" : { "type" : "string", "description" : "Creature ID of shooter located in lower tower. Used only if lower tower is present." },
+
+				"wallsHealth" :       { "type" : "number", "description" : "Maximum health of destructible walls. Walls are only present if their health is above zero" },
+				"citadelHealth" :     { "type" : "number", "description" : "Maximum health of central tower or 0 if not present. Requires walls presence." },
+				"upperTowerHealth" :  { "type" : "number", "description" : "Maximum health of upper tower or 0 if not present. Requires walls presence." },
+				"lowerTowerHealth" :  { "type" : "number", "description" : "Maximum health of lower tower or 0 if not present. Requires walls presence." },
+				"hasMoat" :           { "type" : "boolean","description" : "If set to true, moat will be placed in front of the walls. Requires walls presence." }
+			}
+		},
 		"cost" : {
 			"type" : "object",
 			"additionalProperties" : false,

+ 26 - 2
docs/modders/Entities_Format/Town_Building_Format.md

@@ -157,9 +157,33 @@ These are just a couple of examples of what can be done in VCMI. See vcmi config
 	"produce" : { 
 		"sulfur" : 1,
 		"gold" : 2000
-	}, 
+	},
+	
+	// Optional, allows this building to add fortifications during siege
+	"fortifications" : {
+		// Maximum health of destructible walls. Walls are only present if their health is above zero".
+		// Presence of walls is required for all other fortification types
+		"wallsHealth" : 3,
+
+		// If set to true, moat will be placed in front of the walls. Requires walls presence.
+		"hasMoat" : true
+
+		// Maximum health of central tower or 0 if not present. Requires walls presence.
+		"citadelHealth" : 2,
+		// Maximum health of upper tower or 0 if not present. Requires walls presence.
+		"upperTowerHealth" : 2,
+		// Maximum health of lower tower or 0 if not present. Requires walls presence.
+		"lowerTowerHealth" : 2,
+
+		// Creature ID of shooter located in central keep (citadel). Used only if citadel is present.
+		"citadelShooter" : "archer",
+		// Creature ID of shooter located in upper tower. Used only if upper tower is present.
+		"upperTowerShooter" : "archer",
+		// Creature ID of shooter located in lower tower. Used only if lower tower is present.
+		"lowerTowerShooter" : "archer",
+	},
 
-    //determine how this building can be built. Possible values are:
+	//determine how this building can be built. Possible values are:
 	// normal  - default value. Fulfill requirements, use resources, spend one day
 	// auto    - building appears when all requirements are built
 	// special - building can not be built manually

+ 1 - 0
lib/CMakeLists.txt

@@ -450,6 +450,7 @@ set(lib_MAIN_HEADERS
 
 	entities/building/CBuilding.h
 	entities/building/CBuildingHandler.h
+	entities/building/TownFortifications.h
 	entities/faction/CFaction.h
 	entities/faction/CTown.h
 	entities/faction/CTownHandler.h

+ 18 - 22
lib/battle/BattleInfo.cpp

@@ -14,6 +14,7 @@
 #include "bonuses/Updaters.h"
 #include "../CStack.h"
 #include "../CHeroHandler.h"
+#include "../entities/building/TownFortifications.h"
 #include "../filesystem/Filesystem.h"
 #include "../mapObjects/CGTownInstance.h"
 #include "../texts/CGeneralTextHandler.h"
@@ -202,28 +203,25 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const
 	}
 
 	//setting up siege obstacles
-	if (town && town->hasFort())
+	if (town && town->fortificationsLevel().wallsHealth != 0)
 	{
+		auto fortification = town->fortificationsLevel();
+
 		curB->si.gateState = EGateState::CLOSED;
 
 		curB->si.wallState[EWallPart::GATE] = EWallState::INTACT;
 
 		for(const auto wall : {EWallPart::BOTTOM_WALL, EWallPart::BELOW_GATE, EWallPart::OVER_GATE, EWallPart::UPPER_WALL})
-		{
-			if (town->hasBuilt(BuildingID::CASTLE))
-				curB->si.wallState[wall] = EWallState::REINFORCED;
-			else
-				curB->si.wallState[wall] = EWallState::INTACT;
-		}
+			curB->si.wallState[wall] = static_cast<EWallState>(fortification.wallsHealth);
 
-		if (town->hasBuilt(BuildingID::CITADEL))
-			curB->si.wallState[EWallPart::KEEP] = EWallState::INTACT;
+		if (fortification.citadelHealth != 0)
+			curB->si.wallState[EWallPart::KEEP] = static_cast<EWallState>(fortification.citadelHealth);
 
-		if (town->hasBuilt(BuildingID::CASTLE))
-		{
-			curB->si.wallState[EWallPart::UPPER_TOWER] = EWallState::INTACT;
-			curB->si.wallState[EWallPart::BOTTOM_TOWER] = EWallState::INTACT;
-		}
+		if (fortification.upperTowerHealth != 0)
+			curB->si.wallState[EWallPart::UPPER_TOWER] = static_cast<EWallState>(fortification.upperTowerHealth);
+
+		if (fortification.lowerTowerHealth != 0)
+			curB->si.wallState[EWallPart::BOTTOM_TOWER] = static_cast<EWallState>(fortification.lowerTowerHealth);
 	}
 
 	//randomize obstacles
@@ -369,7 +367,7 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const
 			handleWarMachine(BattleSide::ATTACKER, ArtifactPosition::MACH1, 52);
 			handleWarMachine(BattleSide::ATTACKER, ArtifactPosition::MACH2, 18);
 			handleWarMachine(BattleSide::ATTACKER, ArtifactPosition::MACH3, 154);
-			if(town && town->hasFort())
+			if(town && town->fortificationsLevel().wallsHealth > 0)
 				handleWarMachine(BattleSide::ATTACKER, ArtifactPosition::MACH4, 120);
 		}
 
@@ -419,18 +417,16 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const
 
 	}
 
-	if (curB->town && curB->town->fortLevel() >= CGTownInstance::CITADEL)
+	if (curB->town)
 	{
-		// keep tower
-		curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), BattleSide::DEFENDER, SlotID::ARROW_TOWERS_SLOT, BattleHex::CASTLE_CENTRAL_TOWER);
+		if (curB->town->fortificationsLevel().citadelHealth != 0)
+			curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), BattleSide::DEFENDER, SlotID::ARROW_TOWERS_SLOT, BattleHex::CASTLE_CENTRAL_TOWER);
 
-		if (curB->town->fortLevel() >= CGTownInstance::CASTLE)
-		{
-			// lower tower + upper tower
+		if (curB->town->fortificationsLevel().upperTowerHealth != 0)
 			curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), BattleSide::DEFENDER, SlotID::ARROW_TOWERS_SLOT, BattleHex::CASTLE_UPPER_TOWER);
 
+		if (curB->town->fortificationsLevel().lowerTowerHealth != 0)
 			curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), BattleSide::DEFENDER, SlotID::ARROW_TOWERS_SLOT, BattleHex::CASTLE_BOTTOM_TOWER);
-		}
 
 		//Moat generating is done on server
 	}

+ 6 - 5
lib/battle/CBattleInfoCallback.cpp

@@ -18,6 +18,7 @@
 #include "CObstacleInstance.h"
 #include "DamageCalculator.h"
 #include "PossiblePlayerBattleAction.h"
+#include "../entities/building/TownFortifications.h"
 #include "../spells/ObstacleCasterProxy.h"
 #include "../spells/ISpellMechanics.h"
 #include "../spells/Problem.h"
@@ -237,7 +238,7 @@ bool CBattleInfoCallback::battleHasPenaltyOnLine(BattleHex from, BattleHex dest,
 bool CBattleInfoCallback::battleHasWallPenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const
 {
 	RETURN_IF_NOT_BATTLE(false);
-	if(!battleGetSiegeLevel())
+	if(battleGetFortifications().wallsHealth == 0)
 		return false;
 
 	const std::string cachingStrNoWallPenalty = "type_NO_WALL_PENALTY";
@@ -288,7 +289,7 @@ std::vector<PossiblePlayerBattleAction> CBattleInfoCallback::getClientActionsFor
 			allowedActionList.push_back(PossiblePlayerBattleAction::MOVE_STACK);
 
 		const auto * siegedTown = battleGetDefendedTown();
-		if(siegedTown && siegedTown->hasFort() && stack->hasBonusOfType(BonusType::CATAPULT)) //TODO: check shots
+		if(siegedTown && siegedTown->fortificationsLevel().wallsHealth > 0 && stack->hasBonusOfType(BonusType::CATAPULT)) //TODO: check shots
 			allowedActionList.push_back(PossiblePlayerBattleAction::CATAPULT);
 		if(stack->hasBonusOfType(BonusType::HEALER))
 			allowedActionList.push_back(PossiblePlayerBattleAction::HEAL);
@@ -943,7 +944,7 @@ AccessibilityInfo CBattleInfoCallback::getAccessibility() const
 	}
 
 	//gate -> should be before stacks
-	if(battleGetSiegeLevel() > 0)
+	if(battleGetFortifications().wallsHealth > 0)
 	{
 		EAccessibility accessibility = EAccessibility::ACCESSIBLE;
 		switch(battleGetGateState())
@@ -975,7 +976,7 @@ AccessibilityInfo CBattleInfoCallback::getAccessibility() const
 	}
 
 	//walls
-	if(battleGetSiegeLevel() > 0)
+	if(battleGetFortifications().wallsHealth > 0)
 	{
 		static const int permanentlyLocked[] = {12, 45, 62, 112, 147, 165};
 		for(auto hex : permanentlyLocked)
@@ -1612,7 +1613,7 @@ bool CBattleInfoCallback::isWallPartAttackable(EWallPart wallPart) const
 	if(isWallPartPotentiallyAttackable(wallPart))
 	{
 		auto wallState = battleGetWallState(wallPart);
-		return (wallState == EWallState::REINFORCED || wallState == EWallState::INTACT || wallState == EWallState::DAMAGED);
+		return (wallState != EWallState::NONE && wallState != EWallState::DESTROYED);
 	}
 	return false;
 }

+ 11 - 8
lib/battle/CBattleInfoEssentials.cpp

@@ -9,12 +9,15 @@
  */
 #include "StdInc.h"
 #include "CBattleInfoEssentials.h"
+
 #include "../CStack.h"
 #include "BattleInfo.h"
 #include "CObstacleInstance.h"
-#include "../mapObjects/CGTownInstance.h"
-#include "../gameState/InfoAboutArmy.h"
+
 #include "../constants/EntityIdentifiers.h"
+#include "../entities/building/TownFortifications.h"
+#include "../gameState/InfoAboutArmy.h"
+#include "../mapObjects/CGTownInstance.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -345,10 +348,10 @@ bool CBattleInfoEssentials::playerHasAccessToHeroInfo(const PlayerColor & player
 	return false;
 }
 
-ui8 CBattleInfoEssentials::battleGetSiegeLevel() const
+TownFortifications CBattleInfoEssentials::battleGetFortifications() const
 {
-	RETURN_IF_NOT_BATTLE(CGTownInstance::NONE);
-	return getBattle()->getDefendedTown() ? getBattle()->getDefendedTown()->fortLevel() : CGTownInstance::NONE;
+	RETURN_IF_NOT_BATTLE(TownFortifications());
+	return getBattle()->getDefendedTown() ? getBattle()->getDefendedTown()->fortificationsLevel() : TownFortifications();
 }
 
 bool CBattleInfoEssentials::battleCanSurrender(const PlayerColor & player) const
@@ -371,7 +374,7 @@ bool CBattleInfoEssentials::battleHasHero(BattleSide side) const
 EWallState CBattleInfoEssentials::battleGetWallState(EWallPart partOfWall) const
 {
 	RETURN_IF_NOT_BATTLE(EWallState::NONE);
-	if(battleGetSiegeLevel() == CGTownInstance::NONE)
+	if(battleGetFortifications().wallsHealth == 0)
 		return EWallState::NONE;
 
 	return getBattle()->getWallState(partOfWall);
@@ -380,7 +383,7 @@ EWallState CBattleInfoEssentials::battleGetWallState(EWallPart partOfWall) const
 EGateState CBattleInfoEssentials::battleGetGateState() const
 {
 	RETURN_IF_NOT_BATTLE(EGateState::NONE);
-	if(battleGetSiegeLevel() == CGTownInstance::NONE)
+	if(battleGetFortifications().wallsHealth == 0)
 		return EGateState::NONE;
 
 	return getBattle()->getGateState();
@@ -389,7 +392,7 @@ EGateState CBattleInfoEssentials::battleGetGateState() const
 bool CBattleInfoEssentials::battleIsGatePassable() const
 {
 	RETURN_IF_NOT_BATTLE(true);
-	if(battleGetSiegeLevel() == CGTownInstance::NONE)
+	if(battleGetFortifications().wallsHealth == 0)
 		return true;
 
 	return battleGetGateState() == EGateState::OPENED || battleGetGateState() == EGateState::DESTROYED; 

+ 2 - 1
lib/battle/CBattleInfoEssentials.h

@@ -18,6 +18,7 @@ class CGHeroInstance;
 class CStack;
 class IBonusBearer;
 struct InfoAboutHero;
+struct TownFortifications;
 class CArmedInstance;
 
 using TStacks = std::vector<const CStack *>;
@@ -75,7 +76,7 @@ public:
 	BattleSide playerToSide(const PlayerColor & player) const;
 	PlayerColor sideToPlayer(BattleSide side) const;
 	bool playerHasAccessToHeroInfo(const PlayerColor & player, const CGHeroInstance * h) const;
-	ui8 battleGetSiegeLevel() const; //returns 0 when there is no siege, 1 if fort, 2 is citadel, 3 is castle
+	TownFortifications battleGetFortifications() const;
 	bool battleHasHero(BattleSide side) const;
 	uint32_t battleCastSpells(BattleSide side) const; //how many spells has given side cast
 	const CGHeroInstance * battleGetFightingHero(BattleSide side) const; //deprecated for players callback, easy to get wrong

+ 3 - 0
lib/entities/building/CBuilding.h

@@ -9,6 +9,8 @@
  */
 #pragma once
 
+#include "TownFortifications.h"
+
 #include "../../constants/EntityIdentifiers.h"
 #include "../../LogicalExpression.h"
 #include "../../ResourceSet.h"
@@ -35,6 +37,7 @@ public:
 	TResources produce;
 	TRequired requirements;
 	ArtifactID warMachine;
+	TownFortifications fortifications;
 	std::set<EMarketMode> marketModes;
 
 	BuildingID bid; //structure ID

+ 49 - 0
lib/entities/building/TownFortifications.h

@@ -0,0 +1,49 @@
+/*
+ * TownFortifications.h, 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
+ *
+ */
+#pragma once
+
+#include "../../constants/EntityIdentifiers.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+struct TownFortifications
+{
+	CreatureID citadelShooter;
+	CreatureID upperTowerShooter;
+	CreatureID lowerTowerShooter;
+	SpellID moatSpell;
+
+	int8_t wallsHealth = 0;
+	int8_t citadelHealth = 0;
+	int8_t upperTowerHealth = 0;
+	int8_t lowerTowerHealth = 0;
+	bool hasMoat = false;
+
+	const TownFortifications & operator +=(const TownFortifications & other)
+	{
+		if (other.citadelShooter.hasValue())
+			citadelShooter = other.citadelShooter;
+		if (other.upperTowerShooter.hasValue())
+			upperTowerShooter = other.upperTowerShooter;
+		if (other.lowerTowerShooter.hasValue())
+			lowerTowerShooter = other.lowerTowerShooter;
+		if (other.moatSpell.hasValue())
+			moatSpell = other.moatSpell;
+
+		wallsHealth = std::max(wallsHealth, other.wallsHealth);
+		citadelHealth = std::max(citadelHealth, other.citadelHealth);
+		upperTowerHealth = std::max(upperTowerHealth, other.upperTowerHealth);
+		lowerTowerHealth = std::max(lowerTowerHealth, other.lowerTowerHealth);
+		hasMoat = hasMoat || other.hasMoat;
+		return *this;
+	}
+};
+
+VCMI_LIB_NAMESPACE_END

+ 1 - 1
lib/entities/faction/CTown.cpp

@@ -18,7 +18,7 @@
 VCMI_LIB_NAMESPACE_BEGIN
 
 CTown::CTown()
-	: faction(nullptr), mageLevel(0), primaryRes(0), moatAbility(SpellID::NONE), defaultTavernChance(0)
+	: faction(nullptr), mageLevel(0), primaryRes(0), defaultTavernChance(0)
 {
 }
 

+ 5 - 2
lib/entities/faction/CTown.h

@@ -9,6 +9,7 @@
  */
 #pragma once
 
+#include "../building/TownFortifications.h"
 #include "../../ConstTransitivePtr.h"
 #include "../../Point.h"
 #include "../../constants/EntityIdentifiers.h"
@@ -70,7 +71,10 @@ public:
 	ui32 mageLevel; //max available mage guild level
 	GameResID primaryRes;
 	CreatureID warMachineDeprecated;
-	SpellID moatAbility;
+
+	/// Base state of fortifications for empty town.
+	/// Used to define shooter units and moat spell ID
+	TownFortifications fortifications;
 
 	// default chance for hero of specific class to appear in tavern, if field "tavern" was not set
 	// resulting chance = sqrt(town.chance * heroClass.chance)
@@ -99,7 +103,6 @@ public:
 
 		std::string siegePrefix;
 		std::vector<Point> siegePositions;
-		CreatureID siegeShooter; // shooter creature ID
 		std::string towerIconSmall;
 		std::string towerIconLarge;
 

+ 30 - 3
lib/entities/faction/CTownHandler.cpp

@@ -299,6 +299,31 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons
 	ret->resources = TResources(source["cost"]);
 	ret->produce =   TResources(source["produce"]);
 
+	const JsonNode & fortifications = source["fortifications"];
+	if (!fortifications.isNull())
+	{
+		VLC->identifiers()->requestIdentifierOptional("creature", fortifications["citadelShooter"], [=](si32 identifier)
+		{
+			ret->fortifications.citadelShooter = CreatureID(identifier);
+		});
+
+		VLC->identifiers()->requestIdentifierOptional("creature", fortifications["upperTowerShooter"], [=](si32 identifier)
+		{
+			ret->fortifications.upperTowerShooter = CreatureID(identifier);
+		});
+
+		VLC->identifiers()->requestIdentifierOptional("creature", fortifications["lowerTowerShooter"], [=](si32 identifier)
+		{
+			ret->fortifications.lowerTowerShooter = CreatureID(identifier);
+		});
+
+		ret->fortifications.wallsHealth = fortifications["wallsHealth"].Integer();
+		ret->fortifications.citadelHealth = fortifications["citadelHealth"].Integer();
+		ret->fortifications.upperTowerHealth = fortifications["upperTowerHealth"].Integer();
+		ret->fortifications.lowerTowerHealth = fortifications["lowerTowerHealth"].Integer();
+		ret->fortifications.hasMoat = fortifications["hasMoat"].Bool();
+	}
+
 	loadBuildingBonuses(source["bonuses"], ret->buildingBonuses, ret);
 
 	if(!source["configuration"].isNull())
@@ -477,7 +502,9 @@ void CTownHandler::loadSiegeScreen(CTown &town, const JsonNode & source) const
 				, town.faction->getNameTranslated()
 				, (*VLC->creh)[crId]->getNameSingularTranslated());
 
-		town.clientInfo.siegeShooter = crId;
+		town.fortifications.citadelShooter = crId;
+		town.fortifications.upperTowerShooter = crId;
+		town.fortifications.lowerTowerShooter = crId;
 	});
 
 	auto & pos = town.clientInfo.siegePositions;
@@ -581,14 +608,14 @@ void CTownHandler::loadTown(CTown * town, const JsonNode & source)
 	{
 		VLC->identifiers()->requestIdentifier( "spell", source["moatAbility"], [=](si32 ability)
 		{
-			town->moatAbility = SpellID(ability);
+			town->fortifications.moatSpell = SpellID(ability);
 		});
 	}
 	else
 	{
 		VLC->identifiers()->requestIdentifier( source.getModScope(), "spell", "castleMoat", [=](si32 ability)
 		{
-			town->moatAbility = SpellID(ability);
+			town->fortifications.moatSpell = SpellID(ability);
 		});
 	}
 

+ 13 - 4
lib/mapObjects/CGTownInstance.cpp

@@ -245,6 +245,19 @@ bool CGTownInstance::hasCapitol() const
 	return hasBuilt(BuildingID::CAPITOL);
 }
 
+TownFortifications CGTownInstance::fortificationsLevel() const
+{
+	auto result = town->fortifications;
+
+	for (auto const	& buildingID : builtBuildings)
+		result += town->buildings.at(buildingID)->fortifications;
+
+	if (result.wallsHealth == 0)
+		return TownFortifications();
+
+	return result;
+}
+
 CGTownInstance::CGTownInstance(IGameCallback *cb):
 	CGDwelling(cb),
 	town(nullptr),
@@ -384,8 +397,6 @@ void CGTownInstance::initializeConfigurableBuildings(vstd::RNG & rand)
 
 DamageRange CGTownInstance::getTowerDamageRange() const
 {
-	assert(hasBuilt(BuildingID::CASTLE));
-
 	// http://heroes.thelazy.net/wiki/Arrow_tower
 	// base damage, irregardless of town level
 	static constexpr int baseDamage = 6;
@@ -402,8 +413,6 @@ DamageRange CGTownInstance::getTowerDamageRange() const
 
 DamageRange CGTownInstance::getKeepDamageRange() const
 {
-	assert(hasBuilt(BuildingID::CITADEL));
-
 	// http://heroes.thelazy.net/wiki/Arrow_tower
 	// base damage, irregardless of town level
 	static constexpr int baseDamage = 10;

+ 2 - 0
lib/mapObjects/CGTownInstance.h

@@ -19,6 +19,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 class CCastleEvent;
 class CTown;
 class TownBuildingInstance;
+struct TownFortifications;
 class TownRewardableBuildingInstance;
 struct DamageRange;
 
@@ -162,6 +163,7 @@ public:
 
 	bool needsLastStack() const override;
 	CGTownInstance::EFortLevel fortLevel() const;
+	TownFortifications fortificationsLevel() const;
 	int hallLevel() const; // -1 - none, 0 - village, 1 - town, 2 - city, 3 - capitol
 	int mageGuildLevel() const; // -1 - none, 0 - village, 1 - town, 2 - city, 3 - capitol
 	int getHordeLevel(const int & HID) const; //HID - 0 or 1; returns creature level or -1 if that horde structure is not present

+ 6 - 0
lib/modding/IdentifierStorage.cpp

@@ -190,6 +190,12 @@ void CIdentifierStorage::requestIdentifier(const JsonNode & name, const std::fun
 	requestIdentifier(ObjectCallback::fromNameWithType(name.getModScope(), name.String(), callback, false));
 }
 
+void CIdentifierStorage::requestIdentifierOptional(const std::string & type, const JsonNode & name, const std::function<void(si32)> & callback) const
+{
+	if (!name.isNull())
+		requestIdentifier(type, name, callback);
+}
+
 void CIdentifierStorage::tryRequestIdentifier(const std::string & scope, const std::string & type, const std::string & name, const std::function<void(si32)> & callback) const
 {
 	requestIdentifier(ObjectCallback::fromNameAndType(scope, type, name, callback, true));

+ 2 - 0
lib/modding/IdentifierStorage.h

@@ -84,6 +84,8 @@ public:
 	void requestIdentifier(const std::string & type, const JsonNode & name, const std::function<void(si32)> & callback) const;
 	void requestIdentifier(const JsonNode & name, const std::function<void(si32)> & callback) const;
 
+	void requestIdentifierOptional(const std::string & type, const JsonNode & name, const std::function<void(si32)> & callback) const;
+
 	/// try to request ID. If ID with such name won't be loaded, callback function will not be called
 	void tryRequestIdentifier(const std::string & scope, const std::string & type, const std::string & name, const std::function<void(si32)> & callback) const;
 	void tryRequestIdentifier(const std::string & type, const JsonNode & name, const std::function<void(si32)> & callback) const;

+ 2 - 1
lib/networkPacks/NetPacksLib.cpp

@@ -33,6 +33,7 @@
 #include "CPlayerState.h"
 #include "TerrainHandler.h"
 #include "entities/building/CBuilding.h"
+#include "entities/building/TownFortifications.h"
 #include "mapObjects/CBank.h"
 #include "mapObjects/CGCreature.h"
 #include "mapObjects/CGMarket.h"
@@ -2328,7 +2329,7 @@ void CatapultAttack::applyBattle(IBattleState * battleState)
 	if(!town)
 		return;
 
-	if(town->fortLevel() == CGTownInstance::NONE)
+	if(town->fortificationsLevel().wallsHealth == 0)
 		return;
 
 	for(const auto & part : attackedParts)

+ 2 - 1
lib/spells/effects/Catapult.cpp

@@ -18,6 +18,7 @@
 #include "../../battle/CBattleInfoCallback.h"
 #include "../../battle/Unit.h"
 #include "../../mapObjects/CGTownInstance.h"
+#include "../../entities/building/TownFortifications.h"
 #include "../../networkPacks/PacksForClientBattle.h"
 #include "../../serializer/JsonSerializeFormat.h"
 
@@ -39,7 +40,7 @@ bool Catapult::applicable(Problem & problem, const Mechanics * m) const
 		return m->adaptProblem(ESpellCastProblem::NO_APPROPRIATE_TARGET, problem);
 	}
 
-	if(CGTownInstance::NONE == town->fortLevel())
+	if(town->fortificationsLevel().wallsHealth == 0)
 	{
 		return m->adaptProblem(ESpellCastProblem::NO_APPROPRIATE_TARGET, problem);
 	}

+ 3 - 2
lib/spells/effects/Moat.cpp

@@ -19,6 +19,7 @@
 #include "../../bonuses/Limiters.h"
 #include "../../battle/IBattleState.h"
 #include "../../battle/CBattleInfoCallback.h"
+#include "../../entities/building/TownFortifications.h"
 #include "../../json/JsonBonus.h"
 #include "../../serializer/JsonSerializeFormat.h"
 #include "../../networkPacks/PacksForClient.h"
@@ -85,7 +86,7 @@ void Moat::convertBonus(const Mechanics * m, std::vector<Bonus> & converted) con
 		//Moat battlefield effect is always permanent
 		nb.duration = BonusDuration::ONE_BATTLE;
 
-		if(m->battle()->battleGetDefendedTown() && m->battle()->battleGetSiegeLevel() >= CGTownInstance::CITADEL)
+		if(m->battle()->battleGetDefendedTown() && m->battle()->battleGetFortifications().hasMoat)
 		{
 			nb.sid = BonusSourceID(m->battle()->battleGetDefendedTown()->town->buildings.at(BuildingID::CITADEL)->getUniqueTypeID());
 			nb.source = BonusSource::TOWN_STRUCTURE;
@@ -109,7 +110,7 @@ void Moat::apply(ServerCallback * server, const Mechanics * m, const EffectTarge
 {
 	assert(m->isMassive());
 	assert(m->battle()->battleGetDefendedTown());
-	if(m->isMassive() && m->battle()->battleGetSiegeLevel() >= CGTownInstance::CITADEL)
+	if(m->isMassive() && m->battle()->battleGetFortifications().hasMoat)
 	{
 		EffectTarget moat;
 		placeObstacles(server, m, moat);

+ 2 - 1
lib/spells/effects/Obstacle.cpp

@@ -16,6 +16,7 @@
 
 #include "../../battle/IBattleState.h"
 #include "../../battle/CBattleInfoCallback.h"
+#include "../../entities/building/TownFortifications.h"
 #include "../../networkPacks/PacksForClientBattle.h"
 #include "../../serializer/JsonSerializeFormat.h"
 
@@ -239,7 +240,7 @@ bool Obstacle::isHexAvailable(const CBattleInfoCallback * cb, const BattleHex &
 		if(i->obstacleType != CObstacleInstance::MOAT)
 			return false;
 
-	if(cb->battleGetSiegeLevel() != 0)
+	if(cb->battleGetFortifications().wallsHealth != 0)
 	{
 		EWallPart part = cb->battleHexToWallPart(hex);
 

+ 2 - 1
lib/spells/effects/Teleport.cpp

@@ -15,6 +15,7 @@
 #include "../../battle/IBattleState.h"
 #include "../../battle/CBattleInfoCallback.h"
 #include "../../battle/Unit.h"
+#include "../../entities/building/TownFortifications.h"
 #include "../../networkPacks/PacksForClientBattle.h"
 #include "../../serializer/JsonSerializeFormat.h"
 
@@ -64,7 +65,7 @@ bool Teleport::applicable(Problem & problem, const Mechanics * m, const EffectTa
 	if(!targetHex.isValid() || !m->battle()->getAccessibility(targetUnit).accessible(targetHex, targetUnit))
 		return m->adaptProblem(ESpellCastProblem::WRONG_SPELL_TARGET, problem);
 
-	if(m->battle()->battleGetSiegeLevel() && !(isWallPassable && isMoatPassable))
+	if(m->battle()->battleGetFortifications().wallsHealth > 0 && !(isWallPassable && isMoatPassable))
 	{
 		return !m->battle()->battleHasPenaltyOnLine(target[0].hexValue, target[1].hexValue, !isWallPassable, !isMoatPassable);
 	}

+ 2 - 1
server/battles/BattleActionProcessor.cpp

@@ -21,6 +21,7 @@
 #include "../../lib/battle/CObstacleInstance.h"
 #include "../../lib/battle/IBattleState.h"
 #include "../../lib/battle/BattleAction.h"
+#include "../../lib/entities/building/TownFortifications.h"
 #include "../../lib/gameState/CGameState.h"
 #include "../../lib/networkPacks/PacksForClientBattle.h"
 #include "../../lib/networkPacks/SetStackEffect.h"
@@ -651,7 +652,7 @@ int BattleActionProcessor::moveStack(const CBattleInfoCallback & battle, int sta
 
 	bool canUseGate = false;
 	auto dbState = battle.battleGetGateState();
-	if(battle.battleGetSiegeLevel() > 0 && curStack->unitSide() == BattleSide::DEFENDER &&
+	if(battle.battleGetFortifications().wallsHealth > 0 && curStack->unitSide() == BattleSide::DEFENDER &&
 		dbState != EGateState::DESTROYED &&
 		dbState != EGateState::BLOCKED)
 	{

+ 8 - 2
server/battles/BattleFlowProcessor.cpp

@@ -19,6 +19,7 @@
 #include "../../lib/GameSettings.h"
 #include "../../lib/battle/CBattleInfoCallback.h"
 #include "../../lib/battle/IBattleState.h"
+#include "../../lib/entities/building/TownFortifications.h"
 #include "../../lib/gameState/CGameState.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
 #include "../../lib/networkPacks/PacksForClientBattle.h"
@@ -114,13 +115,18 @@ void BattleFlowProcessor::tryPlaceMoats(const CBattleInfoCallback & battle)
 {
 	const auto * town = battle.battleGetDefendedTown();
 
+	if (!town)
+		return;
+
+	const auto & fortifications = town->fortificationsLevel();
+
 	//Moat should be initialized here, because only here we can use spellcasting
-	if (town && town->fortLevel() >= CGTownInstance::CITADEL)
+	if (fortifications.hasMoat)
 	{
 		const auto * h = battle.battleGetFightingHero(BattleSide::DEFENDER);
 		const auto * actualCaster = h ? static_cast<const spells::Caster*>(h) : nullptr;
 		auto moatCaster = spells::SilentCaster(battle.sideToPlayer(BattleSide::DEFENDER), actualCaster);
-		auto cast = spells::BattleCast(&battle, &moatCaster, spells::Mode::PASSIVE, town->town->moatAbility.toSpell());
+		auto cast = spells::BattleCast(&battle, &moatCaster, spells::Mode::PASSIVE, fortifications.moatSpell.toSpell());
 		auto target = spells::Target();
 		cast.cast(gameHandler->spellEnv, target);
 	}

+ 2 - 1
server/battles/BattleProcessor.cpp

@@ -23,6 +23,7 @@
 #include "../../lib/battle/CBattleInfoCallback.h"
 #include "../../lib/battle/CObstacleInstance.h"
 #include "../../lib/battle/BattleInfo.h"
+#include "../../lib/entities/building/TownFortifications.h"
 #include "../../lib/gameState/CGameState.h"
 #include "../../lib/mapping/CMap.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
@@ -192,7 +193,7 @@ BattleID BattleProcessor::setupBattle(int3 tile, BattleSideArray<const CArmedIns
 bool BattleProcessor::checkBattleStateChanges(const CBattleInfoCallback & battle)
 {
 	//check if drawbridge state need to be changes
-	if (battle.battleGetSiegeLevel() > 0)
+	if (battle.battleGetFortifications().wallsHealth > 0)
 		updateGateState(battle);
 
 	if (resultProcessor->battleIsEnding(battle))