Modding combat mechanics?

tromozil

Chieftain
Joined
Jan 26, 2006
Messages
33
Hello,

I think Civ4 is the best game in the series, but it has one huuge disadvantage - it gets extremely slow in the late game. This kills all the fun for me, so I am trying to reduce the waiting. My idea is to reduce significantly the number of units in the game. Instead to represent a regiment or another small military unit, one game unit should represent, more or less, one division or army. It is similar to Civ V, but without the one-unit-per-tile rule.
To achieve this, I would like to:
1. increase the cost of all units
2. limit the combat duration to 1 or 2 rounds - both units should normally survive one combat; maybe 2 round, so that withdraw mechanics can still work;

The first one is quite simple - I just edit INITIAL_GOLD_PER_UNIT in GlobalDefines.xml.
The second one is too difficult for me. Can you ,please, give me some advice or tutorial how to do it?
 
I have been struggling with CvUnit.cpp for a while now, but it is quite unclear to me how it works. I tried some changes to void CvUnit::resolveCombat(CvUnit* pDefender, CvPlot* pPlot, CvBattleDefinition& kBattle), but doesn't work quite. At the end of the combat just one unit gets any damage.

Spoiler :
I replaced while (true) with for(int j=0;j<2;j++)


I know its lame, but I just had few hours of programming at school long time ago...
If anyone would bother helping a dummy, I will appreciate it very much! :)

EDIT: just tested with j<5 and the results are better => now both units get damage, but the results from my little statistic of about 50 combats do not correspond very well to the calculated odds.
 
I like what you doing. I think less units would make war more strategic and bigger maps more playable. I think you should also increase the building costs of units. Otherwise you can just build maximum sized army in few turns and attacking campaigns become impossible.

I'm not sure, but maybe your code doesn't go well with combat odds, because first strikes will play bigger role if combat rounds are reduced?

Btw. this thread probably belongs to main C&C forums.
 
Thanks for the reply! I was hoping for some tutorial on modding combat mechanics, but maybe you are right. Now I can not find a way to move the thread to C&C myself...

Do you know if there is a general modifier on unit building costs, or do I have to increase it manually for each unit?
 
it gets extremely slow in the late game. This kills all the fun for me, so I am trying to reduce the waiting.
You mean waiting for the AI to take its turn?
If that is the case, then the best option is to profile the game and see where the CPU time ends up. However even without measuring your game, I would say that you are likely right to target units as pathfinding is a big slowdown.

As for how to limit the number of rounds. Looking at CvUnit::resolveCombat(), I would think the interesting part is
PHP:
while (true)
Try changing that to a for loop and make it go max X turns and see what happens.

I'm not entirely sure it would work well with first strike. It appears that the vanilla idea of first strike is that the unit is immune to damage for the first X turns where X is the amount of times it has first strike. Limiting combat to two rounds mean a unit with 2*first strike will be immune to damage (at least if I read the code right).

Maybe a good start would be to figure out how many rounds it takes to kill a unit. First change the while to
PHP:
for (int iRound = 0; iRound < 65000; ++iRound)
Then at the end of the loop, add a display in the dead check.
PHP:
if (isDead() || pDefender->isDead())
{
	FAssertMsg(false, CvString::format("Unit died in %d rounds", iRound).c_str());
Build an assert build (with Makefile 2.x), run in window mode and wait for the assert messages to tell you how many rounds it takes to kill a unit. Naturally you should click ignore once and read multiple asserts to get an idea of how different the results can be. If it says something like 30, then maybe limit combat to 10 would do what you want. I think I have seen somebody experimenting with a 7 round limit as well.
 
Number of combat rounds: Air combat uses 5 rounds (INTERCEPTION_MAX_ROUNDS in GlobalDefines). Something like 5-8 should be right.

Displayed odds: The combat odds are computed separately from the combat resolution algorithm, I think in CvGameCoreUtils::getCombatOdds. Not trivial. Air combat already has a limited number of combat rounds (see CvUnit::resolveAirCombat), but I don't think air combat odds are computed anywhere, so it doesn't really help.
There are also three outcomes now: the attacker can kill the defender, vice versa, or both can survive; I guess the last case could be treated like a withdrawal in most regards. So the output format could remain "Attack odds: 72%; Retreat odds: 8%". This is handled by CvGameTextMgr.
Some other things you might want to look into:
* How is an undecided combat outcome displayed to the player; "your Axeman has withdrawn ...?" A custom message?
* How much combat experience is assigned?
* Does the AI count undecided attacks as war successes? What about war weariness?
* Do siege units in the defending stack receive flanking damage when the defender isn't killed?
* Do certain unit abilities become more or less powerful, in particular withdrawal, flank attack, healing, collateral damage and first strikes (see Nightinggale's post)? Do fast units get better?
* Does the round limit make it easier to defend cities? Are large stacks of weak units at a disadvantage?
* If the game balance does change, should there be a counterbalance; should the AI be adapted?
(From my own experience with writing a similar mod -- but not quite what you want.)

A recent thread with the same request: http://forums.civfanatics.com/showthread.php?t=536309
 
Nightinggale, thanks for your excellent answer!
My practical experience with C++ does not go much further than "Hello world!":), but I tried to play around with exactly that loop.

Now that you point out possible problems with the first strikes, I changed it like this:

Code:
int iRoundF123 = 0;
    while (iRoundF123<2)
    {
        iRoundF123++;
                ................
        if (getCombatFirstStrikes() > 0)
        {
            changeCombatFirstStrikes(-1);
            iRoundF123--;
        }

        if (pDefender->getCombatFirstStrikes() > 0)
        {
            pDefender->changeCombatFirstStrikes(-1);
            iRoundF123--;
        }
              ................
}

Do you find any errors in this code?

It seems to work, but once I hat 5 identical units attack 5 identical enemy units of the same type. The first three results were completely identical. Then I waited a little bit and fought the other 2 battles - their results were identical, but different than the first 3.

Another question: how can I change the combat odds, that show up before attacking a unit? I want to make them appropriate.
 
A lot of usefull information. Thanks, f1rpo! I'll have to look into all of those things, but my priority is to make units get any experience when killing an enemy. I can not understand why, but now they almost never do.
 
Do you find any errors in this code?

It seems to work, but once I hat 5 identical units attack 5 identical enemy units of the same type. The first three results were completely identical. Then I waited a little bit and fought the other 2 battles - their results were identical, but different than the first 3.
Each round has a winner, which mean there are two outcomes. With just two rounds, you have 22 = 4 possible results and the odds are not 25% for each. If we start to use the theory of probability, then it might not be the most likely output, but at the same time the odds for this particular output could be high enough to not be unusual.

The problem you have here with randomness and probability is a bit tricky unless you actually studied it. You made a test run with 5 combats. If you redo that with a new random number 1000 times, you may discover that your output happens 5% of the times. This mean by doing it a single time like you did, the odds for your outcome would be 1 out of 20.

Have fun resetting the setup and make that run 1000 times and record the results to see if the chance of each output is as you would like :p
 
I have made progress. It seems to work just as intended. :D

I play Pie's Ancient Europe mod. It does not modify the DLL file. I downloaded Lead From Behind DLL and source and used it as my base. I read in the forums, that LFB improves speed significantly. In CvUnit.cpp I edited only void CvUnit::resolveCombat like this:

Spoiler :
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;
int iAttackerDamage;
int iDefenderDamage;
int iDefenderOdds;

getDefenderCombatValues(*pDefender, pPlot, iAttackerStrength, iAttackerFirepower, iDefenderOdds, iDefenderStrength, iAttackerDamage, iDefenderDamage, &cdDefenderDetails);
int iAttackerKillOdds = iDefenderOdds * (100 - withdrawalProbability()) / 100;

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));
CvEventReporter::getInstance().genericEvent("combatLogCalc", pyArgsCD.makeFunctionArgs());
}

collateralCombat(pPlot, pDefender);

int iRoundF123 = 0;//tromozil
//while (true)
while (iRoundF123<4)//tromozil
{
iRoundF123++;//tromozil
// UncutDragon
// original
//if (GC.getGameINLINE().getSorenRandNum(GC.getDefineINT("COMBAT_DIE_SIDES"), "Combat") < iDefenderOdds)
// modified
if (GC.getGameINLINE().getSorenRandNum(GC.getCOMBAT_DIE_SIDES(), "Combat") < iDefenderOdds)
// /UncutDragon
{
if (getCombatFirstStrikes() == 0)
{
if (getDamage() + iAttackerDamage >= maxHitPoints() && GC.getGameINLINE().getSorenRandNum(100, "Withdrawal") < withdrawalProbability())
{
flankingStrikeCombat(pPlot, iAttackerStrength, iAttackerFirepower, iAttackerKillOdds, iDefenderDamage, pDefender);

changeExperience(GC.getDefineINT("EXPERIENCE_FROM_WITHDRAWL"), pDefender->maxXPValue(), true, pPlot->getOwnerINLINE() == getOwnerINLINE(), !pDefender->isBarbarian());
break;
}

changeDamage(iAttackerDamage, pDefender->getOwnerINLINE());

if (pDefender->getCombatFirstStrikes() > 0 && pDefender->isRanged())
{
kBattle.addFirstStrikes(BATTLE_UNIT_DEFENDER, 1);
kBattle.addDamage(BATTLE_UNIT_ATTACKER, BATTLE_TIME_RANGED, iAttackerDamage);
}

cdAttackerDetails.iCurrHitPoints = currHitPoints();

if (isHuman() || pDefender->isHuman())
{
CyArgsList pyArgs;
pyArgs.add(gDLL->getPythonIFace()->makePythonObject(&cdAttackerDetails));
pyArgs.add(gDLL->getPythonIFace()->makePythonObject(&cdDefenderDetails));
pyArgs.add(1);
pyArgs.add(iAttackerDamage);
CvEventReporter::getInstance().genericEvent("combatLogHit", pyArgs.makeFunctionArgs());
}
}
}
else
{
if (pDefender->getCombatFirstStrikes() == 0)
{
if (std::min(GC.getMAX_HIT_POINTS(), pDefender->getDamage() + iDefenderDamage) > combatLimit())
{
changeExperience(GC.getDefineINT("EXPERIENCE_FROM_WITHDRAWL"), pDefender->maxXPValue(), true, pPlot->getOwnerINLINE() == getOwnerINLINE(), !pDefender->isBarbarian());
pDefender->setDamage(combatLimit(), getOwnerINLINE());
break;
}

pDefender->changeDamage(iDefenderDamage, getOwnerINLINE());

if (getCombatFirstStrikes() > 0 && isRanged())
{
kBattle.addFirstStrikes(BATTLE_UNIT_ATTACKER, 1);
kBattle.addDamage(BATTLE_UNIT_DEFENDER, BATTLE_TIME_RANGED, iDefenderDamage);
}

cdDefenderDetails.iCurrHitPoints=pDefender->currHitPoints();

if (isHuman() || pDefender->isHuman())
{
CyArgsList pyArgs;
pyArgs.add(gDLL->getPythonIFace()->makePythonObject(&cdAttackerDetails));
pyArgs.add(gDLL->getPythonIFace()->makePythonObject(&cdDefenderDetails));
pyArgs.add(0);
pyArgs.add(iDefenderDamage);
CvEventReporter::getInstance().genericEvent("combatLogHit", pyArgs.makeFunctionArgs());
}
}
}

if (getCombatFirstStrikes() > 0)
{
changeCombatFirstStrikes(-1);
iRoundF123 = 0;//tromozil
}

if (pDefender->getCombatFirstStrikes() > 0)
{
pDefender->changeCombatFirstStrikes(-1);
iRoundF123 = 0;//tromozil
}

if (isDead() || pDefender->isDead())
{
if (isDead())
{
int iExperience = defenseXPValue();
iExperience = ( 1 + (iExperience * iAttackerStrength) / iDefenderStrength); //tromozil -> 1 +
iExperience = range(iExperience, GC.getDefineINT("MIN_EXPERIENCE_PER_COMBAT"), GC.getDefineINT("MAX_EXPERIENCE_PER_COMBAT"));
pDefender->changeExperience(iExperience, maxXPValue(), true, pPlot->getOwnerINLINE() == pDefender->getOwnerINLINE(), !isBarbarian());
}
else
{
flankingStrikeCombat(pPlot, iAttackerStrength, iAttackerFirepower, iAttackerKillOdds, iDefenderDamage, pDefender);

int iExperience = pDefender->attackXPValue();
iExperience = ( 1 + (iExperience * iDefenderStrength) / iAttackerStrength); //tromozil -> 1 +
iExperience = range(iExperience, GC.getDefineINT("MIN_EXPERIENCE_PER_COMBAT"), GC.getDefineINT("MAX_EXPERIENCE_PER_COMBAT"));
changeExperience(iExperience, pDefender->maxXPValue(), true, pPlot->getOwnerINLINE() == getOwnerINLINE(), !pDefender->isBarbarian());
}

break;
}
}
//tromozil:
if (!isDead() && !pDefender->isDead())
{
if (iAttackerDamage > iDefenderDamage)
{
changeExperience(GC.getDefineINT("EXPERIENCE_FROM_WITHDRAWL"), pDefender->maxXPValue(), true, pPlot->getOwnerINLINE() == getOwnerINLINE(), !pDefender->isBarbarian());
}
else
{
pDefender->changeExperience(GC.getDefineINT("EXPERIENCE_FROM_WITHDRAWL"), pDefender->maxXPValue(), true, pPlot->getOwnerINLINE() == getOwnerINLINE(), !pDefender->isBarbarian());
}
}
}


My changes are marked with //tromozil. You will see some changes by UncutDragon (LFB mod), but I don't think they have any significance for my combat rules.

I am in the middle of a long game now and it looks fine. I have few more questions:

- Do you know how the AI decides when to build a military unit and how it calculates the unit limit?

- After combat now I get a message saying that this or that unit has withdrawn from combat. I want to change it to something like "Your UNIT was victorious" or "..was defeated", depending who did more damage.

- I want to change the combat odd display to either:
Survival chance: xx% vs. xx%
or
Damage: min.-max. vs. min.-max.

I have not looked yet into CvGameTextMgr, but that is what I will do next.
Thanks for any advice!
 
tromozil said:
I have made progress. It seems to work just as intended. :D
:trophy2: Swift progress given your (lack of) experience with C++ and the Civ code.

AI unit production: The AI cities mostly decide autonomously (and randomly) what to build. The main function should be CvCityAI::AI_chooseProduction. iMaxUnitSpending appears to be the unit limit, and a unit build probability is returned by CvCityAI::AI_buildUnitProb.

Not counting the first strike rounds (as you do) sounds like the only reasonable approach; otherwise, units with many first strikes would be invincible.

A potential problem with drastically increased unit costs: Gold/ commerce may matter a lot more than production.
 
AI unit production: The AI cities mostly decide autonomously (and randomly) what to build. The main function should be CvCityAI::AI_chooseProduction. iMaxUnitSpending appears to be the unit limit, and a unit build probability is returned by CvCityAI::AI_buildUnitProb.

Thanks! Do you know what is getBuildUnitProb()? How is this value calculated? Can you tell me, what is iMaxUnitSpending - number of units, money or something else?
 
iMaxUnitSpending -- This gets compared with iUnitCostPercentage, i.e. the quotient unit cost / total cost (as a percentage). So, I'd say it's a target for that quotient. "Total cost" includes costs for cities and civics.
Spoiler :
Code:
int iUnitCostPercentage = (kPlayer.calculateUnitCost() * 100) /
std::max(1, kPlayer.calculatePreInflatedCosts());
...
...
if ((!bImportantCity || bDefenseWar)
&& (iUnitCostPercentage < iMaxUnitSpending)) {
  if (!bFinancialTrouble && !bGetBetterUnits && (bLandWar ||
  ((bAssault || kPlayer.AI_isDoStrategy(AI_STRATEGY_DAGGER))
  && !bAssaultAssist))) {
Note that bFinancialTrouble also prevents military build-up; this is set when the expenses exceed 60% of the income (72% when planning war); see CvPlayerAI::AI_isFinancialTrouble.

getBuildUnitProb -- It's defined for each AI leader in Assets\XML\Civilizations\LeaderHeadInfos.xml. Ragnar has one of the highest values (40%), Gandhi the lowest (15%).
 
Thanks for explaining, f1rpo! It seems to me, that the production mechanics should work well with my modifications. I will just play around with GlobalDefines.xml: unit cost and free units.

Now I try to make the right message appear after combat. There will be 4 different additional messages for all indecisive combats. Just for experimentation, I tried with 2 - victory and defeat. I entered the correct text into a xml file in folder "...\Assets\XML\Text":

Spoiler :
for experimental purposes there are only two messages
The positions of the unit names in the second message are changed - see updateCombat

<TEXT>
<Tag>TXT_KEY_VM_UNIT_COMBAT_VICTORY1</Tag>
<English>Your %s1_UnitName won a battle against a %s2_EnUName!</English>
<French> .......blabla....... </French>
<German> .......blabla....... </German>
<Italian> .......blabla....... </Italian>
<Spanish> .......blabla....... </Spanish>
</TEXT>
<TEXT>
<Tag>TXT_KEY_VM_UNIT_COMBAT_DEFEAT1</Tag>
<English>Your %s2_UnitName lost a battle against a %s1_EnUName!</English>
<French> .......blabla....... </French>
<German> .......blabla....... </German>
<Italian> .......blabla....... </Italian>
<Spanish> .......blabla....... </Spanish>
</TEXT>


Just above CvUnit::resolveCombat I declared my variable, that will determine what type of message should appear:
int iVictoryType123 = 0;
Inside resolveCombat I assign the value:

Spoiler :
if (!isDead() && !pDefender->isDead())
{
if (iAttackerDamage > iDefenderDamage)
{
changeExperience(GC.getDefineINT("EXPERIENCE_FROM_WITHDRAWL"), pDefender->maxXPValue(), true, pPlot->getOwnerINLINE() == getOwnerINLINE(), !pDefender->isBarbarian());
iVictoryType123 = 1;
}
else
{
pDefender->changeExperience(GC.getDefineINT("EXPERIENCE_FROM_WITHDRAWL"), pDefender->maxXPValue(), true, pPlot->getOwnerINLINE() == getOwnerINLINE(), !pDefender->isBarbarian());
iVictoryType123 = 2;
}
}


Then I edit the last lines of CvUnit::updateCombat in order to make the message appear:

Spoiler :

else
{
bAdvance = canAdvance(pPlot, ((pDefender->canDefend()) ? 1 : 0));

if (bAdvance)
{
if (!isNoCapture())
{
pDefender->setCapturingPlayer(getOwnerINLINE());
}
}


pDefender->kill(false);
pDefender = NULL;

if (!bAdvance)
{
changeMoves(std::max(GC.getMOVE_DENOMINATOR(), pPlot->movementCost(this, plot())));
checkRemoveSelectionAfterAttack();
}
}

if (pPlot->getNumVisibleEnemyDefenders(this) == 0)
{
getGroup()->groupMove(pPlot, true, ((bAdvance) ? this : NULL));
}

// This is is put before the plot advancement, the unit will always try to walk back
// to the square that they came from, before advancing.
getGroup()->clearMissionQueue();
}

else if (iVictoryType123 == 1)
{
iVictoryType123 = 0;
szBuffer = gDLL->getText("TXT_KEY_VM_UNIT_COMBAT_VICTORY1", getNameKey(), pDefender->getNameKey());
gDLL->getInterfaceIFace()->addMessage(getOwnerINLINE(), true, GC.getEVENT_MESSAGE_TIME(), szBuffer, "AS2D_OUR_WITHDRAWL", MESSAGE_TYPE_INFO, NULL, (ColorTypes)GC.getInfoTypeForString("COLOR_GREEN"), pPlot->getX_INLINE(), pPlot->getY_INLINE());
szBuffer = gDLL->getText("TXT_KEY_VM_UNIT_COMBAT_DEFEAT1", getNameKey(), pDefender->getNameKey());
gDLL->getInterfaceIFace()->addMessage(pDefender->getOwnerINLINE(), true, GC.getEVENT_MESSAGE_TIME(), szBuffer, "AS2D_THEIR_WITHDRAWL", MESSAGE_TYPE_INFO, NULL, (ColorTypes)GC.getInfoTypeForString("COLOR_RED"), pPlot->getX_INLINE(), pPlot->getY_INLINE());

changeMoves(std::max(GC.getMOVE_DENOMINATOR(), pPlot->movementCost(this, plot())));
checkRemoveSelectionAfterAttack();

getGroup()->clearMissionQueue();
}
else if (iVictoryType123 == 2)
{
iVictoryType123 = 0;
szBuffer = gDLL->getText("TXT_KEY_VM_UNIT_COMBAT_DEFEAT1", getNameKey(), pDefender->getNameKey());
gDLL->getInterfaceIFace()->addMessage(getOwnerINLINE(), true, GC.getEVENT_MESSAGE_TIME(), szBuffer, "AS2D_OUR_WITHDRAWL", MESSAGE_TYPE_INFO, NULL, (ColorTypes)GC.getInfoTypeForString("COLOR_RED"), pPlot->getX_INLINE(), pPlot->getY_INLINE());
szBuffer = gDLL->getText("TXT_KEY_VM_UNIT_COMBAT_VICTORY1", getNameKey(), pDefender->getNameKey());
gDLL->getInterfaceIFace()->addMessage(pDefender->getOwnerINLINE(), true, GC.getEVENT_MESSAGE_TIME(), szBuffer, "AS2D_THEIR_WITHDRAWL", MESSAGE_TYPE_INFO, NULL, (ColorTypes)GC.getInfoTypeForString("COLOR_GREEN"), pPlot->getX_INLINE(), pPlot->getY_INLINE());

changeMoves(std::max(GC.getMOVE_DENOMINATOR(), pPlot->movementCost(this, plot())));
checkRemoveSelectionAfterAttack();

getGroup()->clearMissionQueue();
}

else
{
szBuffer = gDLL->getText("TXT_KEY_MISC_YOU_UNIT_WITHDRAW", getNameKey(), pDefender->getNameKey());
gDLL->getInterfaceIFace()->addMessage(getOwnerINLINE(), true, GC.getEVENT_MESSAGE_TIME(), szBuffer, "AS2D_OUR_WITHDRAWL", MESSAGE_TYPE_INFO, NULL, (ColorTypes)GC.getInfoTypeForString("COLOR_GREEN"), pPlot->getX_INLINE(), pPlot->getY_INLINE());
szBuffer = gDLL->getText("TXT_KEY_MISC_ENEMY_UNIT_WITHDRAW", getNameKey(), pDefender->getNameKey());
gDLL->getInterfaceIFace()->addMessage(pDefender->getOwnerINLINE(), true, GC.getEVENT_MESSAGE_TIME(), szBuffer, "AS2D_THEIR_WITHDRAWL", MESSAGE_TYPE_INFO, NULL, (ColorTypes)GC.getInfoTypeForString("COLOR_RED"), pPlot->getX_INLINE(), pPlot->getY_INLINE());

changeMoves(std::max(GC.getMOVE_DENOMINATOR(), pPlot->movementCost(this, plot())));
checkRemoveSelectionAfterAttack();

getGroup()->clearMissionQueue();
}
}


I have no idea how szBuffer and addMessage work, I just copied those lines and changed them according my own interpretation. E.g. no idea what is AS2D_THEIR_WITHDRAWL and if it should be there at all...

Now I get the messages, but they are all wrong - e.g. lost, when I win. I am not sure if I declared the global variable correctly or maybe there is another fault...
 
I think iVictoryType123 == 1 is supposed to indicate that the attacker won, however, iAttackerDamage > iDefenderDamage actually means that the attacker took more damage. Other than that, it looks good.

AS2D_THEIR_WITHDRAWL appears to be the sound that is played. addMessage is not defined within the DLL, but declared in CvDLLInterfaceIFaceBase.h, and there, the respective parameter is called "pszSound".
 
iAttackerDamage > iDefenderDamage actually means that the attacker took more damage. Other than that, it looks good.

Yes, I've got it the wrong way:) I have made also plenty of other mistakes, but...

debugging is sooooo GREAT!!! It just brings light to the darkest parts of the code so easily. iAttackerDamage and iDefenderDamage are calculated just once in the begining of the combat, then each round the die is cast to check who gets the damage. This means that the actual damage at the end of the battle is a factor of iAttackerDamage/iDefenderDamage. For the messages to work properly, I don't have to compare both damage variables, but I have to multiply them with the correct number of actual hits.
I don't know if anyone might be interested, but later I will add the code.
 
Top Bottom