As a French person I feel like it's my duty to explain strikes to you. - AdrienIer

Create an account  

 
Curious Civplayer - Mysteries of the DLL

How does inflation work in civ 4?

There are two important values we need to calculate the added inflation cost. The inflation rate and the Total cost before inflation.




The Total cost before inflation is rather simple. This is the sum of
  • Unit Cost
  • Unit Supply
  • City Maintenance
  • Civic Upkeep
For the inflation rate we look at CvPlayer and there into the calculateInflationRate method

Code:
int iTurns = ((GC.getGameINLINE().getGameTurn() + GC.getGameINLINE().getElapsedGameTurns()) / 2);

if (GC.getGameINLINE().getMaxTurns() > 0)
{
   iTurns = std::min(GC.getGameINLINE().getMaxTurns(), iTurns);
}

//Charriu Inflation Tech Alternative
if (GC.getGame().isOption(GAMEOPTION_INFLATION_TIED_TO_TECH))
{
   float techRatio = (float)getTotalTech() / (float)GC.getNumTechInfos();
   int techTurn = (int)(techRatio * GC.getGameINLINE().getMaxTurns());
   iTurns = std::max(techTurn, iTurns);
}

iTurns += GC.getGameSpeedInfo(GC.getGameINLINE().getGameSpeedType()).getInflationOffset();

if (iTurns <= 0)
{
   return 0;
}

int iInflationPerTurnTimes10000 = GC.getGameSpeedInfo(GC.getGameINLINE().getGameSpeedType()).getInflationPercent();
iInflationPerTurnTimes10000 *= GC.getHandicapInfo(getHandicapType()).getInflationPercent();
iInflationPerTurnTimes10000 /= 100;

int iModifier = m_iInflationModifier;
if (!isHuman() && !isBarbarian())
{
   int iAIModifier = GC.getHandicapInfo(GC.getGameINLINE().getHandicapType()).getAIInflationPercent();
   iAIModifier *= std::max(0, ((GC.getHandicapInfo(GC.getGameINLINE().getHandicapType()).getAIPerEraModifier() * getCurrentEra()) + 100));
   iAIModifier /= 100;

   iModifier += iAIModifier - 100;
}

iInflationPerTurnTimes10000 *= std::max(0, 100 + iModifier);
iInflationPerTurnTimes10000 /= 100;

// Keep up to second order terms in binomial series
int iRatePercent = (iTurns * iInflationPerTurnTimes10000) / 100;
iRatePercent += (iTurns * (iTurns - 1) * iInflationPerTurnTimes10000 * iInflationPerTurnTimes10000) / 2000000;

FAssert(iRatePercent >= 0);

return iRatePercent;

There's quiet a lot to unpack here. I will provide the numbers for the example from my screenshot above. So let's start with the first line:


Code:
int iTurns = ((GC.getGameINLINE().getGameTurn() + GC.getGameINLINE().getElapsedGameTurns()) / 2);


We start by calculating the base turn number. Now getElapsedGameTurns is rather easy, these are the truely elapsed turns. If you start an ancient game and press 'end turn' five times it will be 5 and if you start an medieval game and press 'end turn' five times it will be also be 5.
getGameTurn is a little bit more complicated. This is the turn number as shown in the UI. If you start an ancient game and press 'end turn' five times it will be 5, if you start an medieval game and press 'end turn' five times it will be 130.

Going back to my example. This was an ancient game. So we have this:

int iTurns = ((GC.getGameINLINE().getGameTurn() + GC.getGameINLINE().getElapsedGameTurns()) / 2);
=
(169 + 169) / 2
= 169

What follows next is a safety check, ensuring that the iTurn value doesn't get bigger then the maximum turn.

Now as you can see there is also code from my inflation game option:


Code:
//Charriu Inflation Tech Alternative
if (GC.getGame().isOption(GAMEOPTION_INFLATION_TIED_TO_TECH))
{
   float techRatio = (float)getTotalTech() / (float)GC.getNumTechInfos();
   int techTurn = (int)(techRatio * GC.getGameINLINE().getMaxTurns());
   iTurns = std::max(techTurn, iTurns);
}

I described this in my CtH log. Basically it calculates a turn based on your tech progress and takes whichever value is bigger for iTurn.

The next and also last step for the turn calculation is:


Code:
iTurns += GC.getGameSpeedInfo(GC.getGameINLINE().getGameSpeedType()).getInflationOffset();

Here we just add the inflation offset from the game speed. This offset guarantees that we have no inflation in during these early turns. On normal game speed this is -90.

In our example we therefore have iTurn at 79 now.



We now enter the second phase of the rate calculation and in here we calculate this omnious iInflationPerTurnTimes10000 value.


Code:
int iInflationPerTurnTimes10000 = GC.getGameSpeedInfo(GC.getGameINLINE().getGameSpeedType()).getInflationPercent();
iInflationPerTurnTimes10000 *= GC.getHandicapInfo(getHandicapType()).getInflationPercent();
iInflationPerTurnTimes10000 /= 100;

As you can see we get iInflationPercent from CIV4GameSpeedInfo.xml and multiplicate it with iInflationPercent  from CIV4HandicapInfo.xml before divide by 100.

In our example we are at normal speed and monarch difficulty. Therefore:

iInflationPerTurnTimes10000 = 30 * 100 / 100 = 30


Code:
int iModifier = m_iInflationModifier;


Here we get an inflationModifier. This is a modifier set by an event. In my example this is 0.


Code:
if (!isHuman() && !isBarbarian())
{
   int iAIModifier = GC.getHandicapInfo(GC.getGameINLINE().getHandicapType()).getAIInflationPercent();
   iAIModifier *= std::max(0, ((GC.getHandicapInfo(GC.getGameINLINE().getHandicapType()).getAIPerEraModifier() * getCurrentEra()) + 100));
   iAIModifier /= 100;

   iModifier += iAIModifier - 100;
}

Next up the AI gets their bonus modifier calculated. I won't concentrate too much on it. It should be mostly self-explanatory by now. This bonus gets added to iModifier.


Code:
iInflationPerTurnTimes10000 *= std::max(0, 100 + iModifier);
iInflationPerTurnTimes10000 /= 100;

Lastly we multiplicate our iModifier with our iInflationPerTurnTimes10000. In our example iModifier is 0 and therefore iInflationPerTurnTimes10000 stays at 30


Code:
// Keep up to second order terms in binomial series
int iRatePercent = (iTurns * iInflationPerTurnTimes10000) / 100;
iRatePercent += (iTurns * (iTurns - 1) * iInflationPerTurnTimes10000 * iInflationPerTurnTimes10000) / 2000000;

Now after all this preperation we get to the heart of the calculation: The actual inflationRate. This is basic math, so I just fill in my example numbers for you.

int iRatePercent = (79 * 30) / 100 = 23.7 = 23 because of integer calculation.

23 + (79 * (79 - 1) * 30 * 30) / 2000000 = 25.7729 = 25 because if integer calculation

As you can see we got the 25% we've already seen in the screenshot.

The final formula for the inflation cost is in another method which is just on line of code and boils down to this:

Total cost before inflation * std::max(0, (Inflation rate + 100))) / 100

In our example:

91 * std::max(0, (25 + 100))) / 100 = 113.75 = 113 because of integer calculation.

As you can see we have calculated the Total Expenses at the end. The inflation cost itself is now only the delta between Total Expenses and Total cost before inflation.

Summary

I will try to boil down the calculation to some basic formulas:

Turn = (GameTurn (UI) + elapsedTurns) / 2 + GameSpeed.inflationOffset
InflationPerTurnTimes10000 = GameSpeed.inflationPercent * Handicap.inflationPercent / 100 * (100 + eventModifier) / 100
InflationRate = ((Turn * InflationPerTurnTimes10000) / 100) + (Turn * (Turn - 1) * InflationPerTurnTimes10000 * InflationPerTurnTimes10000) / 2000000
TotalCostBeforeInflation = Unit Cost + Unit Supply + City Maintenance + Civic Upkeep
Total Expenses = (TotalCostBeforeInflation * calculateInflationRate() + 100) / 100;
Mods: RtR    CtH

Pitboss: PB39, PB40PB52, PB59 Useful Collections: Pickmethods, Mapmaking, Curious Civplayer

Buy me a coffee
Reply

How trade connections between cities formed?

This actually a huge question. The game checks for every plots connections towards their 8 neighbor plots. If there is a connection it groups them into CvPlotGroup. Every plot in those groups - and with that every city on those plots - is considered connected and gets trade connection. We don't need to know each every bit of this bigger algorithm. The only thing we need to know is what qualifies two plots to be connected. For this we look into CvPlot and there into the method isTradeNetworkConnected:

Code:
bool CvPlot::isTradeNetworkConnected(const CvPlot* pPlot, TeamTypes eTeam) const
{
    FAssertMsg(eTeam != NO_TEAM, "eTeam is not assigned a valid value");

    if (atWar(eTeam, getTeam()) || atWar(eTeam, pPlot->getTeam()))
    {
        return false;
    }

    if (isTradeNetworkImpassable(eTeam) || pPlot->isTradeNetworkImpassable(eTeam))
    {
        return false;
    }

    if (!isOwned())
    {
        if (!isRevealed(eTeam, false) || !(pPlot->isRevealed(eTeam, false)))
        {
            return false;
        }
    }

    if (isRoute())
    {
        if (pPlot->isRoute())
        {
            return true;
        }
    }

    if (isCity(true, eTeam))
    {
        if (pPlot->isNetworkTerrain(eTeam))
        {
            return true;
        }
    }

    if (isNetworkTerrain(eTeam))
    {
        if (pPlot->isCity(true, eTeam))
        {
            return true;
        }

        if (pPlot->isNetworkTerrain(eTeam))
        {
            return true;
        }

        if (pPlot->isRiverNetwork(eTeam))
        {
            if (pPlot->isRiverConnection(directionXY(pPlot, this)))
            {
                return true;
            }
        }
    }

    if (isRiverNetwork(eTeam))
    {
        if (pPlot->isNetworkTerrain(eTeam))
        {
            if (isRiverConnection(directionXY(this, pPlot)))
            {
                return true;
            }
        }

        if (isRiverConnection(directionXY(this, pPlot)) || pPlot->isRiverConnection(directionXY(pPlot, this)))
        {
            if (pPlot->isRiverNetwork(eTeam))
            {
                return true;
            }
        }
    }

    return false;
}

The first important thing to know is what those initial parameters mean.

pPlot = the other plot, which we want to check connections against
eTeam = the team/player with whom we want to trade, this can be an other civ or your own civ in case of domestic trade

Now we can start checking things between those two plots:


Code:
if (atWar(eTeam, getTeam()) || atWar(eTeam, pPlot->getTeam()))
{
   return false;
}

The first part is rather simple. If we are in a war with the eTeam or in a war with the owner of the other plot, then no trade connection is possible.

Code:
if (isTradeNetworkImpassable(eTeam) || pPlot->isTradeNetworkImpassable(eTeam))
{
   return false;
}

Here we check if our plot or the other is an impassable terrain. If it is, no trade connection. Now there is one exception: If the plot has a river and is being controlled by eTeam or eTeam has Sailing, then isTradeNetworkImpassable returns false and we continue with the next check.

Code:
if (!isOwned())
{
   if (!isRevealed(eTeam, false) || !(pPlot->isRevealed(eTeam, false)))
   {
       return false;
   }
}

If our plot is not owned by anyone (no culture territory on it), then we check if our plot or the other plot are revealed. If not then there is no trade connection possible. With this last check we checked for everything that denies a trade connection. The next checks immediately enable a trade connection.


Code:
if (isRoute())
{
   if (pPlot->isRoute())
   {
       return true;
   }
}

This is the easiest case. If our plot has a route (road, railroad etc.) and the other plot has one two, then we have a trade connection between those.


Code:
if (isCity(true, eTeam))
{
   if (pPlot->isNetworkTerrain(eTeam))
   {
       return true;
   }
}

A little big more complicated. First we check if our plot has a city from eTeam on it. If true, then we check if the other plot gets true from isNetworkTerrain for eTeam. isNetworkTerrain throws true in these cases:
  • If eTeam has the appropiate tech for trading on this terrain (Sailing for coast and Astronomy for Ocean)
  • If our plot is a water plot and owned by eTeam
If this is also true, then we have a trade connection between those plots.


Code:
if (isNetworkTerrain(eTeam))
{
   if (pPlot->isCity(true, eTeam))
   {
       return true;
   }

   if (pPlot->isNetworkTerrain(eTeam))
   {
       return true;
   }

   if (pPlot->isRiverNetwork(eTeam))
   {
       if (pPlot->isRiverConnection(directionXY(pPlot, this)))
       {
           return true;
       }
   }
}

Here we start by checking isNetworkTerrain again, but this time for our plot. If true we check if the other plot has a city from eTeam, if true we have a connection. If not we check if eTeam also gets true from isNetworkTerrain for the other plot and if true we have a connection. This is our basic check for trade across coast or ocean between unowned plots. Lastly we check for rivers with isRiverNetwork and isRiverConnection. I don't want to explain those here, because they are a bit more complicated, just know that this is the check from a water tile to the land tile with a river.

Code:
if (isRiverNetwork(eTeam))
{
   if (pPlot->isNetworkTerrain(eTeam))
   {
       if (isRiverConnection(directionXY(this, pPlot)))
       {
           return true;
       }
   }

   if (isRiverConnection(directionXY(this, pPlot)) || pPlot->isRiverConnection(directionXY(pPlot, this)))
   {
       if (pPlot->isRiverNetwork(eTeam))
       {
           return true;
       }
   }
}

Now here the whole thing gets a lot more complicated. As you can see we have the two method from before back. Let's start with the easier one:

isRiverNetwork:

In this method we check if the plot has a river. If it has one, we check if eTeam has Sailing or the plot is owned by eTeam. In both those cases we return true.

With this knowledge we can start investigating the code again. We start by checking for isRiverNetwork for our plot. We then check if the other plot is a water plot and eTeam has the basic requirements to trade on that terrain. Lastly in that check we check for this omnious isRiverConnection, which I will get to in a minute. This first part of the code is a reverse of the previous check. Last time we check for trade connection from water to river, here we check for river to water.

Now it is time to explain

isRiverConnection:

As you can see we throw the result from an other method into this method, that method being directionXY. That method returns the direction from the first plot thrown in to the second plot. Let's say we check for a plot and the tile NE of it. If we throw in our plot first and then the NE, we get DIRECTION_NORTHEAST from the method. If we reverse the order we get DIRECTION_SOUTHWEST.
Now that we know that we return to isRiverConnection. In this method there are numerous checks for river crossings. To make life easier for me and you I've prepared this screenshot showing all possible checks inside isRiverConnection:





In these examples the desert tile is always the first parameter directionXY and the tundra tile the second parameter. For all of these 16 combinations isRiverConnection will return true for the given direction. If we take my example from before with the plot and a plot NE of it. This would be the direction DIRECTION_NORTHEAST. If we look at the screenshot we have to look for the two examples in the upper right of the screenshot. If our initial plot has a river crossing to its north or its east then isRiverConnection will return true.

With the knowledge of this method we can finish the last check for trade connections between two river connections.


Lastly some additional implications of this code:
  • If there is an enemy civ between our trading plots, then having closed borders with them doesn't break trade connections with any other third civ. Only war can brake trade routes.
  • The last point also implies that you can't have trade routes through barbarian territory as you are always at war with them.
  • You only need to reveal a tile once for the trade connection. You do not need to know if there is a road on it.
  • You only need to uncover all unowned tiles towards a player, in order to have trade routes with them
  • Even better, you even don't need to ever have seen his culture territory. If you uncover tiles and the other player later founds a city there, you will get trade routes with them. Of course you need to have met there units to be able to open borders with them.

EDIT: Fix a previously wrong statement that war does not break trade routes. It does.
Mods: RtR    CtH

Pitboss: PB39, PB40PB52, PB59 Useful Collections: Pickmethods, Mapmaking, Curious Civplayer

Buy me a coffee
Reply

How is trade commerce calculated?

Some may already know the basics behind this question. In fact many most likely read this article, but I noticed that there are some tiny things missing in there.

So first of all the article is right that there are two steps for the trade commerce calculation: The base value and the modifiers on top. Now the game iterates across all cities and always chooses the connection with the most trade commerce for trade routes. In case of a tie domestic trade routes are preferred at least in RtR and CtH. Let's look at the base value calculation first. For this we turn to CvCity and the getBaseTradeProfit method.

Code:
int CvCity::getBaseTradeProfit(CvCity* pCity) const
{
    int iProfit = std::min(pCity->getPopulation() * GC.getDefineINT("THEIR_POPULATION_TRADE_PERCENT"), plotDistance(getX_INLINE(), getY_INLINE(), pCity->getX_INLINE(), pCity->getY_INLINE()) * GC.getWorldInfo(GC.getMapINLINE().getWorldSize()).getTradeProfitPercent());

    iProfit *= GC.getDefineINT("TRADE_PROFIT_PERCENT");
    iProfit /= 100;

    iProfit = std::max(100, iProfit);

    return iProfit;
}

Right in the first line we see the decision being made between population and distance, that the article spoke of. But the actual math works a little bit different. The article is right that we take whichever value is smaller. For population the calculation is:

pCity->getPopulation() * GC.getDefineINT("THEIR_POPULATION_TRADE_PERCENT")

Now pCity is the other city we want to connect to and THEIR_POPULATION_TRADE_PERCENT is defined as 50 in the GlobalDefines.xml. As you can see this turns out rather simple, we just multiply the population of the other city with 50. Now to the distance:

plotDistance(getX_INLINE(), getY_INLINE(), pCity->getX_INLINE(), pCity->getY_INLINE()) * GC.getWorldInfo(GC.getMapINLINE().getWorldSize()).getTradeProfitPercent()

Here we start by calculating the distance between the two cities with the plotDistance method. We then multiply that with a value (iTradeProfitPercent) from the CIV4WorldInfo.XML. For Standard maps this value is 50.

Let's do two examples to follow throw with the logic:

Example A:
Distance = 20
Population of other city = 15

This gives us a value of 1000 for distance and 750 for population. We continue with the smaller value 750.

Example B:
Distance = 6
Population of other city = 20

This gives us a value of 300 for distance and 1000 for population. We continue with the smaller value 300.

Next up in the code is

Code:
    iProfit *= GC.getDefineINT("TRADE_PROFIT_PERCENT");
    iProfit /= 100;

TRADE_PROFIT_PERCENT is also defined in the GlobalDefines.xml and is set to 20. Returning to our examples we have:

Example A:

750 * 20 = 15000
15000 / 100 = 150

Example B:

300 * 20 = 6000
6000 / 100 = 60

In our last step of the method we do

Code:
iProfit = std::max(100, iProfit);

This is just guaranteeing the the value is always 100 or higher. In our examples we now have:

Example A:

150 is higher then 100, so it stays 150

Example B:

60 is smaller then 100, so it gets set to 100

And with that we have the base value. If you divide these values by 100 you already have the base value that the game is showing you in-game.

Now we need to look at the other part of the trade commerce calculation. For this we have to look at CvCity and the method calculateTradeProfit.

Code:
int CvCity::calculateTradeProfit(CvCity* pCity) const
{
    int iProfit = getBaseTradeProfit(pCity);

    iProfit *= totalTradeModifier(pCity);
    iProfit /= 10000;

    return iProfit;
}

First of all we see our familiar getBaseTradeProfit method, which we already discussed. Next the base value is multiplied with the modifiers and then divided by 10000. totalTradeModifier is actually rather simple. Starting with a value of 100 we just add up all the modifiers. The article above alread mentioned the different kinds of modifiers and is correct about the values. Those are:

+5% for population of the Receiving city over 10
+25% if the receiving city has a connection to the Capital
+3% per turn since your last war with the civ (if it is another civ) [max 150%]
+50% if you have a Harbor
+100% for the ToArtemis
+100% if the other city is on another landmass
+100% if it is another civ on another landmass AND you have a Customs House

Let's go back to the examples:

Example A:
Domestic trade route
Our city is at size 14 = +20%
Connected to the capital = +25%
Harbor = +50%

With this we get a totalTradeModifier of 195. Putting it all in the rest of the method we get:

150 * 195 = 29250 / 10000 = 2.925

Thanks to integer calculation this is now rounded down to 2. This actually leads to some interesting hidden effects. Let's say we also have the Temple of Artemis in the city. With that we would get:

150 * 295 = 44250 / 10000 = 4.425

Which is then rounded down to 4. If the city grows on pop it would still get 4 with the ToA, but 3 without it. All thanks to the wonder of integer calculation.

Example B:

Foreign trade route
Our city is at size 19 = +45%
Connected to the capital = +25%
Maximum peace = +150%
Harbor = +50%
On another landmass = +100%
Custom house = +100%

With this we get a totalTradeModifier of 570. Putting it all in the rest of the method we get:

100 * 570 = 57000 / 10000 = 5.7

Which with the magic of integer calculation is 5. Because the base value is a straight 1, we don't get as much integer calculation shenanigans as in Example A.

And that's it. We have our trade value. We now only need to do that every other city in the game. smile

Summary

Basically the calculation is this

Base value:

baseValue = (Smaller value of either population of the other city * 50 or distance between the cities * 50) * 20 / 100
Rounded down to the next integer

if baseValue < 100 set it to 100

totalModifiers = 100 + all other modifiers:

+5% for population of the Receiving city over 10
+25% if the receiving city has a connection to the Capital
+3% per turn since your last war with the civ (if it is another civ) [max 150%]
+50% if you have a Harbor
+100% for the ToArtemis
+100% if the other city is on another landmass
+100% if it is another civ on another landmass AND you have a Customs House

Final trade commerce = baseValue * totalModifiers / 10000
Rounded down to the next integer
Mods: RtR    CtH

Pitboss: PB39, PB40PB52, PB59 Useful Collections: Pickmethods, Mapmaking, Curious Civplayer

Buy me a coffee
Reply

Thanks for digging into this, Charriu. thumbsup

One item to note: Just what does the "+100% if the other city is on another continent" mean by "another continent?" I think this effectively means no land connection, must cross at least one water tile to reach. So a city on a tiny island just off shore of a continent counts for the +100%. And that city can then be a more valuable destination for lots of other cities, making even a crummy island city quite valuable.
Reply

Exactly right. I took that from the linked article. But I will change it to "landmass". I think that's a bit clearer.
Mods: RtR    CtH

Pitboss: PB39, PB40PB52, PB59 Useful Collections: Pickmethods, Mapmaking, Curious Civplayer

Buy me a coffee
Reply

How does the game determine where a watermill can be build?

As most of you know the watermill can only be build on one side of the river. But how exactly does the game determine this. Well to find out we have to look into CvPlot and the method canHaveImprovement.

Code:
if (GC.getImprovementInfo(eImprovement).isRequiresRiverSide())
{
    bValid = false;

    for (iI = 0; iI < NUM_CARDINALDIRECTION_TYPES; ++iI)
    {
        pLoopPlot = plotCardinalDirection(getX_INLINE(), getY_INLINE(), ((CardinalDirectionTypes)iI));

        if (pLoopPlot != NULL)
        {
            if (isRiverCrossing(directionXY(this, pLoopPlot)))
            {
                if (pLoopPlot->getImprovementType() != eImprovement)
                {
                    bValid = true;
                    break;
                }
            }
        }
    }
}

This is actually a rather simple case:

- The first 'if' checks for the bRiverSideMakesValid inside the ImprovementInfo.xml, which in the case of the watermill is 1 meaning true in the civ4 code.
- Next we go through all the four cardinal directions and check if there is a river crossing. This is done by the isRiverCrossing method. I've talked about this method in the past in the How trade connections between cities formed? question. You will find the relevant part at the bottom of that post with the picture.
- Lastly we check if the tile on the other side of the river contains a watermill. If not we set bValid to true, which down the road returns true for the whole canHaveImprovement method.

Also because this will be asked. The direction of the artwork does not matter for the checks. It's just eye-candy.

Summary

This is rather easy. The watermill needs to be on a river and at least one of the four tiles to its north, east, south and west must have not a watermill on it. The direction of the artwork does not matter for the checks.

EDIT: I had mistake in the summary and corrected it.
Mods: RtR    CtH

Pitboss: PB39, PB40PB52, PB59 Useful Collections: Pickmethods, Mapmaking, Curious Civplayer

Buy me a coffee
Reply

For transparency. I had an error in the summary above and fixed that error just now.
Mods: RtR    CtH

Pitboss: PB39, PB40PB52, PB59 Useful Collections: Pickmethods, Mapmaking, Curious Civplayer

Buy me a coffee
Reply

(December 25th, 2020, 16:37)Charriu Wrote: The watermill needs to be on a river and all the at least one of the four tiles to its north, east, south and west must have not a watermill on it.

I think you have an error in your fix.
Reply

Thanks, I'm going to bed now, as it's painfully obvious that I should get some sleep. smile
Mods: RtR    CtH

Pitboss: PB39, PB40PB52, PB59 Useful Collections: Pickmethods, Mapmaking, Curious Civplayer

Buy me a coffee
Reply

Ok, prepare for a big one. In this and the following questions I will try to outline and explain how war weariness works in the game. There's quiet a lot to it hence why I split this into multiple parts.

How does one increase war weariness for a team?

As you can see we already start complicated. The game differentiates between players and teams, this shouldn't be too much of a surprise, because this was always there and available. We just don't normally think about teams, because for the most part team = player.

For the most part this questions is about how the multiple war weariness values from GlobalDefines.xml are integrated in the game. All values in question start with "WW_" in the xml so you should find them rather easily.

For the first values we look into CvUnit and there into updateCombat. This method handles the combat itself. What important to know is that this code runs on the attacking units part hence why most calls like isDead are directed at the attacker. The code part we are interested in runs after combat has finished. There are two segments we are interested in.

Code:
if (isDead())
{
    ...

    if (!m_pUnitInfo->isHiddenNationality() && !pDefender->getUnitInfo().isHiddenNationality())
    {
        GET_TEAM(getTeam()).changeWarWeariness(pDefender->getTeam(), *pPlot, GC.getDefineINT("WW_UNIT_KILLED_ATTACKING"));
        GET_TEAM(pDefender->getTeam()).changeWarWeariness(getTeam(), *pPlot, GC.getDefineINT("WW_KILLED_UNIT_DEFENDING"));
        ...
    }

    ...
}

First we check if the attacker died. Next we check if either one of the involved units has the hiddenNationality flag. If not we call changeWarWeariness for both parties:

Attacker: Gets WW_UNIT_KILLED_ATTACKING added to their War Weariness, which is set to 3
Defender: Gets WW_KILLED_UNIT_DEFENDING added to their War Weariness, which is set to 1

We will get back to the changeWarWeariness method later, because there is an important factor in it. For now we continue with:

Code:
else if (pDefender->isDead())
{
    ...

    if (!m_pUnitInfo->isHiddenNationality() && !pDefender->getUnitInfo().isHiddenNationality())
    {
        GET_TEAM(pDefender->getTeam()).changeWarWeariness(getTeam(), *pPlot, GC.getDefineINT("WW_UNIT_KILLED_DEFENDING"));
        GET_TEAM(getTeam()).changeWarWeariness(pDefender->getTeam(), *pPlot, GC.getDefineINT("WW_KILLED_UNIT_ATTACKING"));
        ...
    }
   
    ...
}

This time the defender died, which is checked by the pDefender->isDead() part. Just like last time we check if any involved unit has the hiddenNationality flag. If not we call changeWarWeariness for both parties:

Defender: Gets WW_UNIT_KILLED_DEFENDING added to their War Weariness, which is set to 2
Attacker: Gets WW_KILLED_UNIT_ATTACKING added to their War Weariness, which is set to 2

For the next values we are still in CvUnit, but now in setXY. This code handles placing units on the map, but hidden in there is logic for capturing units. In this segment we iterate across all units that stood on the tile before the unit moved on this tile:

Code:
if (isEnemy(pLoopUnit->getTeam(), pNewPlot) || pLoopUnit->isEnemy(getTeam()))
{
    if (!pLoopUnit->canCoexistWithEnemyUnit(getTeam()))
    {
        if (NO_UNITCLASS == pLoopUnit->getUnitInfo().getUnitCaptureClassType() && pLoopUnit->canDefend(pNewPlot))
        {
            ...
        }
        else
        {
            if (!m_pUnitInfo->isHiddenNationality() && !pLoopUnit->getUnitInfo().isHiddenNationality())
            {
                GET_TEAM(pLoopUnit->getTeam()).changeWarWeariness(getTeam(), *pNewPlot, GC.getDefineINT("WW_UNIT_CAPTURED"));
                GET_TEAM(getTeam()).changeWarWeariness(pLoopUnit->getTeam(), *pNewPlot, GC.getDefineINT("WW_CAPTURED_UNIT"));
                GET_TEAM(getTeam()).AI_changeWarSuccess(pLoopUnit->getTeam(), GC.getDefineINT("WAR_SUCCESS_UNIT_CAPTURING"));
            }

            ...
        }
    }
}

First we have some checks:

- If the unit is an enemy unit (we are at war with that player)
- If the unit cannot coexist with enemy units (spies can for example)
- If the unit has a value set for "Capture" in the UnitInfo.xml. (Only worker, fast worker and settler have those set)
- If the unit can defend (it has combat strength)

If all those apply we call changeWarWeariness for both parties:

Defender: Gets WW_UNIT_CAPTURED added to their War Weariness, which is set to 2
Attacker: Gets WW_CAPTURED_UNIT added to their War Weariness, which is set to 1

Later in setXY there is another value from the GlobalDefines value. The code is rather uninteresting. It involves capturing a city. For this changeWarWeariness is called:

Attacker: Gets WW_CAPTURED_CITY added to their War Weariness, which is set to 6

This is the last call to changeWarWeariness, so it's time to look into that. You can find this method in CvTeam by the way.

Code:
void CvTeam::changeWarWeariness(TeamTypes eOtherTeam, const CvPlot& kPlot, int iFactor)
{
    int iOurCulture = kPlot.countFriendlyCulture(getID());
    int iTheirCulture = kPlot.countFriendlyCulture(eOtherTeam);

    int iRatio = 100;
    if (0 != iOurCulture + iTheirCulture)
    {
        iRatio = (100 * iTheirCulture) / (iOurCulture + iTheirCulture);
    }

    changeWarWeariness(eOtherTeam, iRatio * iFactor);
}

First we get all our culture on the tile on which the combat happens. Next we do the same for the enemy. Note that countFriendlyCulture not only gives you your own culture. It also adds the culture of every player with whom:

1. You are in a team or
2. Is your vassal or
3. You have open borders with

Back in our code above we next establish a variable iRatio set to 100.
We then check if the some of both culture values is not 0. This check ensures that when fighting in neutral land, you always get the full amount of war weariness added. If this check is true, we set iRatio to

iRatio = (100 * iTheirCulture) / (iOurCulture + iTheirCulture);

In the last line we have another call to changeWarWeariness, but this time it is a different method (it only has 2 parameters). What happens here is that the product of iRatio and iFactor is added to our current war weariness value. iFactor in this case are the values from GlobalDefines, which we already discussed. Another important factor for later is that inside this changeWarWeariness, we collect the war weariness separately for every player. Let's take a examples to better understand what is happening:

Example a)

We attack and kill a unit of player A. We therefore get WW_KILLED_UNIT_ATTACKING (2) added. We are fighting inside our own culture of 120, with no other culture present: This is what happens inside changeWarWeariness:

int iOurCulture = 120;
int iTheirCulture = 0;

int iRatio = 100;
if (0 != 120 + 0)
{
iRatio = (100 * 0) / (120 + 0) = 0 / 120 = 0;
}

changeWarWeariness(eOtherTeam, 0 * 2);

0 war weariness is added towards player A


Example b)

We attack and kill a unit of player A. We therefore get WW_KILLED_UNIT_ATTACKING (2) added. We are fighting inside their culture of 120, with none of our culture present: This is what happens inside changeWarWeariness:

int iOurCulture = 0;
int iTheirCulture = 120;

int iRatio = 100;
if (0 != 0 + 120)
{
iRatio = (100 * 120) / (0 + 120) = 12000 / 120 = 100;
}

changeWarWeariness(eOtherTeam, 100 * 2);

200 war weariness is added towards player A


Example c)

We attack and kill a unit of player A. We therefore get WW_KILLED_UNIT_ATTACKING (2) added. We are fighting inside their culture of 120, with 80 of our culture present: This is what happens inside changeWarWeariness:

int iOurCulture = 80;
int iTheirCulture = 120;

int iRatio = 100;
if (0 != 80 + 120)
{
iRatio = (100 * 120) / (80 + 120) = 12000 / 200 = 60;
}

changeWarWeariness(eOtherTeam, 60 * 2);

120 war weariness is added towards player A


Example d)

We attack and kill a unit of player A. We therefore get WW_KILLED_UNIT_ATTACKING (2) added. We are fighting inside their culture of 120, with 80 of our culture present, 100 culture from player B are present, who is our vassal, 40 culture from player C with whom A has open borders and 100 culture from player D with whom both have open borders with: This is what happens inside changeWarWeariness:

int iOurCulture = 280; (Our culture + B + D)
int iTheirCulture = 260; (A + C + D)

int iRatio = 100;
if (0 != 280 + 260)
{
iRatio = (100 * 260) / (280 + 260) = 12000 / 200 = 48.14 = 48 due to integer calculation;
}

changeWarWeariness(eOtherTeam, 48 * 2);

96 war weariness is added towards player A



Now this was a lot to take in and should cover most cases. There are a few more cases which are rare in our PBs, but should be covered for completeness sake. Also in CvUnit is the method nuke. Here we do not call the changeWarWeariness method that does the culture comparisson. Instead we add:

100 * WW_HIT_BY_NUKE (3) for all enemy players, who control plots in the nukes radius
100 * WW_ATTACKED_WITH_NUKE (12) for the player that nuked.

Also here are some more loose ends:

- The war weariness value shown in the F4 advisor is not the war weariness value we discussed here.
- As some may have noticed there is not explicit check excluding the barbarians. I've tested it and you do not get war weariness towards barbarians. I don't know where and how the original developers did that.

Summary

For the most part this is knowledge that so far is widely known. You get war weariness in these cases for your team:

Attacker kills a unit = 2 * culture ratio
Attacker looses a unit = 3 * culture ratio
Attacker captures a unit = 1 * culture ratio
Attacker captures a city = 6 * culture ratio
Defender kills a unit = 1 * culture ratio
Defender looses a unit either by capture or death = 2 * culture ratio

Attacker nukes someone = 12 (no culture comparison)
Defender gets nuked = 3 (no culture comparison)

It's important to note that for most values the culture of the combat tile is compared. The more culture you have on that tile the less war weariness you get. It's important to note that the culture from players with whom:

1. You are in a team or
2. Is your vassal or
3. You have open borders with

is added towards your culture and/or their cutlure. The important formula for the comparison is:

Culture ratio = (100 * iTheirCulture) / (iOurCulture + iTheirCulture);

With Culture ratio = 100 with no culture present.

Lastly some more important features of the system in place:

- You do not get war weariness from barbarians
- If no unit is killed (Withdraw) no war weariness is added.
- Remember that open borders count towards the culture ratio.
- The team's war weariness value is not the value shown in the F4 advisor.
Mods: RtR    CtH

Pitboss: PB39, PB40PB52, PB59 Useful Collections: Pickmethods, Mapmaking, Curious Civplayer

Buy me a coffee
Reply



Forum Jump: