1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-22 22:13:35 +02:00

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)
This commit is contained in:
Ivan Savenko 2024-08-28 19:33:56 +00:00
parent 247be94015
commit 36c1ed670f
33 changed files with 284 additions and 98 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
//determine how this building can be built. Possible values are:
// 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:
// 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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