Posts: 7,602
Threads: 75
Joined: Jan 2018
Have you ever wondered how a feature of civ 4 actually worked? Did you ever scour the internet for a nagging question about civ 4 in your mind? Well, this mini-series might be the right thing to do.
The initial idea for this started in PB43 when Mr. Cairo wondered about and investigated some effects around collateral damage and immunity to collateral. I told him and other players from PB43 that I will look up the actual implementation. Because this might be interesting to a broader viewership and because there might be more unanswered questions, I started this thread. Feel free to ask anything about how civ4 works and I will try to investigate and answer those questions. But a warning, don't expect a quick answer.
April 24th, 2020, 23:20
(This post was last modified: August 10th, 2023, 04:18 by Charriu.)
Posts: 7,602
Threads: 75
Joined: Jan 2018
Posts: 7,602
Threads: 75
Joined: Jan 2018
The first big question would be from PB43
What units are eligible for collateral damage and how are they selected?
The important part to this question is found in the CvUnit.cpp and in there in the method collateralCombat:
Code: void CvUnit::collateralCombat(const CvPlot* pPlot, CvUnit* pSkipUnit)
{
CLLNode<IDInfo>* pUnitNode;
CvUnit* pLoopUnit;
CvUnit* pBestUnit;
CvWString szBuffer;
int iTheirStrength;
int iStrengthFactor;
int iCollateralDamage;
int iUnitDamage;
int iDamageCount;
int iPossibleTargets;
int iCount;
int iValue;
int iBestValue;
std::map<CvUnit*, int> mapUnitDamage;
std::map<CvUnit*, int>::iterator it;
int iCollateralStrength = (getDomainType() == DOMAIN_AIR ? airBaseCombatStr() : baseCombatStr()) * collateralDamage() / 100;
// UNOFFICIAL_PATCH Start
// * Barrage promotions made working again on Tanks and other units with no base collateral ability
if (iCollateralStrength == 0 && getExtraCollateralDamage() == 0)
// UNOFFICIAL_PATCH End
{
return;
}
iPossibleTargets = std::min((pPlot->getNumVisibleEnemyDefenders(this) - 1), collateralDamageMaxUnits());
pUnitNode = pPlot->headUnitNode();
while (pUnitNode != NULL)
{
pLoopUnit = ::getUnit(pUnitNode->m_data);
pUnitNode = pPlot->nextUnitNode(pUnitNode);
if (pLoopUnit != pSkipUnit)
{
if (isEnemy(pLoopUnit->getTeam(), pPlot))
{
if (!(pLoopUnit->isInvisible(getTeam(), false)))
{
if (pLoopUnit->canDefend())
{
iValue = (1 + GC.getGameINLINE().getSorenRandNum(10000, "Collateral Damage"));
iValue *= pLoopUnit->currHitPoints();
mapUnitDamage[pLoopUnit] = iValue;
}
}
}
}
}
CvCity* pCity = NULL;
if (getDomainType() == DOMAIN_AIR)
{
pCity = pPlot->getPlotCity();
}
iDamageCount = 0;
iCount = 0;
while (iCount < iPossibleTargets)
{
iBestValue = 0;
pBestUnit = NULL;
for (it = mapUnitDamage.begin(); it != mapUnitDamage.end(); it++)
{
if (it->second > iBestValue)
{
iBestValue = it->second;
pBestUnit = it->first;
}
}
if (pBestUnit != NULL)
{
mapUnitDamage.erase(pBestUnit);
if (NO_UNITCOMBAT == getUnitCombatType() || !pBestUnit->getUnitInfo().getUnitCombatCollateralImmune(getUnitCombatType()))
{
iTheirStrength = pBestUnit->baseCombatStr();
iStrengthFactor = ((iCollateralStrength + iTheirStrength + 1) / 2);
iCollateralDamage = (GC.getDefineINT("COLLATERAL_COMBAT_DAMAGE") * (iCollateralStrength + iStrengthFactor)) / (iTheirStrength + iStrengthFactor);
iCollateralDamage *= 100 + getExtraCollateralDamage();
iCollateralDamage *= std::max(0, 100 - pBestUnit->getCollateralDamageProtection());
iCollateralDamage /= 100;
if (pCity != NULL)
{
iCollateralDamage *= 100 + pCity->getAirModifier();
iCollateralDamage /= 100;
}
iCollateralDamage /= 100;
iCollateralDamage = std::max(0, iCollateralDamage);
int iMaxDamage = std::min(collateralDamageLimit(), (collateralDamageLimit() * (iCollateralStrength + iStrengthFactor)) / (iTheirStrength + iStrengthFactor));
iUnitDamage = std::max(pBestUnit->getDamage(), std::min(pBestUnit->getDamage() + iCollateralDamage, iMaxDamage));
if (pBestUnit->getDamage() != iUnitDamage)
{
pBestUnit->setDamage(iUnitDamage, getOwnerINLINE());
iDamageCount++;
}
}
iCount++;
}
else
{
break;
}
}
if (iDamageCount > 0)
{
szBuffer = gDLL->getText("TXT_KEY_MISC_YOU_SUFFER_COL_DMG", iDamageCount);
gDLL->getInterfaceIFace()->addMessage(pSkipUnit->getOwnerINLINE(), (pSkipUnit->getDomainType() != DOMAIN_AIR), GC.getEVENT_MESSAGE_TIME(), szBuffer, "AS2D_COLLATERAL", MESSAGE_TYPE_INFO, getButton(), (ColorTypes)GC.getInfoTypeForString("COLOR_RED"), pSkipUnit->getX_INLINE(), pSkipUnit->getY_INLINE(), true, true);
szBuffer = gDLL->getText("TXT_KEY_MISC_YOU_INFLICT_COL_DMG", getNameKey(), iDamageCount);
gDLL->getInterfaceIFace()->addMessage(getOwnerINLINE(), true, GC.getEVENT_MESSAGE_TIME(), szBuffer, "AS2D_COLLATERAL", MESSAGE_TYPE_INFO, getButton(), (ColorTypes)GC.getInfoTypeForString("COLOR_GREEN"), pSkipUnit->getX_INLINE(), pSkipUnit->getY_INLINE());
}
}
There are two parts in the code that are important to the question. First this:
Code: iPossibleTargets = std::min((pPlot->getNumVisibleEnemyDefenders(this) - 1), collateralDamageMaxUnits());
pUnitNode = pPlot->headUnitNode();
while (pUnitNode != NULL)
{
pLoopUnit = ::getUnit(pUnitNode->m_data);
pUnitNode = pPlot->nextUnitNode(pUnitNode);
if (pLoopUnit != pSkipUnit)
{
if (isEnemy(pLoopUnit->getTeam(), pPlot))
{
if (!(pLoopUnit->isInvisible(getTeam(), false)))
{
if (pLoopUnit->canDefend())
{
iValue = (1 + GC.getGameINLINE().getSorenRandNum(10000, "Collateral Damage"));
iValue *= pLoopUnit->currHitPoints();
mapUnitDamage[pLoopUnit] = iValue;
}
}
}
}
}
Let's dissect this. The first line with "iPossibleTargets" is rather easy. This is the maximum number of units that can be damaged by collateral and it's what you already know. It's the limit set by the attacking unit or the amount of visible enemy units minus the original defender, which ever number is smaller.
Next we go through all the defending units. There are some checks that remove units from the pool:
- if (pLoopUnit != pSkipUnit): pSkipUnit is the actually defending unit. It is not eligible for collateral
- if (isEnemy(pLoopUnit->getTeam(), pPlot)): The unit must be an enemy unit, meaning we are at war with the owner of the unit
- if (!(pLoopUnit->isInvisible(getTeam(), false))): Only visible units are allowed.
- if (pLoopUnit->canDefend()): Among others, this canDefend checks if the defending units combat value is > 0. That way units like settler and worker are not eligible for collateral
After all these checks every unit is assigned a random value between 1 and 10001. This value is then multiplied by their current hitpoints. That way damaged units are still eligible, but it's less likely they are selected.
So now we know which units are eligible and we now need to pick the units and do damage to them. This happens here:
Code: while (iCount < iPossibleTargets)
{
iBestValue = 0;
pBestUnit = NULL;
for (it = mapUnitDamage.begin(); it != mapUnitDamage.end(); it++)
{
if (it->second > iBestValue)
{
iBestValue = it->second;
pBestUnit = it->first;
}
}
if (pBestUnit != NULL)
{
mapUnitDamage.erase(pBestUnit);
if (NO_UNITCOMBAT == getUnitCombatType() || !pBestUnit->getUnitInfo().getUnitCombatCollateralImmune(getUnitCombatType()))
{
do the actual damage
}
iCount++;
}
else
{
break;
}
}
Here we just select the unit with the highest random value and try to do damage to it. We do this to as many units as determined by iPossibleTargets. But there are some important things before we do the actual damage:
- We guarantee that every unit is only considered once. This is the mapUnitDamage.erase(pBestUnit); part.
- We do this check before if (NO_UNITCOMBAT == getUnitCombatType() || !pBestUnit->getUnitInfo().getUnitCombatCollateralImmune(getUnitCombatType()))
Now that last part is important. The "||" is an "or" in C++. That first check before the or is a simple safety check for the unit type. As far as I know, no unit has the UnitType NO_UNITCOMBAT, but if it would have this, would not receive damage. It's the last part that is important. Here we check if the selected unit is immune. If true it does not receive damage. Most importantly the counter iCount is counted up outside of this check. This means that units that are NO_UNITCOMBAT or immune, are considered targets without receiving damage.
Lastly there is the code for the actual damage:
Code: iTheirStrength = pBestUnit->baseCombatStr();
iStrengthFactor = ((iCollateralStrength + iTheirStrength + 1) / 2);
iCollateralDamage = (GC.getDefineINT("COLLATERAL_COMBAT_DAMAGE") * (iCollateralStrength + iStrengthFactor)) / (iTheirStrength + iStrengthFactor);
iCollateralDamage *= 100 + getExtraCollateralDamage();
iCollateralDamage *= std::max(0, 100 - pBestUnit->getCollateralDamageProtection());
iCollateralDamage /= 100;
if (pCity != NULL)
{
iCollateralDamage *= 100 + pCity->getAirModifier();
iCollateralDamage /= 100;
}
iCollateralDamage /= 100;
iCollateralDamage = std::max(0, iCollateralDamage);
int iMaxDamage = std::min(collateralDamageLimit(), (collateralDamageLimit() * (iCollateralStrength + iStrengthFactor)) / (iTheirStrength + iStrengthFactor));
iUnitDamage = std::max(pBestUnit->getDamage(), std::min(pBestUnit->getDamage() + iCollateralDamage, iMaxDamage));
if (pBestUnit->getDamage() != iUnitDamage)
{
pBestUnit->setDamage(iUnitDamage, getOwnerINLINE());
iDamageCount++;
}
The only important part here is that units that are already damaged and under the damage limit, are counted as targets, but receive no damage.
Summary
So to summarize, units that fullfill these criteria are eligible for collateral damage:
- Not the actually defending unit
- Defending units of players, whom we are at war with
- Visible units
- Combat strength > 0
And most importantly units that fullfill these criteria are considered targets, but don't receive damage, therefore reducing the effectiveness of collateral damage, just like Mr. Cairo already discovered
- Units with Type NO_COMBATUNIT (there is none in the game)
- Collateral immune units
- Damaged units that are under the damage limit. (but it's less likely that they are selected because of their reduced HP)
Posts: 6,703
Threads: 131
Joined: Mar 2004
Nice thread and great writeup on that.
I remember doing something similar years ago on the details of granary mechanics. If you grow while building a settler or worker (only possible if the food box shrinks, say by whipping), the 2 food the new citizen consumes are subtracted out of the hammers applied to the settler/worker that same turn. The city screen doesn't account for that ahead of time so you end up with 2 hammers less than it showed. This drove someone crazy in some early reporting thread until I traced through the code line by line to figure it out.
Posts: 4,671
Threads: 36
Joined: Feb 2013
(April 25th, 2020, 00:00)Charriu Wrote: After all these checks every unit is assigned a random value between 1 and 10001. This value is then multiplied by their current hitpoints. That way damaged units are still eligible, but it's less likely they are selected. So, does it mean that units that are immune to collateral damage will become more likely to be selected with each subsequent collateral attack? Because all other units would get damaged but immune units would remain at full health? I actually didn't know that, so thanks!
Posts: 7,602
Threads: 75
Joined: Jan 2018
You are absolutely right about that.
July 10th, 2020, 09:56
(This post was last modified: September 18th, 2020, 00:30 by Charriu.)
Posts: 7,602
Threads: 75
Joined: Jan 2018
Founding a religion in a specific city
In my reasoned game (PB52, don't worry spoiler-free) I planned to found a religion and wondered about these two questions:
1. How exactly does the game choose which city?
2. Does the game deceide on the holy city before or after that city grew?
I start with the second question as it's quicker to answer. The game first decides on the holy city and after that decision the city grows. That's because the holy city decision is done during the tech progress steps of the game and those are done before iterating through all cities.
With that out of the way back to the first question. There's already good knowledge about it and it involves random numbers, the cities population and if the palace is present. First here is the code:
Code: for (pLoopCity = firstCity(&iLoop); pLoopCity != NULL; pLoopCity = nextCity(&iLoop))
{
if (!bStarting || !(pLoopCity->isHolyCity()))
{
iValue = 10;
iValue += pLoopCity->getPopulation();
iValue += GC.getGameINLINE().getSorenRandNum(GC.getDefineINT("FOUND_RELIGION_CITY_RAND"), "Found Religion");
iValue /= (pLoopCity->getReligionCount() + 1);
if (pLoopCity->isCapital())
{
iValue /= 8;
}
iValue = std::max(1, iValue);
if (iValue > iBestValue)
{
iBestValue = iValue;
pBestCity = pLoopCity;
}
}
}
So what does this mean in plain old english. We iterate through all cities in order of founding. Then we do the following
1. The city starts with a base value of 10
2. We add the cities population to that value
3. We add a random number between 0 and 9
4. We divide that number through the amount of religions present in the city + 1 (that last one so that we don't divide by zero)
5. If this city is the capital we divide the number by 8
6. We guarantee that this number is at least 1
7. We compare this value with the last best value if it's higher, then this cities value is the new best value and the city is more likely to be choosen.
Summary
So what do we learn from this:
1. The algorithm favors earlier founded cities, simply because they can set a higher best value earlier
2. Population difference isn't really that big of a game changer, especially if the difference is very small.
Posts: 1,948
Threads: 19
Joined: Apr 2019
So it is basically impossible to get it in the capital even if you get the lucky 10 roll, and has say 4 pop.
"I know that Kilpatrick is a hell of a damned fool, but I want just that sort of man to command my cavalry on this expedition."
- William Tecumseh Sherman
Posts: 7,602
Threads: 75
Joined: Jan 2018
Even with 20 pop it's impossible. The only way is to have only one city.
July 10th, 2020, 11:53
(This post was last modified: July 10th, 2020, 13:05 by T-hawk.)
Posts: 6,703
Threads: 131
Joined: Mar 2004
Wow, I always misunderstood that. I thought the numbers were weights, so it could still be the capital, just 1/8 as often. It's highest-city-wins rather than weights, and so dividing the capital by 8 means it will never beat any city's base value of 10 (it would need ~80 population.) I wonder if Firaxis (Soren) themselves understood what they were doing here, that the capital is shut out rather than 1/8 likely; that 8 value really has no effective meaning.
Your last point really means "earlier city wins a tie" after adding the random(10) to each. That alone is significant, 55%-45% in favor of the earlier city among two with the same population. And population does have a significant effect, even if sometimes overstated. Just one more population for the earlier city shifts it to 64%-36%, almost two-to-one. Two more pop becomes 72%-28%, almost three-to-one. And the earlier city will be likely to have grown to more population.
|