Back in PB49 Fintourist mentioned something that grabbed my attention:
(July 8th, 2020, 16:47)Fintourist Wrote: Speaking of bombarding, we are suffering here from a weird bug (I think it’s a bug). When catapults bombard against city walls they should reduce the defenses by 4 % each bombard. Right? Well that works exactly so, when the defending city has 20 %, 40 % or 80 % culture. However, when the city has 60 % culture, the cats only reduce the defenses by 3.7% or something like that. In essence, our 15 cats don’t get the defenses down to zero in 1 turn and for some reason 6 % defensive bonus remains.. No clue what causes that, but this forced us to move in one turn early as some of our cats have to bombard twice if we want to get rid of all defensive bonuses.
So
How exactly is bombarding calculated?
To answer this we have to look in different source files. First in CvUnit.cpp and there in the
bombard method. This is the interesting part there:
Code:
int iBombardModifier = 0;
if (!ignoreBuildingDefense())
{
iBombardModifier -= pBombardCity->getBuildingBombardDefense();
}
pBombardCity->changeDefenseModifier(-(bombardRate() * std::max(0, 100 + iBombardModifier)) / 100);
- First we get the bombardModifier, which is the 50% reduction from Walls and the 25% reduction from Castles. Of course this only happens if we use a catapult or trebuchet to bombard.
- We then call the changeDefenseModifer method of CvCity with the parameter bombardRate * reduction. This is the reduction we all expect so without Walls or Castles Cats do 8 and trebs do 16 bombard damage, with Walls 4 and 8 and lastly with Castle and Walls 2 and 4
Now we look at what is happening in CvCity:
Code:
void CvCity::changeDefenseModifier(int iChange)
{
if (iChange != 0)
{
int iTotalDefense = getTotalDefense(false);
if (iTotalDefense > 0)
{
changeDefenseDamage(-(GC.getMAX_CITY_DEFENSE_DAMAGE() * iChange) / iTotalDefense);
}
}
}
GetTotalDefense is the current maximum defense of the city. For example if you had the second border pop the city would be at 40% defense. Note that this is the maximum defense value and is not the reduced defense value due to bombard. For the purpose of a single bombardement this value is constant.
The only other interesting bit is the call of the changeDefenseDamage method with parameters:
-(GC.getMAX_CITY_DEFENSE_DAMAGE() * iChange) / iTotalDefense
GC.getMAX_CITY_DEFENSE_DAMAGE() is a constant value set at 100
iChange is our previously caluclated bombard damage (8/4/2 for cats and 16/8/4 for trebs)
iTotalDefense is the previously mentioned GetTotalDefense
So if we assume a second border pop and a bombardement by a catapult we get the following calculation:
-(100 * 8) / 40 = -20
Now we go into the method
changeDefenseDamage in CvCity:
Code:
void CvCity::changeDefenseDamage(int iChange)
{
if (iChange != 0)
{
m_iDefenseDamage = range((m_iDefenseDamage + iChange), 0, GC.getMAX_CITY_DEFENSE_DAMAGE());
if (iChange > 0)
{
setBombarded(true);
}
setInfoDirty(true);
plot()->plotAction(PUF_makeInfoBarDirty);
}
}
The only interesting part here is:
Code:
m_iDefenseDamage = range((m_iDefenseDamage + iChange), 0, GC.getMAX_CITY_DEFENSE_DAMAGE());
Here we just add up the new damage to the previously applied damage and ensure that the value is between 0 and 100. Basically what this means is that cities have a (hidden) HP bar just like units for bombardement. If we go back to our previous example. We apply 20 damage everytime we bombard. So with 5 bombardements we reach the maximum damage of 100 and wouldn't you know it, we also only need 5 catapult bombardements to reduce a 40% city to zero.
Now the last part to the whole riddle is the actual conversion of this HP bar into the defense value. This happens in CvCity in the method
getDefenseModifier:
Code:
int CvCity::getDefenseModifier(bool bIgnoreBuilding) const
{
if (isOccupation())
{
return 0;
}
return ((getTotalDefense(bIgnoreBuilding) * (GC.getMAX_CITY_DEFENSE_DAMAGE() - getDefenseDamage())) / GC.getMAX_CITY_DEFENSE_DAMAGE());
}
We once again see values that we already know by now like getTotalDefense, GC.getMAX_CITY_DEFENSE_DAMAGE(). The new value
getDefenseDamage() is our accumulated damage. So let's go back to our previous example and let's say we bombarded our 40% city with three catapults, which results in 60 damage. The formula now gives this:
(40 * (100 - 60)) / 100 = 16
which is exactly the value we see in-game.
Now going back all to Fintourists comment about 60% defense. Why exactly does this strange behaviour happen? Well that's because of the wonderful thing called integer calculation. And the first time we hit that problem is in the
changeDefenseModifier method. If get back the formula from there:
Code:
changeDefenseDamage(-(GC.getMAX_CITY_DEFENSE_DAMAGE() * iChange) / iTotalDefense);
and put in a 60% culture defense and a catapult bombarding without any Walls, we get the following calculation:
-(100 * 8) / 60 = 13.333333
Now because of integer calculation this will not apply 13.3333 damage to the city, but rather 13. Basically we are missing 1 point of damage every third bombardement.
The next step we hit integer calculation problems is in our final calculation in
getDefenseModifier:
Code:
return ((getTotalDefense(bIgnoreBuilding) * (GC.getMAX_CITY_DEFENSE_DAMAGE() - getDefenseDamage())) / GC.getMAX_CITY_DEFENSE_DAMAGE());
Here we quite often hit a non integer number which is exactly the moment, where Fintourist noticed the strange behavior around 60% culture defense. 60% culture defense is actually the only culture defense, where this quirk is noticable with one major exception. This exception is the wonder Chichen Itza, which increases the culture defense in every city by 25%. With this we can achieve defense values of:
- 25%
- 45%
- 65%
- 75%
- 85%
- 105%
- 125%
With the exception of 25% every other culture defense provided by this wonder hits these strange integer calculation problems. What this means is that not only does Chichen Itza increase your culture defense, it also provides a tiny bombard defense bonus thanks to some quirky math.
Summary
I initially investigated this so that I could bugfix the behaviour, but now that I understand the whole calculation, I'm somewhat hesitant. The big misunderstanding in the reduction value of Walls and Castles is that one would assume that with Walls one reduces the defense by 4% instead of 8% with a catapult. In actuality you only decrease the damage you do to the cities HP. The fact that for the most time this also results in a same reduction of defense is just lucky. The other interesting factor in hear is the hidden bonus of Chichen Itza.