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

When are events triggered and how is the triggered event selected?

To make understanding the whole code a bit better, I first need to mention what xml files there are and how the interact with each other. Basically there are two files important for the events:

CIV4EventInfos.xml and CIV4EventTriggerInfos.xml

The EventInfos contains all the stuff related to the actual event. The EventTrigger everything that determines when an Event is triggered. Both files make extensive interaction with python files to interact with the game most found in CvRandomEventInterface.py. As you can imagine the whole event feature can become hideously complicated quickly. To make it even worse quests are also technically events, but do make the whole event feature even more complicated. So if you ever want to divide deep into that topic, beware. smile

For our purpose we look into CvPlayer.cpp and in there in the method doEvents. This method is called every turn for each player.

Code:
void CvPlayer::doEvents()
{
    if (GC.getGameINLINE().isOption(GAMEOPTION_NO_EVENTS))
    {
        return;
    }

    if (isBarbarian() || isMinorCiv())
    {
        return;
    }

    CvEventMap::iterator it = m_mapEventsOccured.begin();
    while (it != m_mapEventsOccured.end())
    {
        if (checkExpireEvent(it->first, it->second))
        {
            expireEvent(it->first, it->second, true);
            it = m_mapEventsOccured.erase(it);
        }
        else
        {
            ++it;
        }
    }

    bool bNewEventEligible = true;
    if (GC.getGameINLINE().getElapsedGameTurns() < GC.getDefineINT("FIRST_EVENT_DELAY_TURNS"))
    {
        bNewEventEligible = false;
    }

    if (bNewEventEligible)
    {
        if (GC.getGameINLINE().getSorenRandNum(GC.getDefineINT("EVENT_PROBABILITY_ROLL_SIDES"), "Global event check") >= GC.getEraInfo(getCurrentEra()).getEventChancePerTurn())
        {
            bNewEventEligible = false;
        }
    }

    std::vector< std::pair<EventTriggeredData*, int> > aePossibleEventTriggerWeights;
    int iTotalWeight = 0;
    for (int i = 0; i < GC.getNumEventTriggerInfos(); ++i)
    {
        int iWeight = getEventTriggerWeight((EventTriggerTypes)i);
        if (iWeight == -1)
        {
            trigger((EventTriggerTypes)i);
        }
        else if (iWeight > 0 && bNewEventEligible)
        {
            EventTriggeredData* pTriggerData = initTriggeredData((EventTriggerTypes)i);
            if (NULL != pTriggerData)
            {
                iTotalWeight += iWeight;
                aePossibleEventTriggerWeights.push_back(std::make_pair(pTriggerData, iTotalWeight));
            }
        }
    }

    if (iTotalWeight > 0)
    {
        bool bFired = false;
        int iValue = GC.getGameINLINE().getSorenRandNum(iTotalWeight, "Event trigger");
        for (std::vector< std::pair<EventTriggeredData*, int> >::iterator it = aePossibleEventTriggerWeights.begin(); it != aePossibleEventTriggerWeights.end(); ++it)
        {
            EventTriggeredData* pTriggerData = (*it).first;
            if (NULL != pTriggerData)
            {
                if (iValue < (*it).second && !bFired)
                {
                    trigger(*pTriggerData);
                    bFired = true;
                }
                else
                {
                    deleteEventTriggered(pTriggerData->getID());
                }
            }
        }
    }

    std::vector<int> aCleanup;
    for (int i = 0; i < GC.getNumEventInfos(); ++i)
    {
        const EventTriggeredData* pTriggeredData = getEventCountdown((EventTypes)i);
        if (NULL != pTriggeredData)
        {
            if (GC.getGameINLINE().getGameTurn() >= pTriggeredData->m_iTurn)
            {
                applyEvent((EventTypes)i, pTriggeredData->m_iId);
                resetEventCountdown((EventTypes)i);
                aCleanup.push_back(pTriggeredData->m_iId);
            }
        }
    }

    for (std::vector<int>::iterator it = aCleanup.begin(); it != aCleanup.end(); ++it)
    {
        bool bDelete = true;

        for (int i = 0; i < GC.getNumEventInfos(); ++i)
        {
            const EventTriggeredData* pTriggeredData = getEventCountdown((EventTypes)i);
            if (NULL != pTriggeredData)
            {
                if (pTriggeredData->m_iId == *it)
                {
                    bDelete = false;
                    break;
                }
            }
        }

        if (bDelete)
        {
            deleteEventTriggered(*it);
        }
    }
}

I know what you are thinking and yes it is a lot to take into. Let's start simple with the beginning:

Code:
if (GC.getGameINLINE().isOption(GAMEOPTION_NO_EVENTS))
    {
        return;
    }

    if (isBarbarian() || isMinorCiv())
    {
        return;
    }

Of course with the "No Events" Option you won't encounter events and of course if you are a Barbarian or a Minor Civ you don't encounter those too. Let's look at the next block:

Code:
CvEventMap::iterator it = m_mapEventsOccured.begin();
    while (it != m_mapEventsOccured.end())
    {
        if (checkExpireEvent(it->first, it->second))
        {
            expireEvent(it->first, it->second, true);
            it = m_mapEventsOccured.erase(it);
        }
        else
        {
            ++it;
        }
    }

What this code does is checking the python method referenced in PythonExpireCheck inside the EventInfos.xml. This is exclusively used for the quests and ensures quests are expired when their expire conditions are met. But this code otherwise is not relevant for the question above.

Code:
bool bNewEventEligible = true;
    if (GC.getGameINLINE().getElapsedGameTurns() < GC.getDefineINT("FIRST_EVENT_DELAY_TURNS"))
    {
        bNewEventEligible = false;
    }

    if (bNewEventEligible)
    {
        if (GC.getGameINLINE().getSorenRandNum(GC.getDefineINT("EVENT_PROBABILITY_ROLL_SIDES"), "Global event check") >= GC.getEraInfo(getCurrentEra()).getEventChancePerTurn())
        {
            bNewEventEligible = false;
        }
    }

The first part to our question can be found in this next code section. When bNewEventEligible remains true an event will be triggered. So when does this become false. First we look for FIRST_EVENT_DELAY_TURNS in the GlobalDefines.XML. This is set to 20 and our first check uses this. During the first 20 turns no events are triggered.
The next check is a bit more complicated. We once need GlobalDefines.XML again and here EVENT_PROBABILITY_ROLL_SIDES which is set to 100. Basically the game rolls a d100 and the result of that is checked against iEventChancePerTurn inside the EraInfos.xml the respective values are:

Ancient = 1
Classical = 2
Medieval = 4
Renaissance = 4
Industrial = 6
Modern = 8
Future = 10

If the game rolls above or equal to these numbers no event is triggered. With the way the game rolls random numbers the numbers above are basically the chance you have for an event each turn.

Code:
std::vector< std::pair<EventTriggeredData*, int> > aePossibleEventTriggerWeights;
    int iTotalWeight = 0;
    for (int i = 0; i < GC.getNumEventTriggerInfos(); ++i)
    {
        int iWeight = getEventTriggerWeight((EventTriggerTypes)i);
        if (iWeight == -1)
        {
            trigger((EventTriggerTypes)i);
        }
        else if (iWeight > 0 && bNewEventEligible)
        {
            EventTriggeredData* pTriggerData = initTriggeredData((EventTriggerTypes)i);
            if (NULL != pTriggerData)
            {
                iTotalWeight += iWeight;
                aePossibleEventTriggerWeights.push_back(std::make_pair(pTriggerData, iTotalWeight));
            }
        }
    }

We get some answers to the second part of our question in this block. If bNewEventEligible = true we iterate across all the events in EventTriggerInfo and add up all the iWeight bigger then 0 found in the EventTriggerInfo to get the total amount of weights. Now it's important to know that initTriggeredData does check if the conditions for the trigger are met even the ones done by python. So only triggerable events go into the total. By the way initTriggeredData is a 500+ lines monster of a method, which I don't want to delve into with this segment.
Now you may wonder what the "iWeight == -1" part is about and why these events are triggered without any check for EventTriggerInfo and disregarding the previous event chance per era. This is another part of the quests. You will notice that quests have a trigger and an event for the start of the quest. But the end of a quest with the results is also a trigger + event. All of these have the weight -1 and this code ensures that the quests are completed. I don't want to delve any deeper into that part, because it doesn't contribute to the answer of the question, but you can see how the event feature gets complicated thanks to the tagged on quests.
Now what we will do with the TotalWeight will be looked at in the next code segment:

Code:
if (iTotalWeight > 0)
    {
        bool bFired = false;
        int iValue = GC.getGameINLINE().getSorenRandNum(iTotalWeight, "Event trigger");
        for (std::vector< std::pair<EventTriggeredData*, int> >::iterator it = aePossibleEventTriggerWeights.begin(); it != aePossibleEventTriggerWeights.end(); ++it)
        {
            EventTriggeredData* pTriggerData = (*it).first;
            if (NULL != pTriggerData)
            {
                if (iValue < (*it).second && !bFired)
                {
                    trigger(*pTriggerData);
                    bFired = true;
                }
                else
                {
                    deleteEventTriggered(pTriggerData->getID());
                }
            }
        }
    }

So if iTotalWeight is bigger then 0 we roll a random number between 0 and and iTotalWeight - 1 and this number selects the actual trigger and event being fired. For this the game collected all triggers in the previous code segment and arranged them in a specific order. Let's do a quick example:

3 triggers are consideres with the weights 100, 300 and 200. The game adds up all the weights = 600 and stacks the triggers so that a number between 0 - 100 uses the first trigger, a number between 101 and 400 uses the second and a number between 401 and 600 uses the last.

Now you may have noticed that a lot more code comes after this code segment, but that code is no longer relevant for the question. It deals with special events that have follow up events like the healing plant event.

Summary

A lot of code boils down to a simple answer. The chance for an event to happen increases with each era with the following numbers.

Ancient = 1%
Classical = 2%
Medieval = 4%
Renaissance = 4%
Industrial = 6%
Modern = 8%
Future = 10%

The game then chooses an event trigger based on a standard weighted distribution with the iWeight as long as that trigger condition is fullfilled. If no trigger condition is fulfilled no event is fired.
Mods: RtR    CtH

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

Buy me a coffee
Reply

Thanks again for digging into these things and helping explain them, Charriu. thumbsup I do not usually play with events turned on, as I do not like their randomness in a strategy game. But it is good to know more about how they work for mods and special games.
Reply

@Charriu: Can mines outside of city bfc pop resources?
Reply

Uh good question. Worth an investigation how that mechanic works
Mods: RtR    CtH

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

Buy me a coffee
Reply

(September 4th, 2022, 10:46)civac2 Wrote: @Charriu: Can mines outside of city bfc pop resources?

from what i recall its only if they are worked they get a chance to pop resources.
"Superdeath seems to have acquired a rep for aggression somehow. [Image: noidea.gif] In this game that's going to help us because he's going to go to the negotiating table with twitchy eyes and slightly too wide a grin and terrify the neighbors into favorable border agreements, one-sided tech deals and staggered NAPs."
-Old Harry. PB48.
Reply

That's correct, only on a turn that a mine is worked.
Reply

How does Flanking work?

To understand this we have to know three values from the CIV4UnitInfos.xml. Here the values of the Horse Archer:

Code:
<FlankingStrikes>
    <FlankingStrike>
        <FlankingStrikeUnitClass>UNITCLASS_CATAPULT</FlankingStrikeUnitClass>
        <iFlankingStrength>100</iFlankingStrength>
    </FlankingStrike>
    <FlankingStrike>
        <FlankingStrikeUnitClass>UNITCLASS_TREBUCHET</FlankingStrikeUnitClass>
        <iFlankingStrength>100</iFlankingStrength>
    </FlankingStrike>
</FlankingStrikes>
<iCollateralDamageLimit>100</iCollateralDamageLimit>
<iCollateralDamageMaxUnits>6</iCollateralDamageMaxUnits>

As you can we will reuse some collateral values. For the actual code we have to look into CvUnit.cpp and in there in the method flankingStrikeCombat. This method is called when the combat has ended at two times: Either when your unit defeated the enemy unit or if your unit withdrew from the combat.

Code:
void CvUnit::flankingStrikeCombat(const CvPlot* pPlot, int iAttackerStrength, int iAttackerFirepower, int iDefenderOdds, int iDefenderDamage, CvUnit* pSkipUnit)
{
    if (pPlot->isCity(true, pSkipUnit->getTeam()))
    {
        return;
    }

    CLLNode<IDInfo>* pUnitNode = pPlot->headUnitNode();

    std::vector< std::pair<CvUnit*, int> > listFlankedUnits;
    while (NULL != pUnitNode)
    {
        CvUnit* pLoopUnit = ::getUnit(pUnitNode->m_data);
        pUnitNode = pPlot->nextUnitNode(pUnitNode);

        if (pLoopUnit != pSkipUnit)
        {
            if (!pLoopUnit->isDead() && isEnemy(pLoopUnit->getTeam(), pPlot))
            {
                if (!(pLoopUnit->isInvisible(getTeam(), false)))
                {
                    if (pLoopUnit->canDefend())
                    {
                        int iFlankingStrength = m_pUnitInfo->getFlankingStrikeUnitClass(pLoopUnit->getUnitClassType());

                        if (iFlankingStrength > 0)
                        {
                            int iFlankedDefenderStrength;
                            int iFlankedDefenderOdds;
                            int iAttackerDamage;
                            int iFlankedDefenderDamage;

                            getDefenderCombatValues(*pLoopUnit, pPlot, iAttackerStrength, iAttackerFirepower, iFlankedDefenderOdds, iFlankedDefenderStrength, iAttackerDamage, iFlankedDefenderDamage);

                            if (GC.getGameINLINE().getSorenRandNum(GC.getDefineINT("COMBAT_DIE_SIDES"), "Flanking Combat") >= iDefenderOdds)
                            {
                                int iCollateralDamage = (iFlankingStrength * iDefenderDamage) / 100;
                                int iUnitDamage = std::max(pLoopUnit->getDamage(), std::min(pLoopUnit->getDamage() + iCollateralDamage, collateralDamageLimit()));

                                if (pLoopUnit->getDamage() != iUnitDamage)
                                {
                                    listFlankedUnits.push_back(std::make_pair(pLoopUnit, iUnitDamage));
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    int iNumUnitsHit = std::min((int)listFlankedUnits.size(), collateralDamageMaxUnits());

    for (int i = 0; i < iNumUnitsHit; ++i)
    {
        int iIndexHit = GC.getGameINLINE().getSorenRandNum(listFlankedUnits.size(), "Pick Flanked Unit");
        CvUnit* pUnit = listFlankedUnits[iIndexHit].first;
        int iDamage = listFlankedUnits[iIndexHit].second;
        pUnit->setDamage(iDamage, getOwnerINLINE());
        if (pUnit->isDead())
        {
            CvWString szBuffer = gDLL->getText("TXT_KEY_MISC_YOU_KILLED_UNIT_BY_FLANKING", getNameKey(), pUnit->getNameKey(), pUnit->getVisualCivAdjective(getTeam()));
            gDLL->getInterfaceIFace()->addMessage(getOwnerINLINE(), false, GC.getEVENT_MESSAGE_TIME(), szBuffer, GC.getEraInfo(GC.getGameINLINE().getCurrentEra()).getAudioUnitVictoryScript(), MESSAGE_TYPE_INFO, NULL, (ColorTypes)GC.getInfoTypeForString("COLOR_GREEN"), pPlot->getX_INLINE(), pPlot->getY_INLINE());
            szBuffer = gDLL->getText("TXT_KEY_MISC_YOUR_UNIT_DIED_BY_FLANKING", pUnit->getNameKey(), getNameKey(), getVisualCivAdjective(pUnit->getTeam()));
            gDLL->getInterfaceIFace()->addMessage(pUnit->getOwnerINLINE(), false, GC.getEVENT_MESSAGE_TIME(), szBuffer, GC.getEraInfo(GC.getGameINLINE().getCurrentEra()).getAudioUnitDefeatScript(), MESSAGE_TYPE_INFO, NULL, (ColorTypes)GC.getInfoTypeForString("COLOR_RED"), pPlot->getX_INLINE(), pPlot->getY_INLINE());

            pUnit->kill(false);
        }
        
        listFlankedUnits.erase(std::remove(listFlankedUnits.begin(), listFlankedUnits.end(), listFlankedUnits[iIndexHit]));
    }

    if (iNumUnitsHit > 0)
    {
        CvWString szBuffer = gDLL->getText("TXT_KEY_MISC_YOU_DAMAGED_UNITS_BY_FLANKING", getNameKey(), iNumUnitsHit);
        gDLL->getInterfaceIFace()->addMessage(getOwnerINLINE(), false, GC.getEVENT_MESSAGE_TIME(), szBuffer, GC.getEraInfo(GC.getGameINLINE().getCurrentEra()).getAudioUnitVictoryScript(), MESSAGE_TYPE_INFO, NULL, (ColorTypes)GC.getInfoTypeForString("COLOR_GREEN"), pPlot->getX_INLINE(), pPlot->getY_INLINE());

        if (NULL != pSkipUnit)
        {
            szBuffer = gDLL->getText("TXT_KEY_MISC_YOUR_UNITS_DAMAGED_BY_FLANKING", getNameKey(), iNumUnitsHit);
            gDLL->getInterfaceIFace()->addMessage(pSkipUnit->getOwnerINLINE(), false, GC.getEVENT_MESSAGE_TIME(), szBuffer, GC.getEraInfo(GC.getGameINLINE().getCurrentEra()).getAudioUnitDefeatScript(), MESSAGE_TYPE_INFO, NULL, (ColorTypes)GC.getInfoTypeForString("COLOR_RED"), pPlot->getX_INLINE(), pPlot->getY_INLINE());
        }
    }
}

Let's digest all this code slowly. We start with an important information, which most already know:
Code:
    if (pPlot->isCity(true, pSkipUnit->getTeam()))
    {
        return;
    }

Basically if the tile we are fighting on is a city (or fort), then no flanking damage occurs. We follow this code with simple checks:

Code:
    CLLNode<IDInfo>* pUnitNode = pPlot->headUnitNode();

    std::vector< std::pair<CvUnit*, int> > listFlankedUnits;
    while (NULL != pUnitNode)
    {
        CvUnit* pLoopUnit = ::getUnit(pUnitNode->m_data);
        pUnitNode = pPlot->nextUnitNode(pUnitNode);

        if (pLoopUnit != pSkipUnit)
        {
            if (!pLoopUnit->isDead() && isEnemy(pLoopUnit->getTeam(), pPlot))
            {
                if (!(pLoopUnit->isInvisible(getTeam(), false)))
                {
                    if (pLoopUnit->canDefend())
                    {

We iterate over all the units in the combat involved tile and do some basic checks in these order:

1. We check if the unit == pSkipUnit. pSkipUnit is a parameter of the method and checking back with the call to the method reveals that pSkipUnit is the unit that did the actual defending on the tile. Makes sense to skip the unit we fought.
2. We check if the unit is alive and if it is an enemy
3. We check if the unit is invisible
4. We check if the unit canDefend, which most of the time means, does it have combat strength

With these checks out of the way, we get to the first meat of the flanking mechanic:
Code:
                        int iFlankingStrength = m_pUnitInfo->getFlankingStrikeUnitClass(pLoopUnit->getUnitClassType());

                        if (iFlankingStrength > 0)
                        {
                            [...]

                            if (GC.getGameINLINE().getSorenRandNum(GC.getDefineINT("COMBAT_DIE_SIDES"), "Flanking Combat") >= iDefenderOdds)
                            {
                                [actual damage code]
                            }

At first we get the iFlankingStrength of our unit against the current units class and we continue if that strength is bigger then 0. This is were the magic happens and the game differentiates which units can actually be flanked by our unit.
You can see that I left out a bit of code. If you analyse that part yourself you will notice that it's dead code meaning it has no effect in the actual mechanic. I will get back to this bit in a second though.

Next you can see that we role a random number once again and check that number against iDefenderOdds. A positive check means this unit can be damaged. The iDefenderOdds is another parameter of the method and we have to look back how that is calculated, which I did for you:

Code:
iDefenderOdds = iDefenderOdds * (100 - withdrawalProbability()) / 100;

The inner iDefenderOdds is the probability to hit the attacker. You can see that this odd is manipulated by our units withdrawal probability. Let's look at two examples:

Example A: Let's say the defender has 25% odds and our unit has 0% withdrawel. Let's also assume there are 3 catapults in the stack. That means for each of the 3 catapults we do a usual combat roll and if it beats the 25% we can damage this catapult
Example B: Let's say the defender has 75% odds and our unit has 50% withdrawel. We again assume 3 catapults in the stack. That means for each of the 3 catapults we do a usual combat roll and if we beat 75% * (100 - 50%) / 100 = 37% (integer calculation looses the 0.5%) we can damage this catapult

If you do the math you will notice that a withdrawel chance of 100% not only means our unit escapes the battle and does flanking, it also can damage every allowed unit in the stack as the withdrawel chance will reduce the odds to beat to 0%.

The important take away from this is that this is the odd from the initially fought unit and not a newly generated odd against the flanked unit. This is actually what the excluded code most likely did.

Now that we know if we can damage the unit we can look at the damage formula:

Code:
                                int iCollateralDamage = (iFlankingStrength * iDefenderDamage) / 100;
                                int iUnitDamage = std::max(pLoopUnit->getDamage(), std::min(pLoopUnit->getDamage() + iCollateralDamage, collateralDamageLimit()));

                                if (pLoopUnit->getDamage() != iUnitDamage)
                                {
                                    listFlankedUnits.push_back(std::make_pair(pLoopUnit, iUnitDamage));
                                }

The iDefenderDamage is once again a parameter of the method and comes from the initially fought unit. It's the damage that our unit does in a single round of combat. If you use BUGs Advanced Combat Odd Display you can see this damage per round in there. For a Horse Archer against a spearman on flat ground this would be 17 damage.
We multiply this damage with IFlankingStrength which acts as a modifier to the damage. In BtS this is 100 as shown above. In RtR and CtH this is 50 meaning we half the 17 damage of the horse archer to 8 (integer calculation)
The next line looks complicated, but untieing it reveals that we just add the 17 or 8 damage to the defending units cumulated damage, while making sure this damage does not go above the iCollateralDamageLimit = 100. Since all units have maxHP of 100 a iCollateralDamageLimit of 100 means the unit can die by the damage.
Lastly we check if the damage actually changed and if it did we put the unit together with it's new damaged value in a list called listFlankedUnits.


Now we haven't damaged the unit yet. This happens in the next code segment:

Code:
    int iNumUnitsHit = std::min((int)listFlankedUnits.size(), collateralDamageMaxUnits());

    for (int i = 0; i < iNumUnitsHit; ++i)
    {
        int iIndexHit = GC.getGameINLINE().getSorenRandNum(listFlankedUnits.size(), "Pick Flanked Unit");
        CvUnit* pUnit = listFlankedUnits[iIndexHit].first;
        int iDamage = listFlankedUnits[iIndexHit].second;
        pUnit->setDamage(iDamage, getOwnerINLINE());
        if (pUnit->isDead())
        {
            CvWString szBuffer = gDLL->getText("TXT_KEY_MISC_YOU_KILLED_UNIT_BY_FLANKING", getNameKey(), pUnit->getNameKey(), pUnit->getVisualCivAdjective(getTeam()));
            gDLL->getInterfaceIFace()->addMessage(getOwnerINLINE(), false, GC.getEVENT_MESSAGE_TIME(), szBuffer, GC.getEraInfo(GC.getGameINLINE().getCurrentEra()).getAudioUnitVictoryScript(), MESSAGE_TYPE_INFO, NULL, (ColorTypes)GC.getInfoTypeForString("COLOR_GREEN"), pPlot->getX_INLINE(), pPlot->getY_INLINE());
            szBuffer = gDLL->getText("TXT_KEY_MISC_YOUR_UNIT_DIED_BY_FLANKING", pUnit->getNameKey(), getNameKey(), getVisualCivAdjective(pUnit->getTeam()));
            gDLL->getInterfaceIFace()->addMessage(pUnit->getOwnerINLINE(), false, GC.getEVENT_MESSAGE_TIME(), szBuffer, GC.getEraInfo(GC.getGameINLINE().getCurrentEra()).getAudioUnitDefeatScript(), MESSAGE_TYPE_INFO, NULL, (ColorTypes)GC.getInfoTypeForString("COLOR_RED"), pPlot->getX_INLINE(), pPlot->getY_INLINE());

            pUnit->kill(false);
        }
       
        listFlankedUnits.erase(std::remove(listFlankedUnits.begin(), listFlankedUnits.end(), listFlankedUnits[iIndexHit]));
    }

First we get the maximum units to be damage, which is either the amount of units that can be damaged or iCollateralDamageMaxUnits from our unit. Whichever is smaller. With the exception of cavalry (7) and gunships (8) all units can damage a maximum of 6 units via flanking.

With this maximum value we run a for loop next. We take a random unit from our constructed list listFlankedUnits and damage that unit. If it's damaged enough it dies. Lastly we send messages out to inform the players. The important bit here is that if a unit is unlucky it can be damaged multiple times, while others go unharmed.

And that's all there is to the method.

Summary

The important bits from Flanking are the following:

- Flanking does not work in a city or culturally owned fort tile
- For each flanked unit a normal combat round is rolled against the odds of the initially defending unit.
- Withdrawel chance can reduce this odd significantly to the effect that 100% withdrawel guarantess damaging the unit via flanking
- The applied damage is the same as the damage applied during one round of combat against the initial defender
- iCollateralDamageMaxUnits is the maximum units that can be damaged, but be aware units are chosen at random and can be selected multiple times.
Mods: RtR    CtH

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

Buy me a coffee
Reply

(September 4th, 2022, 10:46)civac2 Wrote: @Charriu: Can mines outside of city bfc pop resources?

Totally forgot your question. I checked it:

Yes, the improvement needs to be worked and owned by you. You also need the tech that reveals the bonus resource in order to discover the resource.
Mods: RtR    CtH

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

Buy me a coffee
Reply

If you have time for another Curious Civplayer episode I would appreciate an explanation of how the Sentry promotion works. It does not seem to grant vision by any of the following possible means that I can think of:
- simply expand the vision by +1 tiles as advertised
- nor does it appear to work via "elevating" the vantage point of a unit (as from flat > hill or hill > mountain),
- nor does is work by expanding vision as if viewing from +1 ring of tiles (i.e. as if viewing from 1st ring of "culture" around the unit), which seems to be what is described in the last post at following CivFanatics explanation but does not appear to be the case as documented in my PB65 thread.

Thanks! I appreciate all you do for us!
Reply

So basically: How does vision work? That's an interesting topic worth investigating
Mods: RtR    CtH

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

Buy me a coffee
Reply



Forum Jump: