I´ve read the forums, and I´ve seen that there is much speculation based on incomplete information on how the combat system works, leading to much flawed considerations. So, what I propose in this post is that we look at the combat system at the code level, so you guys get solid facts to decide on what units and promotions you use.
Right now, lets see where you can find CIV core dll, that will give you all the rules, written in C++ language. They are at the installation folder:
They have a lot of Cpp files. The files starting with Cy are python interfaces, we are not interested in them. So lets look at the Cv* files:
There is a file call CvUnit. This is the file where all unit actions and info, including combat calculations, are made. There is a method in there:
All right, let's stop here: what in the heck is this currCombatStr?
It's in this code:
That is, the current combat strength is your max combat strength adjusted to the hit points that you have.
All right, I'm speculating here. After all, what in the heck is maxCombatStr?
It's here:
What in the heck is this? It's a property, saved and load with the unit. The starting value of this surely is zero. But thats come clear after you see the promotion system: they are used for giving the promotions bonus...
... continuing on the maxCombatStr calculation... remember that pPlot is null right now and pAttacker is also null ... also, we are calling this from the method “resolveCombat” that clearly is a situation where this is the current attacker. This makes all the code of maxCombatStr be basically skipped, so it all goes to
Where baseCombatStr is the strength of the attacker (this).
the result is, at last:
return std::max(1, iCombat);
That is: maxCombatStr for the attacker is his strength multiplied by 100. Unless that pesky modifier ,extraCombatPercent, is different than zero.
We know now what that iAttackerStrength has at the resolveCombat code. Now let's see iAttackerFirepower; that goes on this method:
int iAttackerFirepower = currFirepower(NULL, NULL);
this is the method:
So, the firepower of a almost dead attacker is half the firepower of a 100% healthy unit. Interesting...
Back to the resolveCombat code:
... just after the iAttackerStrength and iAttackerFirepower calculations ...
Let's see what the getDefenderCombatValues does:
Ah... for their strength (the defender strength), they inform the plot and the attacker... This makes the whole maxCombatStr calculation different:
if the defender is an animal, the attacker get a bonus, depending on the attacker being human or AI. Here I'm speculating: I think that these are the values of the iAnimalBonus and the iAIAnimalBonus that you get at the GameInfo/CIV4HandicapInfo.xml. So, for prince level, they would be: -30 for humans attacking animals and -70 for AI attacking animals.
So... as these modifier get as divisor at the end of the game, the AI get a greater bonus when fighting animals... cute. That's why those darn AI scouts keep surviving bears...
As you can guess, and I will let you see the code for this, the modifiers get reversed if that's an Animal attacking a unit.
I won't put the code in here. But the same calculation is made if the attacking/defender is a barbarian; in the XML the values would be iBarbarianBonus and iAIBarbarianBonus. They are at prince level, -5 for humans and -40 for AI. So, the AI get a big advantage when fighting barbs.
Then they get the plot defensive value (terrain values: hills, forests, walls, city culture, etc) and add to the iModifier. Unless, of course, the unit doen't receive defensive bonuses.
Well, making a long story short, all bonuses/onuses of the attacker and the defender get summed up with the proper signals (a bonus of the attacker is of course an onus to the defender) at the defender side, and the following calculation is made:
This leads to some interesting conclusions:
I've seen people making calcs saying for instance that a dog soldier is better than an axeman because the bonus of the dog soldier is 100%, so 4 * 2 = 8, against the axeman, where the bonus is 5 * 1,5 = 7,5.
You now know that that calculation is not exactly right (even more, as we will see later...)
A dog soldier attacking an axeman in clear terrain:
attacker bonus: 100%; defender bonus: 50%; the modifiers are stacked up at the defender, resulting in -50 to the defender, making the combat 4 against 5 / 1,5, that is, 4 against 3,333; better than 8 against 7,5 that people usually make. Yes, that axeman is probably toast in clear terrain...
Continuing on the analysis of getDefenderCombatValues:
This function has calc'ed iTheirStrength and iTheirFirepower. Then they calc iTheirOdds:
If you get it right, yes, this indicates the odds of the defender winning a round.
But look at it: when dueling barbs for the first "n" times, the odds get capped at 90% if I'm the attacker and 10% if I'm the defender.
The Constant “COMBAT_DIE_SIDES” is 1000, as showed on the xml GlobalDefines.xml at the root of the Assets/XML folder.
Wins vs barbs is 1 at prince level. This means that they put a capped 10% chance of barbarians winning against me if I never had won a struggle against barbs. Neat...
Now, continuing the getDefenderCombatValues code, we get to the firepower thing:
COMBAT_DAMAGE = (as you probably have guessed) 20. Look at the globalDefines.xml.
This is the damage I get with each round won. And this at least is 1.
So, let's calc the dog soldier x axeman thing again:
The attacker get strength 400; the defender get strength 333. The attacker has firepower 400 and the defender 333. The strength factor gets to be (400+333+1)/2 = 367.
So ourDamage gets to be 20*(333+367)/(400+367). So we get to get 17 damage. Our dog soldier gets only 17 damage each turn. So we can lose 5 rounds and still survive.
TheirDamage get to be 20*(400+367)/(333+367). So they get 21 damage. If they lose 5 turns, they DIE.
And the defender chances on winning a round are (333)/(333+400) = 45,4%. Plus a free lose that we have, they are really toast.
Back to the resolveCombat function, look at this:
Hey, that getCombatOdds seems to be the odds shown at the civ screen!
All you guys that think that the odds are bad calculated, well, go for it...
Moving on... they calc the collateralDamage:
At the civ4UnitInfos.xml we have this values for the catapult
100
50
6
So, the collateral strength for a catapult would be 5.
They throw some dice in here, deciding what units will be eligible for damage:
See, this iValue is used at the unit selection phase: the biggest values will be the damage units. So, the units with more hitpoints get more chance to be the damaged ones. But, other units can be damaged as well. The defender is the pSkipUnit: it is not eligible for collateral damage.
Look that they don't look if the defender is immune to collateral damage. That is, catapults are eligible to receive collateral damage (zero, in this case): they get in the count!
The damage is calculated as follows:
Their strength is based on baseCombatStr, so the defense bonuses simply doesn't count.
The collateral combat damage constant is 10, as written in the “GlobalDefines.xml”.
Let's see this: A catapult attacks a stack of six longbowman:
The collateral damage to each of them is:
iStrengthFactor = ((500 + 600 + 1)/2) = 550
iCollateralDamage = 10 * (500+550)/(600+550) = 9
But some of the archers get drill2 (-20% collateral damage). So, iCollateralDamage = 9 – 9*20/100 = 9-1 = 8. These get only 8 damage.
ImaxDamage = 50*(500+550)/(600+550), so ImaxDamage = 45.
No, we can't put those longbowman below 55 hit points...
Some interesting conclusions:
that's not really a good idea to counter that rifleman army with a lot of catapults;
catapults are good defenses against collateral damage...
damaged catapults are as good as new catapults when doing collateral damage.
Continuing the resolveCombat...
The code is a little too big, you can follow just by reading. I will only coment it here:
The first strikes are calculated before this function call, at the setCombatUnit function; basically, they see if the opponent is immune to first strikes. If not, they sum the firstStrike() stat of the unit and a random value saying how much extra first strikes the unit will get, between 0 and the extra first strikes unit value number.
So, if the unit has 1 extra first strike chance. There will be a 50% chance that it will have 1 first strike.
If the unit has 2 extra first strikes chance there will be equal chances that it will have 0, 1 or 2 first strikes. So, it will have 1 extra first strike, on the average.
At each round the available first strikes for the attacker and defender are reduced by 1. If the loser of the round is out of first strikes, then it will get damage, the amount calculated at the getDefenderCombatValues.
If the loser is the defender, the amount of damage is capped at the iCombatLimit attribute of the attacker (at the CIV4UnitInfos.xml).
If the attacker has flanking strike and it survives combat, the flankingStrikeCombat function is activated:
The idea is that if the defender is out of city squares, all catapults get attacked by one round by the defender, with the strength the attacker had before the beginning of combat.
So, that's it. The combat system. The end...
Right now, lets see where you can find CIV core dll, that will give you all the rules, written in C++ language. They are at the installation folder:
Code:
[]Civ folder []Beyond the Sword []Assets []CvGameCoreDLL
There is a file call CvUnit. This is the file where all unit actions and info, including combat calculations, are made. There is a method in there:
Code:
void CvUnit::resolveCombat(CvUnit* pDefender, CvPlot* pPlot, CvBattleDefinition& kBattle) { CombatDetails cdAttackerDetails; CombatDetails cdDefenderDetails; int iAttackerStrength = currCombatStr(NULL, NULL, &cdAttackerDetails); int iAttackerFirepower = currFirepower(NULL, NULL); int iDefenderStrength;
It's in this code:
Code:
int CvUnit::currCombatStr(const CvPlot* pPlot, const CvUnit* pAttacker, CombatDetails* pCombatDetails) const { return ((maxCombatStr(pPlot, pAttacker, pCombatDetails) * currHitPoints()) / maxHitPoints()); }
All right, I'm speculating here. After all, what in the heck is maxCombatStr?
It's here:
Code:
int CvUnit::maxCombatStr(const CvPlot* pPlot, const CvUnit* pAttacker, CombatDetails* pCombatDetails) const { ... iExtraModifier = getExtraCombatPercent();
... continuing on the maxCombatStr calculation... remember that pPlot is null right now and pAttacker is also null ... also, we are calling this from the method “resolveCombat” that clearly is a situation where this is the current attacker. This makes all the code of maxCombatStr be basically skipped, so it all goes to
Code:
else { iCombat = ((baseCombatStr() * 10000) / (100 - iModifier)); }
the result is, at last:
return std::max(1, iCombat);
That is: maxCombatStr for the attacker is his strength multiplied by 100. Unless that pesky modifier ,extraCombatPercent, is different than zero.
We know now what that iAttackerStrength has at the resolveCombat code. Now let's see iAttackerFirepower; that goes on this method:
int iAttackerFirepower = currFirepower(NULL, NULL);
this is the method:
Code:
int CvUnit::currFirepower(const CvPlot* pPlot, const CvUnit* pAttacker) const { return ((maxCombatStr(pPlot, pAttacker) + currCombatStr(pPlot, pAttacker) + 1) / 2); }
Back to the resolveCombat code:
... just after the iAttackerStrength and iAttackerFirepower calculations ...
Code:
getDefenderCombatValues(*pDefender, pPlot, iAttackerStrength, iAttackerFirepower, iDefenderOdds, iDefenderStrength, iAttackerDamage, iDefenderDamage, &cdDefenderDetails);
Code:
void CvUnit::getDefenderCombatValues(CvUnit& kDefender, const CvPlot* pPlot, int iOurStrength, int iOurFirepower, int& iTheirOdds, int& iTheirStrength, int& iOurDamage, int& iTheirDamage, CombatDetails* pTheirDetails) const { iTheirStrength = kDefender.currCombatStr(pPlot, this, pTheirDetails); int iTheirFirepower = kDefender.currFirepower(pPlot, this);
Code:
int CvUnit::maxCombatStr(const CvPlot* pPlot, const CvUnit* pAttacker, CombatDetails* pCombatDetails) const { ... if (pAttacker != NULL) { if (isAnimal()) { if (pAttacker->isHuman()) { iExtraModifier = GC.getHandicapInfo( GC.getGameINLINE().getHandicapType()) .getAnimalCombatModifier(); iModifier += iExtraModifier; if (pCombatDetails != NULL) { pCombatDetails->iAnimalCombatModifierTA = iExtraModifier; } } else { iExtraModifier = GC.getHandicapInfo( GC.getGameINLINE().getHandicapType()) .getAIAnimalCombatModifier(); iModifier += iExtraModifier; if (pCombatDetails != NULL) { pCombatDetails->iAIAnimalCombatModifierTA = iExtraModifier; } } }
So... as these modifier get as divisor at the end of the game, the AI get a greater bonus when fighting animals... cute. That's why those darn AI scouts keep surviving bears...
As you can guess, and I will let you see the code for this, the modifiers get reversed if that's an Animal attacking a unit.
I won't put the code in here. But the same calculation is made if the attacking/defender is a barbarian; in the XML the values would be iBarbarianBonus and iAIBarbarianBonus. They are at prince level, -5 for humans and -40 for AI. So, the AI get a big advantage when fighting barbs.
Then they get the plot defensive value (terrain values: hills, forests, walls, city culture, etc) and add to the iModifier. Unless, of course, the unit doen't receive defensive bonuses.
Well, making a long story short, all bonuses/onuses of the attacker and the defender get summed up with the proper signals (a bonus of the attacker is of course an onus to the defender) at the defender side, and the following calculation is made:
Code:
if (iModifier > 0) { iCombat = (baseCombatStr() * (iModifier + 100)); } else { iCombat = ((baseCombatStr() * 10000) / (100 - iModifier)); }
I've seen people making calcs saying for instance that a dog soldier is better than an axeman because the bonus of the dog soldier is 100%, so 4 * 2 = 8, against the axeman, where the bonus is 5 * 1,5 = 7,5.
You now know that that calculation is not exactly right (even more, as we will see later...)
A dog soldier attacking an axeman in clear terrain:
attacker bonus: 100%; defender bonus: 50%; the modifiers are stacked up at the defender, resulting in -50 to the defender, making the combat 4 against 5 / 1,5, that is, 4 against 3,333; better than 8 against 7,5 that people usually make. Yes, that axeman is probably toast in clear terrain...
Continuing on the analysis of getDefenderCombatValues:
This function has calc'ed iTheirStrength and iTheirFirepower. Then they calc iTheirOdds:
Code:
iTheirOdds = ((GC.getDefineINT("COMBAT_DIE_SIDES") * iTheirStrength) / (iOurStrength + iTheirStrength)); if (kDefender.isBarbarian()) { if (GET_PLAYER(getOwnerINLINE()) .getWinsVsBarbs() < GC.getHandicapInfo(GET_PLAYER(getOwnerINLINE()).getHandicapType()).getFreeWinsVsBarbs()) { iTheirOdds = std::min((10 * GC.getDefineINT("COMBAT_DIE_SIDES")) / 100, iTheirOdds); } } if (isBarbarian()) { if (GET_PLAYER(kDefender.getOwnerINLINE()).getWinsVsBarbs() < GC.getHandicapInfo(GET_PLAYER(kDefender.getOwnerINLINE()).getHandicapType()).getFreeWinsVsBarbs()) { iTheirOdds = std::max((90 * GC.getDefineINT("COMBAT_DIE_SIDES")) / 100, iTheirOdds); } }
But look at it: when dueling barbs for the first "n" times, the odds get capped at 90% if I'm the attacker and 10% if I'm the defender.
The Constant “COMBAT_DIE_SIDES” is 1000, as showed on the xml GlobalDefines.xml at the root of the Assets/XML folder.
Wins vs barbs is 1 at prince level. This means that they put a capped 10% chance of barbarians winning against me if I never had won a struggle against barbs. Neat...
Now, continuing the getDefenderCombatValues code, we get to the firepower thing:
Code:
int iStrengthFactor = ((iOurFirepower + iTheirFirepower + 1) / 2); iOurDamage = std::max(1, ((GC.getDefineINT("COMBAT_DAMAGE") * (iTheirFirepower + iStrengthFactor)) / (iOurFirepower + iStrengthFactor))); iTheirDamage = std::max(1, ((GC.getDefineINT("COMBAT_DAMAGE") * (iOurFirepower + iStrengthFactor)) / (iTheirFirepower + iStrengthFactor)));
This is the damage I get with each round won. And this at least is 1.
So, let's calc the dog soldier x axeman thing again:
The attacker get strength 400; the defender get strength 333. The attacker has firepower 400 and the defender 333. The strength factor gets to be (400+333+1)/2 = 367.
So ourDamage gets to be 20*(333+367)/(400+367). So we get to get 17 damage. Our dog soldier gets only 17 damage each turn. So we can lose 5 rounds and still survive.
TheirDamage get to be 20*(400+367)/(333+367). So they get 21 damage. If they lose 5 turns, they DIE.
And the defender chances on winning a round are (333)/(333+400) = 45,4%. Plus a free lose that we have, they are really toast.
Back to the resolveCombat function, look at this:
Code:
if (isHuman() || pDefender->isHuman()) { //Added ST CyArgsList pyArgsCD; pyArgsCD.add(gDLL->getPythonIFace()->makePythonObject(&cdAttackerDetails)); pyArgsCD.add(gDLL->getPythonIFace()->makePythonObject(&cdDefenderDetails)); pyArgsCD.add(getCombatOdds(this, pDefender)); gDLL->getEventReporterIFace()->genericEvent("combatLogCalc", pyArgsCD.makeFunctionArgs()); }
All you guys that think that the odds are bad calculated, well, go for it...
Moving on... they calc the collateralDamage:
Code:
collateralCombat(pPlot, pDefender); First, the collateral strength is calculated, as follows: iCollateralStrength = ((((getDomainType() == DOMAIN_AIR) ? airBaseCombatStr() : baseCombatStr()) * collateralDamage()) / 100);
So, the collateral strength for a catapult would be 5.
They throw some dice in here, deciding what units will be eligible for damage:
Code:
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; } } } }
Look that they don't look if the defender is immune to collateral damage. That is, catapults are eligible to receive collateral damage (zero, in this case): they get in the count!
The damage is calculated as follows:
Code:
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 -= (iCollateralDamage * pBestUnit->getCollateralDamageProtection()) / 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++;
The collateral combat damage constant is 10, as written in the “GlobalDefines.xml”.
Let's see this: A catapult attacks a stack of six longbowman:
The collateral damage to each of them is:
iStrengthFactor = ((500 + 600 + 1)/2) = 550
iCollateralDamage = 10 * (500+550)/(600+550) = 9
But some of the archers get drill2 (-20% collateral damage). So, iCollateralDamage = 9 – 9*20/100 = 9-1 = 8. These get only 8 damage.
ImaxDamage = 50*(500+550)/(600+550), so ImaxDamage = 45.
No, we can't put those longbowman below 55 hit points...
Some interesting conclusions:
that's not really a good idea to counter that rifleman army with a lot of catapults;
catapults are good defenses against collateral damage...
damaged catapults are as good as new catapults when doing collateral damage.
Continuing the resolveCombat...
The code is a little too big, you can follow just by reading. I will only coment it here:
The first strikes are calculated before this function call, at the setCombatUnit function; basically, they see if the opponent is immune to first strikes. If not, they sum the firstStrike() stat of the unit and a random value saying how much extra first strikes the unit will get, between 0 and the extra first strikes unit value number.
So, if the unit has 1 extra first strike chance. There will be a 50% chance that it will have 1 first strike.
If the unit has 2 extra first strikes chance there will be equal chances that it will have 0, 1 or 2 first strikes. So, it will have 1 extra first strike, on the average.
At each round the available first strikes for the attacker and defender are reduced by 1. If the loser of the round is out of first strikes, then it will get damage, the amount calculated at the getDefenderCombatValues.
If the loser is the defender, the amount of damage is capped at the iCombatLimit attribute of the attacker (at the CIV4UnitInfos.xml).
If the attacker has flanking strike and it survives combat, the flankingStrikeCombat function is activated:
The idea is that if the defender is out of city squares, all catapults get attacked by one round by the defender, with the strength the attacker had before the beginning of combat.
So, that's it. The combat system. The end...
Comment