MNAI-U: unofficial build & bugfixes

MagisterCultuum

Great Sage
Joined
Feb 14, 2007
Messages
16,524
Location
Kael's head
Can you expose pUnit.setDelayedSpell(iSpell) to python?

At first I was only wanting to use it in def miscast(argsList): to clear the delayed spell so that the unit can properly cast spells again, but then I got to thinking it would be nice if worldbuilder could set units as being in the middle of casting delayed spells.



Any ETA on the next MNAI release? I was hoping to have the next MagisterModmod update (nothing major, mostly big fixes) out over a month ago, but have been delayed by household repairs. I just got back into it and may not have much time for play testing this month we have a work deadline approaching on the 22nd. It would of course be nice to be able to include a DLL that reduces the risks of crashes.
 

lfgr

Emperor
Joined
Feb 6, 2010
Messages
1,095
Can you expose pUnit.setDelayedSpell(iSpell) to python?
Sure, noted.
Any ETA on the next MNAI release? I was hoping to have the next MagisterModmod update (nothing major, mostly big fixes) out over a month ago, but have been delayed by household repairs. I just got back into it and may not have much time for play testing this month we have a work deadline approaching on the 22nd. It would of course be nice to be able to include a DLL that reduces the risks of crashes.
I expect to release relatively soon, but I need to do some more playtesting. Probably no later than the end of the month.
 

Deaf Metal

Warlord
Joined
Oct 5, 2015
Messages
114
Do you have any requests?
The resource icon shrinkage is the main one to me and I think it would be to other people too (saw the recent discussion that some people turn the display off presumably because the default size is obnoxious). Other things to consider: abilty to change what happens when a settler is selected, ability to toggle notifications for jungle/forest growth outside one's borders (I'd also suggest something specific for FfH: notifications for terraforming ocurring (maybe lair exploration too). I play with citizen automation always off and it's tedious when the illians are in the game to scroll over the map every turn to make sure new tundra/ice isn't being worked), toggle for autosave notifcations, toggle for contact greetings, ability to right-click to exit the city screen (don't remember if this is already in mnai), some edits to make the "Actual effects" displays more logical, and some bugfixes to ple (which in mnai is partially broken, such as the health/movement bars).
 

Alekseyev_

Warlord
Joined
Jul 18, 2014
Messages
259
If it is possible, I would like to request a small game option: A setting that will not prevent hell terrain (like the option that we have right now), but will cause the plot counter of terrain to slowly decrease again if the Infernals have been destroyed. It feels thematic to have the Infernals accompanied by hell terrain, but once hell terrain gets below mountains or the ocean, it's basically impossible to get rid of ever again, which causes annoying micromanagement. It would be nice to have the banishment of the infernals rewarded by a) stopping of hell terrain spread and b) hell terrain slowly disappearing on its own again.
Could make an exception for Ashen Veil aligned territory, where it can still persist.

Hope you see the appeal of my idea :D
 

MagisterCultuum

Great Sage
Joined
Feb 14, 2007
Messages
16,524
Location
Kael's head
If it is possible, I would like to request a small game option: A setting that will not prevent hell terrain (like the option that we have right now), but will cause the plot counter of terrain to slowly decrease again if the Infernals have been destroyed. It feels thematic to have the Infernals accompanied by hell terrain, but once hell terrain gets below mountains or the ocean, it's basically impossible to get rid of ever again, which causes annoying micromanagement. It would be nice to have the banishment of the infernals rewarded by a) stopping of hell terrain spread and b) hell terrain slowly disappearing on its own again.
Could make an exception for Ashen Veil aligned territory, where it can still persist.

Hope you see the appeal of my idea :D
This is not something that has to wait until the next release. It can be modded easily using only python.

You cannot add a new game option without hardcoding them into the DLL, but you can repurpose existing options.

lfgr already added 40 extra "dummy" game options that don't do anything, but can easily be made to do things with python.

The Hell terrain spread is handled in python in Firaxis Games\Sid Meier's Civilization 4\Beyond the Sword\Mods\More Naval AI\Assets\python\CustomFunctions.py under def doHellTerrain(self): starting in line 694

I haven't tested it, but I think this code should work
Spoiler :
Code:
    def doHellTerrain(self):
        iAshenVeil = gc.getInfoTypeForString('RELIGION_THE_ASHEN_VEIL')
        iBurningSands = gc.getInfoTypeForString('TERRAIN_BURNING_SANDS')
        iBanana = gc.getInfoTypeForString('BONUS_BANANA')
        iCotton = gc.getInfoTypeForString('BONUS_COTTON')
        iCorn = gc.getInfoTypeForString('BONUS_CORN')
        iCow = gc.getInfoTypeForString('BONUS_COW')
        iEvil = gc.getInfoTypeForString('ALIGNMENT_EVIL')
        iFarm = gc.getInfoTypeForString('IMPROVEMENT_FARM')
        iFlames = gc.getInfoTypeForString('FEATURE_FLAMES')
        iFlamesSpreadChance = gc.getDefineINT('FLAMES_SPREAD_CHANCE')
        iGulagarm = gc.getInfoTypeForString('BONUS_GULAGARM')
        iHorse = gc.getInfoTypeForString('BONUS_HORSE')
        iInfernal = gc.getInfoTypeForString('CIVILIZATION_INFERNAL')
        iMarble = gc.getInfoTypeForString('BONUS_MARBLE')
        iNeutral = gc.getInfoTypeForString('ALIGNMENT_NEUTRAL')
        iNightmare = gc.getInfoTypeForString('BONUS_NIGHTMARE')
        iPig = gc.getInfoTypeForString('BONUS_PIG')
        iRazorweed = gc.getInfoTypeForString('BONUS_RAZORWEED')
        iRice = gc.getInfoTypeForString('BONUS_RICE')
        iSheep = gc.getInfoTypeForString('BONUS_SHEEP')
        iSheutStone = gc.getInfoTypeForString('BONUS_SHEUT_STONE')
        iSilk = gc.getInfoTypeForString('BONUS_SILK')
        iSnakePillar = gc.getInfoTypeForString('IMPROVEMENT_SNAKE_PILLAR')
        iSugar = gc.getInfoTypeForString('BONUS_SUGAR')
        iToad = gc.getInfoTypeForString('BONUS_TOAD')
        iWheat = gc.getInfoTypeForString('BONUS_WHEAT')
        iForest = gc.getInfoTypeForString('FEATURE_FOREST')
        iJungle = gc.getInfoTypeForString('FEATURE_JUNGLE')
        iAForest = gc.getInfoTypeForString('FEATURE_FOREST_ANCIENT')
        iNForest = gc.getInfoTypeForString('FEATURE_FOREST_NEW')
        iBForest = gc.getInfoTypeForString('FEATURE_FOREST_BURNT')
        iCount = CyGame().getGlobalCounter()
      
        #Alekseyev_'s game option start
        bInfernalsVanquished = False
        if gc.getGame().isOption(gc.getInfoTypeForString('GAMEOPTION_DUMMY_01')):
            iPlayerInfernal = self.getCivilization(iInfernal)
            if iPlayerInfernal != -1:
                pPlayerInfernal = gc.getPlayer(iPlayerInfernal)
                bInfernalsVanquished = not pPlayerInfernal.isAlive()
        #Alekseyev_'s game option stop

        for i in range (CyMap().numPlots()):
            pPlot = CyMap().plotByIndex(i)
            iFeature = pPlot.getFeatureType()
            iTerrain = pPlot.getTerrainType()
            iBonus = pPlot.getBonusType(-1)
            iImprovement = pPlot.getImprovementType()
            bUntouched = True
            if pPlot.isOwned():
                pPlayer = gc.getPlayer(pPlot.getOwner())
                iAlignment = pPlayer.getAlignment()
                if pPlayer.getCivilizationType() == iInfernal:
                    pPlot.changePlotCounter(100)
                    bUntouched = False
                if (bUntouched and pPlayer.getStateReligion() == iAshenVeil or (iCount >= 50 and iAlignment == iEvil) or (iCount >= 75 and iAlignment == iNeutral)):
                    iX = pPlot.getX()
                    iY = pPlot.getY()
                    for iiX in range(iX-1, iX+2, 1):
                        for iiY in range(iY-1, iY+2, 1):
                            pAdjacentPlot = CyMap().plot(iiX,iiY)
                            if pAdjacentPlot.isNone() == False:
                                if pAdjacentPlot.getPlotCounter() > 10:
                                    pPlot.changePlotCounter(1)
                                    bUntouched = False
            if (bUntouched and pPlot.isOwned() == False and iCount > 25):
                iX = pPlot.getX()
                iY = pPlot.getY()
                for iiX in range(iX-1, iX+2, 1):
                    for iiY in range(iY-1, iY+2, 1):
                        pAdjacentPlot = CyMap().plot(iiX,iiY)
                        if pAdjacentPlot.isNone() == False:
                            if pAdjacentPlot.getPlotCounter() > 10:
                                pPlot.changePlotCounter(1)
                                bUntouched = False
            iPlotCount = pPlot.getPlotCounter()
            if (bUntouched and iPlotCount > 0):
                pPlot.changePlotCounter(-1)
              
            if bInfernalsVanquished:#Alekseyev_'s game option
                pPlot.changePlotCounter(-1)#Alekseyev_'s game option
              
            if iPlotCount > 9:
                if (iBonus == iSheep or iBonus == iPig):
                    pPlot.setBonusType(iToad)
                if (iBonus == iHorse or iBonus == iCow):
                    pPlot.setBonusType(iNightmare)
                if (iBonus == iCotton or iBonus == iSilk):
                    pPlot.setBonusType(iRazorweed)
                if (iBonus == iBanana or iBonus == iSugar):
                    pPlot.setBonusType(iGulagarm)
                if (iBonus == iMarble):
                    pPlot.setBonusType(iSheutStone)
                if (iBonus == iCorn or iBonus == iRice or iBonus == iWheat):
                    pPlot.setBonusType(-1)
                    pPlot.setImprovementType(iSnakePillar)

                if (iFeature == iForest or iFeature == iAForest or iFeature == iNForest or iFeature == iJungle):
                    iRandom = CyGame().getSorenRandNum(100, "Hell Terrain Burnt Forest")
                    if iRandom < 10:
                        pPlot.setFeatureType(iBForest, 0)
                if pPlot.isPeak() == True:
                    iRandom = CyGame().getSorenRandNum(1000, "Hell Terrain Volcanos")
                    if iRandom < 2:
                        iEvent = CvUtil.findInfoTypeNum(gc.getEventTriggerInfo, gc.getNumEventTriggerInfos(), 'EVENTTRIGGER_VOLCANO_CREATION')
                        if pPlot.isOwned():
                            triggerData = pPlayer.initTriggeredData(iEvent, True, -1, pPlot.getX(), pPlot.getY(), -1, -1, -1, -1, -1, -1)

            if iPlotCount < 10:
                if iBonus == iToad:
                    if CyGame().getSorenRandNum(100, "Hell Convert") < 50:
                        pPlot.setBonusType(iSheep)
                    else:
                        pPlot.setBonusType(iPig)
                if iBonus == iNightmare:
                    if CyGame().getSorenRandNum(100, "Hell Convert") < 50:
                        pPlot.setBonusType(iHorse)
                    else:
                        pPlot.setBonusType(iCow)
                if iBonus == iRazorweed:
                    if CyGame().getSorenRandNum(100, "Hell Convert") < 50:
                        pPlot.setBonusType(iCotton)
                    else:
                        pPlot.setBonusType(iSilk)
                if iBonus == iGulagarm:
                    if CyGame().getSorenRandNum(100, "Hell Convert") < 50:
                        pPlot.setBonusType(iBanana)
                    else:
                        pPlot.setBonusType(iSugar)
                if (iBonus == iSheutStone):
                    pPlot.setBonusType(iMarble)
                if iImprovement == iSnakePillar:
                    pPlot.setImprovementType(iFarm)
                    iCount = CyGame().getSorenRandNum(100, "Hell Convert")
                    if  iCount < 33:
                        pPlot.setBonusType(iCorn)
                    else:
                        if iCount < 66:
                            pPlot.setBonusType(iRice)
                        else:
                            pPlot.setBonusType(iWheat)
            if iTerrain == iBurningSands:
                if pPlot.isCity() == False:
                    if pPlot.isPeak() == False:
                        if CyGame().getSorenRandNum(100, "Flames") < iFlamesSpreadChance:
                            pPlot.setFeatureType(iFlames, 0)

However, there could be issues in scenarios like Lord of the Balors where there are multiple Infernal players, or perhaps if revolutions leads to ome other demon lords entering the game besides just Hyborem. The above code only checks the first Infernal player.


You'd have to decide whether you want the plot counters to do down for each vanquished demon lord or only if all of them are gone.

I think this version should make hell terrain slowly retreat only if all Infernal players are dead.
Spoiler :
Code:
    def doHellTerrain(self):
        iAshenVeil = gc.getInfoTypeForString('RELIGION_THE_ASHEN_VEIL')
        iBurningSands = gc.getInfoTypeForString('TERRAIN_BURNING_SANDS')
        iBanana = gc.getInfoTypeForString('BONUS_BANANA')
        iCotton = gc.getInfoTypeForString('BONUS_COTTON')
        iCorn = gc.getInfoTypeForString('BONUS_CORN')
        iCow = gc.getInfoTypeForString('BONUS_COW')
        iEvil = gc.getInfoTypeForString('ALIGNMENT_EVIL')
        iFarm = gc.getInfoTypeForString('IMPROVEMENT_FARM')
        iFlames = gc.getInfoTypeForString('FEATURE_FLAMES')
        iFlamesSpreadChance = gc.getDefineINT('FLAMES_SPREAD_CHANCE')
        iGulagarm = gc.getInfoTypeForString('BONUS_GULAGARM')
        iHorse = gc.getInfoTypeForString('BONUS_HORSE')
        iInfernal = gc.getInfoTypeForString('CIVILIZATION_INFERNAL')
        iMarble = gc.getInfoTypeForString('BONUS_MARBLE')
        iNeutral = gc.getInfoTypeForString('ALIGNMENT_NEUTRAL')
        iNightmare = gc.getInfoTypeForString('BONUS_NIGHTMARE')
        iPig = gc.getInfoTypeForString('BONUS_PIG')
        iRazorweed = gc.getInfoTypeForString('BONUS_RAZORWEED')
        iRice = gc.getInfoTypeForString('BONUS_RICE')
        iSheep = gc.getInfoTypeForString('BONUS_SHEEP')
        iSheutStone = gc.getInfoTypeForString('BONUS_SHEUT_STONE')
        iSilk = gc.getInfoTypeForString('BONUS_SILK')
        iSnakePillar = gc.getInfoTypeForString('IMPROVEMENT_SNAKE_PILLAR')
        iSugar = gc.getInfoTypeForString('BONUS_SUGAR')
        iToad = gc.getInfoTypeForString('BONUS_TOAD')
        iWheat = gc.getInfoTypeForString('BONUS_WHEAT')
        iForest = gc.getInfoTypeForString('FEATURE_FOREST')
        iJungle = gc.getInfoTypeForString('FEATURE_JUNGLE')
        iAForest = gc.getInfoTypeForString('FEATURE_FOREST_ANCIENT')
        iNForest = gc.getInfoTypeForString('FEATURE_FOREST_NEW')
        iBForest = gc.getInfoTypeForString('FEATURE_FOREST_BURNT')
        iCount = CyGame().getGlobalCounter()
      
        #Alekseyev_'s game option start
        bInfernalsVanquished = False
        if gc.getGame().isOption(gc.getInfoTypeForString('GAMEOPTION_DUMMY_01')):#Make this extra game option cause Hell terrain to fade once the Infernals are vanquished
            iNumDemonLords = 0
            iNumVanquishedDemonLords = 0
            for iPlayer2 in xrange(gc.getMAX_PLAYERS()):
                pPlayer2 = gc.getPlayer(iPlayer2)
                if pPlayer2.getCivilizationType() == iInfernal:
                    iNumDemonLords += 1
                    if not pPlayer2.isAlive():
                        iNumVanquishedDemonLords += 1
            if iNumDemonLords > 0:
                if iNumVanquishedDemonLords == iNumDemonLords:
                    bInfernalsVanquished = True
        #Alekseyev_'s game option stop
      
        for i in range (CyMap().numPlots()):
            pPlot = CyMap().plotByIndex(i)
            iFeature = pPlot.getFeatureType()
            iTerrain = pPlot.getTerrainType()
            iBonus = pPlot.getBonusType(-1)
            iImprovement = pPlot.getImprovementType()
            bUntouched = True
            if pPlot.isOwned():
                pPlayer = gc.getPlayer(pPlot.getOwner())
                iAlignment = pPlayer.getAlignment()
                if pPlayer.getCivilizationType() == iInfernal:
                    pPlot.changePlotCounter(100)
                    bUntouched = False
                if (bUntouched and pPlayer.getStateReligion() == iAshenVeil or (iCount >= 50 and iAlignment == iEvil) or (iCount >= 75 and iAlignment == iNeutral)):
                    iX = pPlot.getX()
                    iY = pPlot.getY()
                    for iiX in range(iX-1, iX+2, 1):
                        for iiY in range(iY-1, iY+2, 1):
                            pAdjacentPlot = CyMap().plot(iiX,iiY)
                            if pAdjacentPlot.isNone() == False:
                                if pAdjacentPlot.getPlotCounter() > 10:
                                    pPlot.changePlotCounter(1)
                                    bUntouched = False
            if (bUntouched and pPlot.isOwned() == False and iCount > 25):
                iX = pPlot.getX()
                iY = pPlot.getY()
                for iiX in range(iX-1, iX+2, 1):
                    for iiY in range(iY-1, iY+2, 1):
                        pAdjacentPlot = CyMap().plot(iiX,iiY)
                        if pAdjacentPlot.isNone() == False:
                            if pAdjacentPlot.getPlotCounter() > 10:
                                pPlot.changePlotCounter(1)
                                bUntouched = False
            iPlotCount = pPlot.getPlotCounter()
            if (bUntouched and iPlotCount > 0):
                pPlot.changePlotCounter(-1)
              
            if bInfernalsVanquished:#Alekseyev_'s game option
                pPlot.changePlotCounter(-1)#Alekseyev_'s game option
              
            if iPlotCount > 9:
                if (iBonus == iSheep or iBonus == iPig):
                    pPlot.setBonusType(iToad)
                if (iBonus == iHorse or iBonus == iCow):
                    pPlot.setBonusType(iNightmare)
                if (iBonus == iCotton or iBonus == iSilk):
                    pPlot.setBonusType(iRazorweed)
                if (iBonus == iBanana or iBonus == iSugar):
                    pPlot.setBonusType(iGulagarm)
                if (iBonus == iMarble):
                    pPlot.setBonusType(iSheutStone)
                if (iBonus == iCorn or iBonus == iRice or iBonus == iWheat):
                    pPlot.setBonusType(-1)
                    pPlot.setImprovementType(iSnakePillar)

                if (iFeature == iForest or iFeature == iAForest or iFeature == iNForest or iFeature == iJungle):
                    iRandom = CyGame().getSorenRandNum(100, "Hell Terrain Burnt Forest")
                    if iRandom < 10:
                        pPlot.setFeatureType(iBForest, 0)
                if pPlot.isPeak() == True:
                    iRandom = CyGame().getSorenRandNum(1000, "Hell Terrain Volcanos")
                    if iRandom < 2:
                        iEvent = CvUtil.findInfoTypeNum(gc.getEventTriggerInfo, gc.getNumEventTriggerInfos(), 'EVENTTRIGGER_VOLCANO_CREATION')
                        if pPlot.isOwned():
                            triggerData = pPlayer.initTriggeredData(iEvent, True, -1, pPlot.getX(), pPlot.getY(), -1, -1, -1, -1, -1, -1)

            if iPlotCount < 10:
                if iBonus == iToad:
                    if CyGame().getSorenRandNum(100, "Hell Convert") < 50:
                        pPlot.setBonusType(iSheep)
                    else:
                        pPlot.setBonusType(iPig)
                if iBonus == iNightmare:
                    if CyGame().getSorenRandNum(100, "Hell Convert") < 50:
                        pPlot.setBonusType(iHorse)
                    else:
                        pPlot.setBonusType(iCow)
                if iBonus == iRazorweed:
                    if CyGame().getSorenRandNum(100, "Hell Convert") < 50:
                        pPlot.setBonusType(iCotton)
                    else:
                        pPlot.setBonusType(iSilk)
                if iBonus == iGulagarm:
                    if CyGame().getSorenRandNum(100, "Hell Convert") < 50:
                        pPlot.setBonusType(iBanana)
                    else:
                        pPlot.setBonusType(iSugar)
                if (iBonus == iSheutStone):
                    pPlot.setBonusType(iMarble)
                if iImprovement == iSnakePillar:
                    pPlot.setImprovementType(iFarm)
                    iCount = CyGame().getSorenRandNum(100, "Hell Convert")
                    if  iCount < 33:
                        pPlot.setBonusType(iCorn)
                    else:
                        if iCount < 66:
                            pPlot.setBonusType(iRice)
                        else:
                            pPlot.setBonusType(iWheat)
            if iTerrain == iBurningSands:
                if pPlot.isCity() == False:
                    if pPlot.isPeak() == False:
                        if CyGame().getSorenRandNum(100, "Flames") < iFlamesSpreadChance:
                            pPlot.setFeatureType(iFlames, 0)



You would probably also want to edit Mods\More Naval AI\Assets\XML\Gameinfo\CIV4GameOptionInfos.xml to change the description of whatever game option you use.
Code:
        <GameOptionInfo>
            <Type>GAMEOPTION_DUMMY_01</Type>
            <Description>Dummy</Description>
            <Help>Dummy</Help>
            <bDefault>0</bDefault>
            <bVisible>0</bVisible>
        </GameOptionInfo>
You need to change <bVisible> to 1 to let you see the option and be able to turn it on for your games.

You probably want to change the description tag. You can write something like "Hell retreats when the Infernals are vanquished" right in that spot or make a TXT_KEY to place in one of the text files to had descriptions in multiple languages.

You can change the Type tag too. If it is renamed something like GAMEOPTION_HELL_RETREATS then you of course have to use the new name in the python too.
 
Last edited:

Alekseyev_

Warlord
Joined
Jul 18, 2014
Messages
259
That's interesting, thank you Magister! I wasn't aware that there were dummy options for usage already.

Might still be worth including your code in an official update if it is deemed interesting enough for others to have as well. I'll certainly test it when I have time in the next days.

Edit: On the topic of hell terrain, what do you think about restoring Obsidian Plains functionality as a floodplains replacement? The Obsidian Plains are in the game, they're just not set up to be exchanged with floodplains upon hell terrain conversion, and therefore hell terrain irreversibly ruins cities near desert rivers.
 
Last edited:

Alekseyev_

Warlord
Joined
Jul 18, 2014
Messages
259
While looking at the mod files I came across this code:

1696437729679.png


From my understanding, this means that AI players will only ever use the Escape ability on units that are undamaged or up to 50% damaged, but not on significantly damaged units. Shouldn't this be the other way round, with combat-unviable units being sent to the capital so they can heal and be used again?
 

lfgr

Emperor
Joined
Feb 6, 2010
Messages
1,095
No, it uses it when the damage is 50% or more. caster.getDamage() is 0 for undamaged units.
EDIT: Ah, I see, return False. Yeah, looks like you're right.
EDIT2: I checked, AI currently doesn't use the spell since it has <iAIWeight> zero. If I increase that, the spell is used but causes an assertion. I will investigate this after the next release.
 
Last edited:

MagisterCultuum

Great Sage
Joined
Feb 14, 2007
Messages
16,524
Location
Kael's head
No, it uses it when the damage is 50% or more. caster.getDamage() is 0 for undamaged units.
EDIT: Ah, I see, return False. Yeah, looks like you're right.
EDIT2: I checked, AI currently doesn't use the spell since it has <iAIWeight> zero. If I increase that, the spell is used but causes an assertion. I will investigate this after the next release.
Is it always causing an exception, or only if the AI unit belongs to a player who does not have a capital city?
This is the MNAI Escape code:
Code:
        <SpellInfo>
            <Type>SPELL_ESCAPE</Type>
            <Description>TXT_KEY_SPELL_ESCAPE</Description>
            <Civilopedia>TXT_KEY_SPELL_ESCAPE_PEDIA</Civilopedia>
            <Help>TXT_KEY_SPELL_ESCAPE_HELP</Help>
            <UnitPrereq>UNIT_CHANTER</UnitPrereq>
            <bAllowAI>1</bAllowAI>
            <bDisplayWhenDisabled>1</bDisplayWhenDisabled>
            <bHasCasted>1</bHasCasted>
            <PyResult>spellTeleport(pCaster,'Capital')</PyResult>
            <PyRequirement>reqEscape(pCaster)</PyRequirement>
            <Effect>EFFECT_SPELL2</Effect>
            <Sound>AS3D_SPELL_ESCAPE</Sound>
            <Button>Art/Interface/Buttons/Spells/Escape.dds</Button>
        </SpellInfo>


def reqEscape(caster):
    if caster.getOwner() == gc.getBARBARIAN_PLAYER():
        return False
    pPlayer = gc.getPlayer(caster.getOwner())
    if pPlayer.isHuman() == False:
        if caster.getDamage() >= 50:
            return False
    return True

def spellTeleport(caster,loc):
    player = caster.getOwner()
    pPlayer = gc.getPlayer(player)
    pCity = pPlayer.getCapitalCity()
    caster.setXY(pCity.getX(), pCity.getY(), False, True, True)

It seems pretty obvious why there would be a problem with the AI using it when they don't have a capital city. The code checks to prevent barbarian units from casting, since the barbs cannot have a capital, but never checks for other AI civs have cities to which they can escape. The Mercurian Gate and the Rebel mechanic can definitely lead to situations when there is no where to go.



For comparison, this is the way the escape spell(s) currently work in my modmod:
Code:
        <SpellInfo>
            <Type>SPELL_ESCAPE_GREATER</Type>
            <Description>TXT_KEY_SPELL_ESCAPE</Description>
            <Civilopedia>TXT_KEY_SPELL_ESCAPE_PEDIA</Civilopedia>
            <Help>TXT_KEY_SPELL_ESCAPE_HELP</Help>
            <PromotionPrereq1>PROMOTION_AFFINITY_DIMENSIONAL</PromotionPrereq1>
            <bAllowAI>1</bAllowAI>
            <bDisplayWhenDisabled>1</bDisplayWhenDisabled>
            <bIgnoreHasCasted>0</bIgnoreHasCasted>
            <PyResult>spellEscape(pCaster, eSpell)</PyResult>
            <PyRequirement>reqEscape(pCaster, eSpell)</PyRequirement>
            <PyHelp>helpEscape(lpUnits, eSpell)</PyHelp>
            <Effect>EFFECT_SPELL2</Effect>
            <Sound>AS3D_SPELL_ESCAPE</Sound>
            <Button>Art/Interface/Buttons/Spells/Escape.dds</Button>
        </SpellInfo>
        <SpellInfo>
            <Type>SPELL_ESCAPE_FACILITATED</Type>
            <Description>TXT_KEY_SPELL_ESCAPE_FACILITATED</Description>
            <Civilopedia>TXT_KEY_SPELL_ESCAPE_PEDIA</Civilopedia>
            <Help>TXT_KEY_SPELL_ESCAPE_HELP</Help>
            <PromotionInStackPrereq>PROMOTION_AFFINITY_DIMENSIONAL</PromotionInStackPrereq>
            <bAllowAI>1</bAllowAI>
            <bDisplayWhenDisabled>0</bDisplayWhenDisabled>
            <bHasCasted>1</bHasCasted>
            <iMiscastChance>35</iMiscastChance>
            <PyMiscast>miscastEscape(pCaster, eSpell)</PyMiscast>
            <PyResult>spellEscape(pCaster, eSpell)</PyResult>
            <PyRequirement>reqEscapeFacilitated(pCaster, eSpell)</PyRequirement>
            <PyHelp>helpEscape(lpUnits, eSpell)</PyHelp>
            <Effect>EFFECT_SPELL2</Effect>
            <Sound>AS3D_SPELL_ESCAPE</Sound>
            <Button>Art/Interface/Buttons/Spells/EscapeFacilitated.dds</Button>
        </SpellInfo>
        <SpellInfo>
            <Type>SPELL_ESCAPE</Type>
            <Description>TXT_KEY_SPELL_ESCAPE_LESSER</Description>
            <Civilopedia>TXT_KEY_SPELL_ESCAPE_PEDIA</Civilopedia>
            <Help>TXT_KEY_SPELL_ESCAPE_HELP</Help>
            <PromotionPrereq1>PROMOTION_DIMENSIONAL1</PromotionPrereq1>
            <bAllowAI>1</bAllowAI>
            <bDisplayWhenDisabled>1</bDisplayWhenDisabled>
            <bHasCasted>1</bHasCasted>
            <iMiscastChance>25</iMiscastChance>
            <PyMiscast>miscastEscape(pCaster, eSpell)</PyMiscast>
            <PyResult>spellEscape(pCaster, eSpell)</PyResult>
            <PyRequirement>reqEscape(pCaster, eSpell)</PyRequirement>
            <PyHelp>helpEscape(lpUnits, eSpell)</PyHelp>
            <Effect>EFFECT_SPELL2</Effect>
            <Sound>AS3D_SPELL_ESCAPE</Sound>
            <Button>Art/Interface/Buttons/Spells/EscapeLesser.dds</Button>
        </SpellInfo>


def reqEscape(pCaster, eSpell=-1):
    if not reqDimensionalAllowed(pCaster):
        return False
    iPlayer = pCaster.getOwner()
    pPlayer = gc.getPlayer(iPlayer)
    if not pPlayer.isHuman():
        if pCaster.getDamage() < 50:
            return False
    pPlot = -1
    if pCaster.getSummoner() != -1:
        pSummoner = pPlayer.getUnit(pCaster.getSummoner())
        if not pSummoner.isNone():
            if not pCaster.atPlot(pSummoner.plot()):
                pPlot = pSummoner.plot()
    if pPlot == -1:
        pCapital = pPlayer.getCapitalCity()
        if pCapital.isNone():
            return False
        if pCapital.atPlot(pCaster.plot()):
            return False
        if not pCapital.isCoastal(1):
            if pCaster.getDomainType() == gc.getInfoTypeForString('DOMAIN_SEA'):
                return False
    return True

def reqEscapeFacilitated(pCaster, eSpell=1):
    if pCaster.isHasPromotion(gc.getInfoTypeForString('PROMOTION_DIMENSIONAL1')):
        return False
##    if pCaster.isHasPromotion(gc.getInfoTypeForString('PROMOTION_AFFINITY_DIMENSIONAL')):#I believe <PromotionInStackPrereq> already eliminates these units
##        return False
    return reqEscape(pCaster, eSpell)

def miscastEscape(pCaster, eSpell):
    pPlot = pCaster.plot()

    bWater = pCaster.getDomainType() == gc.getInfoTypeForString('DOMAIN_SEA')

    iBestValue = 0
    pBestPlot = -1
    for i in xrange (CyMap().numPlots()):
        iValue = 0
        pTargetPlot = CyMap().plotByIndex(i)
        if bWater == pTargetPlot.isWater():
            iValue = CyGame().getSorenRandNum(1000, "Escape miscast move "+ str(pCaster.getName()))
            if not pTargetPlot.isOwned():
                iValue += 1000
            if pTargetPlot == pPlot:
                iValue = 0
            if pTargetPlot.isCity():
                iValue = 0
            if iValue > iBestValue:
                iBestValue = iValue
                pBestPlot = pTargetPlot
    if pBestPlot != -1:
        pCaster.setXY(pBestPlot.getX(), pBestPlot.getY(), False, True, True)

def spellEscape(pCaster, eSpell=-1):
    iPlayer = pCaster.getOwner()
    pPlayer = gc.getPlayer(iPlayer)
    pPlot = -1
    pCapital = pPlayer.getCapitalCity()
    if not pCapital.isNone():
        if pCapital.isCoastal(1) or pCaster.getDomainType() != gc.getInfoTypeForString('DOMAIN_SEA'):
            pPlot = pCapital.plot()
    if pPlot == -1:
        if pCaster.getSummoner() != -1:
            pSummoner = pPlayer.getUnit(pCaster.getSummoner())
            if not pSummoner.isNone():
                if not pCaster.atPlot(pSummoner.plot()):
                    pPlot = pSummoner.plot()
    if pPlot != -1:
        pCaster.setXY(pPlot.getX(), pPlot.getY(), False, True, True)

def helpEscape(lpUnits, eSpell=1):
    szBuffer = ''
    pCaster = lpUnits[0]
    iPlayer = pCaster.getOwner()
    pPlayer = gc.getPlayer(iPlayer)
    sNameRefuge = ''
    for pCaster in lpUnits:
        iX1 = pCaster.getX()
        iY1 = pCaster.getY()
        pExitPlot = -1
        iX2 = -1
        iY2 = -1
        pCapital = pPlayer.getCapitalCity()
        if not pCapital.isNone():
            if pCapital.isCoastal(1) or pCaster.getDomainType() != gc.getInfoTypeForString('DOMAIN_SEA'):
                if not pCaster.atPlot(pCapital.plot()):
                    pExitPlot = pCapital.plot()
                    iX2 = pCapital.getX()
                    iY2 = pCapital.getY()
                    sNameRefuge ="<color=%d,%d,%d,%d>%s</color>" %(pPlayer.getPlayerTextColorR(), pPlayer.getPlayerTextColorG(), pPlayer.getPlayerTextColorB(), pPlayer.getPlayerTextColorA(), pCapital.getName())

        if pExitPlot == -1:
            if pCaster.getSummoner() != -1:
                pSummoner = pPlayer.getUnit(pCaster.getSummoner())
                if not pSummoner.isNone():
                    if not pCaster.atPlot(pSummoner.plot()):
                        pExitPlot = pSummoner.plot()
                        iX2 = pSummoner.getX()
                        iY2 = pSummoner.getY()
                        sNameRefuge = cf.getNameWithColorScheme(pSummoner, pSummoner.isInvisible(pCaster.getTeam(), False))
        if not (pExitPlot == -1 or pExitPlot.isNone()):
            sList = cf.getNameWithColorScheme(pCaster)
            szBuffer += localText.getText("TXT_KEY_SPELL_ESCAPE", ()) + ' to ' + sNameRefuge
            idX = iX2 - iX1
            if idX != 0:
                sdX = 'East'
                if idX < 0:
                    idX = -idX
                    sdX = 'West'
                szBuffer += localText.getText("TXT_KEY_HELP_MOVE_UNIT", (sList,idX, sdX, ))
            idY = iY2 - iY1
            if idY != 0:
                sdY = 'North'
                if idY < 0:
                    idY = -idY
                    sdY = 'South'
                szBuffer += localText.getText("TXT_KEY_HELP_MOVE_UNIT", (sList,idY, sdY, ))
    szBuffer += helpManaBlocked(lpUnits, 'VOTE_NO_DIMENSIONAL_MANA')
    return szBuffer

I currently have it so that summons will escape to their summoner instead of to the capital, but I think I may change that in my next release..

I'm thinking it might be better to make the spell cause the unit to escape to the closest team city, and then to the capital if cast from within a different city. Or maybe just make it always make the unit escape to the closest friendly city skipping any city on the unit's current plot.

That should be fairly easy to do using the function
CyCity findCity (INT iX, INT iY, PlayerType eOwner, TeamType eTeam, BOOL bSameArea, BOOL bCoastalOnly, TeamType eTeamAtWarWith, DirectionType eDirection, CyCity pSkipCity)
CyCity* (int iX, int iY, int (PlayerTypes) eOwner = NO_PLAYER, int (TeamTypes) eTeam = NO_TEAM, bool bSameArea = true, bool bCoastalOnly = false, int (TeamTypes) eTeamAtWarWith = NO_TEAM, int (DirectionTypes) eDirection = NO_DIRECTION, CvCity* pSkipCity = NULL) - finds city
 

lfgr

Emperor
Joined
Feb 6, 2010
Messages
1,095
You're right that it fails if the player has no capital city. (Is this currently possible, though? I think puppet states currently do get capitals.)
But that wasn't the problem, I got an assertion (in the DLL) complaining about the unit's coordinates not being right (and then an assertion about an infinite AI loop). The Wildmana AI code Tholal merged lets the AI cast spells at a weird time in the update cycle. There's probably an implicit assumption somewhere that the unit doesn't change location when casting the spell.
 

MagisterCultuum

Great Sage
Joined
Feb 14, 2007
Messages
16,524
Location
Kael's head
You're right that it fails if the player has no capital city. (Is this currently possible, though? I think puppet states currently do get capitals.)
But that wasn't the problem, I got an assertion (in the DLL) complaining about the unit's coordinates not being right (and then an assertion about an infinite AI loop). The Wildmana AI code Tholal merged lets the AI cast spells at a weird time in the update cycle. There's probably an implicit assumption somewhere that the unit doesn't change location when casting the spell.
Puppet states get capitals, but Rebels may not. I cannot recall if I've ever seen Adepts among the units generated by rebellions before they capture a city though. (Based on the lore, a Dimensional mage leading a rebellion makes almost as much sense as Fire/Spirit mages like Soren Castamer instigating the revolt.)


That reminded me of a minor issue I've come across in my modmod, where I have Spellstaffs passively allow 2 spells per turn. A lot of spells disappear from the options a unit has after being cast once, only reappearing if I select another unit and then go back to that caster. Also, it can be annoying when casting one spell causes a unit to be deselected when it should be able to cast a second spell immediately.
 

lfgr

Emperor
Joined
Feb 6, 2010
Messages
1,095
Puppet states get capitals, but Rebels may not.
Right, good point.
That reminded me of a minor issue I've come across in my modmod, where I have Spellstaffs passively allow 2 spells per turn. A lot of spells disappear from the options a unit has after being cast once, only reappearing if I select another unit and then go back to that caster. Also, it can be annoying when casting one spell causes a unit to be deselected when it should be able to cast a second spell immediately.
Can you share the code for your spell staffs?
 

lfgr

Emperor
Joined
Feb 6, 2010
Messages
1,095
New release: mnai-2.9.2u.
Should be compatible with the 2.9u savegames.

Download setup
Download archive

This my first release with larger gameplay changes, namely a revamped stability system for the Revolution component. With this, the "balance/improve stability" part of my plan for Revolutions is mostly done (although I expect balance adjustment in the next few releases). Large parts of the Revolution system are still untouched, like unit selection for revolutions (so stupid all-scout revolutions are still possible).
I put a basic overview of the stability system into the Revolutions concept page in the pedia, and details can be found in the revolution advisor. (I have by now deviated somewhat from the plan.)

I welcome feedback in particular on the stability system. I had to make some relatively late changes, because conquest of large cities was basically guaranteed to result in a revolt (this is what delayed the release for a few weeks). I expect that there are other balanced problems still.
I also wrote comparatively large amounts of text this time (the pedia concept page), and English is not my native language, so please let me know if you find any errors or have any suggestions.

Some other interesting changes:
  • Incorporated MapScriptTools by Temudjin and Terkhen
  • There is a new game option "Better expansion AI", which features a change proposed by @f1rpo (see this post and linked thread). I tested it and observed slightly better AI. I plan to make this into a more general "Experimental AI" option and incorporate some other changes I thought of, but didn't dare putting into the game by default.
  • Many smaller quality-of-life changes and bugfixes
  • A GlobalDefine NO_WAR_TRADE that disables war and war preparation bribes completely; this is primarily inteded as a last-resort solution for the related problems in MagisterModMod
  • For modmodders or people interested in XML tweaking: Revolution effects of buildings and civics are more flexible now and share a set of tags. See the documentation for more information.

For a full changelog, see the file mnai_changelog.md included in the download or here.

Thanks everybody for bug reports, discussion, and contributions.
 

MagisterCultuum

Great Sage
Joined
Feb 14, 2007
Messages
16,524
Location
Kael's head
Thanks!

I'm merging it with my modmod now.

One thing I'd note is that a lot of files have a really ugly mixture of spaces and tabs. (That is especially true of the map scripts; Totestra seems to use four spaces to mean one tab and one tab to mean two tabs, and a lot of them have random spaces mixed in where they shouldn't be.) It makes the files larger, harder to read, and harder to identify python indentation bugs. I'm trying to clean up those files during my merge.

Maybe you could use my cleaned up files from my next release in your next release so the merge will go easier next time?
 
Top Bottom