[MOD] MagisterModmod

There is a known crash caused by AI Lanun Workboats building Pirates Coves. It is different from the previous crash caused by automated human Lanun workboats building Pirates Coves (this time it is caused by the code Tholal uses to make Naval units choose crew promotions), but can be solved the same way; by switching the spell to kill the unit (with a delayed death) in the python effect instead of through the xml.

I have added more python code, including things that work through spell prerequisites that run quite often. This may be slowing things down, although haven't really noticed a performance decrease on my end.

Adventurers will only revert while in cities that are predominately of their owner's culture. Nothing odd will happen to them while marching through enemy territory. They should also be quite normal while garrisoned in a newly conquered city that has not yet been assimilated into your culture. They will never revert if they have already moved that turn.

(I just got to thinking that an adventure could revert while defending a city which you just recently and barely liberated from an enemy. That seems a bit wrong. I'm thinking about changing this so that they will never revert if there is a hostile unit nearby. That slow things down slightly, but less than other bits of code run more often.)

They can trigger the explore lair code without actually exploring a lair only while in territory that no player owns. Again this only applies if they have not already moved.
 
Has anyone noticed a problem with the latest version crashing frequently?

I'm having this problem now, whereas the last few versions of the mod were rock solid. I also get slowdowns at times.

It's possible some other program is interfering with things (Avast is awful chatty now, always letting me know it has "updated").

Also what is the story with the Grigori adventurers? Do they only revert to basic adventurers in towns? I am leery of playing the Grigori because I have this image of sending a stack somewhere, camping it outside a town for a siege, and the guy the whole stack is built around decides to run off drinking or something.

I had thought as long as they moved every turn they would stay stable, but sometimes I need them to stand still for a couple of rounds.


Happens to me continuously. As the game goes on and more units are involved it becomes more common. I have to save every turn 5-6 times to avoid too much replaying. My last game eventually crashed so bad I've had to abandon it. Even reducing my graphics to minimums has not stopped the game from full stop at one place.
 
damn, WoC again, save and logs...
 

Attachments

  • QuickSave.CivBeyondSwordSave
    299.3 KB · Views: 72
  • Logs.7z
    4.3 KB · Views: 52
I don't know what the problem is, but now when I try to start a new game, it continually crashes by about turn 20.
 
I'm getting the same problem now.

These are the problem details from the latest crash:

Problem signature:
Problem Event Name: APPCRASH
Application Name: Civ4BeyondSword.exe
Application Version: 3.1.9.0
Application Timestamp: 4a0c27e6
Fault Module Name: CvGameCoreDLL.dll
Fault Module Version: 0.0.0.0
Fault Module Timestamp: 50e99934
Exception Code: c0000005
Exception Offset: 0021c35a
OS Version: 6.1.7601.2.1.0.768.3
Locale ID: 1033
Additional Information 1: 8485
Additional Information 2: 84853502223d8164a31468861a7c3d71
Additional Information 3: a4ea
Additional Information 4: a4eacf9d27e5ee8cb3a70ac0035eb8ab
 
I think all of these problems are actually related to how Tholal changed things that used to use delayed death in the DLL so that they instead kill the unit immediately. I'm not quite sure why he felt the need to do that.

@Cutlass & sunbeam:
May I assume that the Lanun are present in all of those games? If you are using the latest release and have not made the fix I mentioned several times, then a crash right around that time due to an AI-controlled Lanun Work Boat using the Pirate Cove ability would be entirely expected. The fix is to remove the <bSacrificeCaster>1 tag from the spell's XML define and make the python result end with the line pCaster.kill(True, iPlayer).

(The True indicates delayed death. Tholal changed the XML to kill units using kill(False, iPlayer), and did not notice that this could cause my work boats which have UNITCOMBAT_NAVAL to try to run the code that tried to change crew promotions after the unit has already died.

Tholal's has fixed this for his next release, but I think I'll keep killing the unit in python anyway. I should have left it in last time, but instead removed it because a previous crash it had been intended to prevent was resolved.)


@SergiuSS:
I noticed the AI adventurer duplication issue in a game last night.

Looking back at previous versions of he code, I saw that the 24th December version is the first one where I removed what I thought had become an unnecessary precaution to prevent Adventurer duplication (granting the caster PROMOTION_DARK_REFLECTION before converting it to a new unit so that ti would not trigger the sluagh code, and then removing the promotion form the new unit). (I believe it had been unnecessary with the current code in def sluagh, until Tholal changed a bunch of things to no longer use delayed death.)

I have fixed this, but not had time to test the fix yet.

@scutarii:
I don't see any obvious problem in the log files. I haven't yet had time to roll back to my last release in order to test your saved game.
 
Magister, I just played 150 turns or so in a game where I specified the opposing civs, and made sure the Lanun were not included.

The game played normally. Also the slowdowns I've noticed with the new rev of your mod were gone.

I'll make the changes you were talking about. Right now it seems like the Lanun work boats are the problem as you say.
 
My game that kept crashing late in the game, the Lanun were not in that game. My new game that's crashing at the beginning, I think they are, but I'd have to open worldbuilder to confirm it. It is crashing before any meeting of other civs.
 
Are you sure the Lanun are not in the game? If Advanced Tactics is active, then the Lanun may have entered the game later through a revolt.

----

I just discovered that units with <bAlwaysHostile>1 actually are not always hostile toward units in cities. They can kill rival units without war outside of cities, but can still enter rival cities peacefully. I went ahead and gave Dragon Fanatics this tag.

I also changed the Evangelize Units spells so that they no longer change the religions of units whose religions were granted in XML. Personally I'd rather it be less likely rather than impossible for them to convert, but it did cause confusion related to the religions being changed back after such units rebel and change owners. That confusion should now be gone.

The Luonnotar's Enlighten Units spell no longer uses the same python code. It does not have the same limitation, so it can still convert and disband rival priests.

Edit: I just discovered a complication with giving Dragon Fanatics <bAlwaysHostile>1. When a rival unit with that tag is in one of your cities, it blocks your own units from entering the city. Your units can be built or summoned there just fine, but not moved from an adjacent tile or airlifted to reinforce the city. They are not allowed to attack the bAlwaysHostile unit in a city, but also cannot move into the same tile as the bAlwaysHostile unit. bHiddenNationaltiy units can be used to kill such bAlwaysHostile units, but regular unis cannot and neither can other bAlwaysHostile units under your own control. If you don't have an HN unit handy, you have to declare war on the units owner before your units are allowed to enter your own city.

I may have to delay this change until after Tholal can address the issue with a new DLL.
 
The late game crash I'm certain. I have nearly all the map explored and most of the map under intermittent surveillance. This is very late in the game. I'd have to check, but after turn 300. I was going to start construction of Tower of Mastery the turn where it stops and I can't get past it.
 
IS there a full change log somewhere :p? Also is it better to play this on mnai or not?
 
Unfortunately, there is no full change log. I've been changing little things as I go without keeping track of all the changes. I suppose I can try to take careful notes the next time I run a comparison with base FfH2 for the purposes of deciding what files I need to include in the download, and make a change log that way.


This modmod includes MNAI. It should not make any difference whether or not you install MNAI first.

If you meant to ask whether it is better to play with MNAI's Advanced Tactics option, then I can only say I personally prefer it. It can cause some problems if you activate it in scenarios that use the permanent war and peace option though.
 
There seems to be quite a deal of changes and there are some stuff hard to understand or to know how many new additions there are in a certain area.

I thought the mod came without tholals mod and I had the option to install over it or not.
 
I'm playing the Momus scenario with this modmod...mod and at about turn 350 started getting the Waiting On Civ error. I haven't played much of the game period and it's gotten to the point where all my backtracks lead to the spinny globe.

In a moment of annoyance I took the world builder and deleted half the units and cities in the game. That fixed it :x Besides trying to delete every unit one at a time (oh god) is there anything I can do? I skimmed through the thread and googled but I'm not entirely familiar with the game or mods and how they work so I'm not even sure what I'm looking for. I've heard Avatar of War(?or chaos?) causes it but I didn't see any notice of it (or not even sure it happens in the Momus). I seem to have a lot of enraged/crazy units and I think I read they can cause the problem also.

I've uploaded 3 separate saves, they all lead to a freeze within the next few rounds.

And yes, my leader's name is Madam Kickass.
 

Attachments

  • argh.CivBeyondSwordSave
    650.4 KB · Views: 53
  • Madam Kickass AD-0466.CivBeyondSwordSave
    652.3 KB · Views: 44
  • AutoSave_AD-0468.CivBeyondSwordSave
    648.8 KB · Views: 65
The Avatar of Wrath only appears when the Armageddon Counter is quite high. Since the AC is 0 here, that event is not relevant.

I loaded you game on my current version, which is not identical to the last release but is close enough for compatibility. I have not tried rolling back to the last release yet.

I'm getting the WOC issues too here, but I'm not really sure what to say about this.

My first guess was that the problem was caused having too many puppets summoned, which as summons kept triggering the Ring of Warding code. I tried making that code just kill the units so it wouldn't run again for them, but it didn't help.

Based on BBAI.log, the problem seems to be happening when Perpentch is deciding whether to use some mage to build an Entropy Node. I'm not sure why it is happening though.

For a moment I thought it might be caused by a recent change, where I blocked members of the Overcouncil from even building the improvements that could connect resources like Entropy Mana once they are banned. Perpentach is in the Undercouncil here though, so that should not be at all relevant.

@SergiuSS:
I noticed the AI adventurer duplication issue in a game last night.

Looking back at previous versions of he code, I saw that the 24th December version is the first one where I removed what I thought had become an unnecessary precaution to prevent Adventurer duplication (granting the caster PROMOTION_DARK_REFLECTION before converting it to a new unit so that ti would not trigger the sluagh code, and then removing the promotion form the new unit). (I believe it had been unnecessary with the current code in def sluagh, until Tholal changed a bunch of things to no longer use delayed death.)

I have fixed this, but not had time to test the fix yet.

Well, I was wrong about this being the problem. My tests have confirmed that it is not the Sluagh code that is causing problems, but the spellUpgradeAdventurer.

BBAI has revealed that the AI somehow seems to be able to cast a spell to upgrade the adventurer and then also upgrade the adventurer the old fashioned way before newUnit.convert(pCaster) can kill pCaster. I don't think that adding and removing PROMOTION_DARK_REFLECTION actually effects anything, but it did help me track the order in which things were happening.

For now, I changed the prereq of those abilities so that units cannot upgrade through them while within their team's borders. That means that no old fashioned upgrades would be possible for the AI, so there should be no adventurer duplication. It would also make it so that more upgrades are available outside of your territory than within though, as upgrading through spells is not limited by buildings or bonuses.

Eventually I'd like to get rid of those spells and just allow adventurers to upgrade the normal way regardless of location. That would probably be faster and easier for the AI to understand.
 
Magister, how did you make the Issue Challenge spell? Its brilliant! :D

(hopefully the code is simple, because I would like to add it for my mod)


------------------

On a separate note, Sea of Sorrow and Ocean of Despair are brilliant as well, but I suppose it may be trickier to add to the mod due to art files (trying to keep it as just an assets folder tbh).
 
Well, it isn't particularly simple, but it isn't too hard.


There are actually 2 different spells, Issue Challenge and Accept Challenge, so that you can choose both units that will fight each other instead of risking having one unit fight another unit that you don't want to risk loosing.


Here is Issue Challenge:
Spoiler :

CIV4SpellInfos.xml:
Code:
		<SpellInfo>
			<Type>SPELL_CHALLENGE_ISSUE</Type>
			<Description>TXT_KEY_SPELL_CHALLENGE_ISSUE</Description>
			<Civilopedia>TXT_KEY_SPELL_PLACEHOLDER_PEDIA</Civilopedia>
			<UnitCombatPrereq>UNITCOMBAT_MELEE</UnitCombatPrereq>
			<CivilizationPrereq>CIVILIZATION_DOVIELLO</CivilizationPrereq>
			<AddPromotionType1>PROMOTION_CHALLENGER</AddPromotionType1>
			<bBuffCasterOnly>1</bBuffCasterOnly>
			<bAbility>1</bAbility>
			<PyRequirement>reqIssueChallenge(pCaster)</PyRequirement>
			<Effect>EFFECT_SPELL1</Effect>
			<Sound>AS3D_SPELL_TRAIN</Sound>
			<bGraphicalOnly>1</bGraphicalOnly>
			<Button>Art/Interface/Buttons/Units/Battlemaster.dds</Button>
		</SpellInfo>
CvSpellInterface.py:
Code:
def reqIssueChallenge(pCaster):
	if not pCaster.isHuman():
		return False
	if pCaster.isMadeAttack() and not pCaster.isBlitz ():
		return False
	if pCaster.isHasPromotion(gc.getInfoTypeForString('PROMOTION_CHALLENGER')):
		return False
	if pCaster.canCast(gc.getInfoTypeForString('SPELL_CHALLENGE_ACCEPT'), False):
		return False
	iMelee = gc.getInfoTypeForString('UNITCOMBAT_MELEE')
	ID = pCaster.getID()
	iPlayer = pCaster.getOwner()
	pPlot = pCaster.plot()
	for i in range(pPlot.getNumUnits()):
		pUnit = pPlot.getUnit(i)
		if pUnit.getOwner() == iPlayer:
			if ID != pUnit.getID():
				if pUnit.getUnitCombatType() == iMelee:
					if not pUnit.isMadeAttack() or pUnit.isBlitz ():
						return True
	return False

Originally this spell was XML-only, but then I decided it ought to be more limited.

I believe that the AI was using this spell too much and loosing too many units. I just blocked them from using it rather than trying to create more complicated logic governing their decisions. It might be better to have them use it at least when their funds are low enough that they would have to start disbanding units.

Since the spell pair mimics combat, I decided it should not be used by units who could not engage in normal combat.

The loop makes sure that there is at least one other unit which can engage in combat and accept the challenge.

When it was an XML-only spell then it could never be cast if caster already had the promotion it grants, but I think including the python prereq required this.

Checking to make sure that the caster cannot already cast the second spell prevents there from being more than one unit issuing a challenger at the same time.


Spoiler :

CIV4SpellInfos.xml
Code:
		<SpellInfo>
			<Type>SPELL_CHALLENGE_ACCEPT</Type>
			<Description>TXT_KEY_SPELL_CHALLENGE_ACCEPT</Description>
			<Civilopedia>TXT_KEY_SPELL_PLACEHOLDER_PEDIA</Civilopedia>
			<UnitCombatPrereq>UNITCOMBAT_MELEE</UnitCombatPrereq>
			<CivilizationPrereq>CIVILIZATION_DOVIELLO</CivilizationPrereq>
			<PromotionInStackPrereq>PROMOTION_CHALLENGER</PromotionInStackPrereq>
			<bAbility>1</bAbility>
			<PyResult>spellAcceptChallenge(pCaster)</PyResult>
			<Effect>EFFECT_SPELL1</Effect>
			<Sound>AS3D_SPELL_TRAIN</Sound>
			<bGraphicalOnly>1</bGraphicalOnly>
			<Button>Art/Interface/Buttons/Units/Battlemaster.dds</Button>
		</SpellInfo>
CvSpellInterface.py:
Code:
def spellAcceptChallenge(pCaster):
	pPlot = pCaster.plot()
	iChallengerProm = gc.getInfoTypeForString('PROMOTION_CHALLENGER')
	iBronze = gc.getInfoTypeForString('PROMOTION_BRONZE_WEAPONS')
	iIron = gc.getInfoTypeForString('PROMOTION_IRON_WEAPONS')
	iMithril = gc.getInfoTypeForString('PROMOTION_MITHRIL_WEAPONS')
	iRust = gc.getInfoTypeForString('PROMOTION_RUSTED')
	iEnchant = gc.getInfoTypeForString('PROMOTION_ENCHANTED_BLADE')
	iPoison = gc.getInfoTypeForString('PROMOTION_POISONED_BLADE')
	for i in range(pPlot.getNumUnits()):
		pUnit = pPlot.getUnit(i)
		if pCaster is not pUnit:
			if pUnit.isHasPromotion(iChallengerProm) == True:
				break
	iRnd = CyGame().getSorenRandNum(100, "Doviello Duel")
	iChallengerOdds = getCombatOdds(pUnit, pCaster)
	iChallengerRnd = (iRnd + (iChallengerOdds * 2)) / 3
	iDefenderOdds = getCombatOdds(pCaster, pUnit)
	iDefenderRnd = (iRnd + (iDefenderOdds * 2)) / 3
	if iDefenderOdds > iChallengerOdds:
		iDefenseRnd = CyGame().getSorenRandNum(100, "Doviello Duel Defender")
		iDefenseTieRnd = CyGame().getSorenRandNum(100, "Doviello Duel Defender Tie")
		if iDefenseTieRnd < 10:
			pCaster.changeExperience(pUnit.getExperience() / 10, -1, False, False, False)
			pCaster.setDamage(75, pCaster.getOwner())
			pUnit.changeExperience(pCaster.getExperience() / 10, -1, False, False, False)
			pUnit.setDamage(75, pUnit.getOwner())
			pUnit.setHasPromotion(gc.getInfoTypeForString('PROMOTION_CHALLENGER'), False)
			CyInterface().addMessage(pCaster.getOwner(),True,25,CyTranslator().getText("TXT_KEY_MESSAGE_DOVIELLO_DUEL_DRAW", ()),'',1,'Art/Interface/Buttons/Buildings/Arena.dds',ColorTypes(8),pCaster.getX(),pCaster.getY(),True,True)
		elif iDefenseTieRnd < 12 and iDefenseTieRnd >= 10:
			CyInterface().addMessage(pCaster.getOwner(),True,25,CyTranslator().getText("TXT_KEY_MESSAGE_ARENA_DEATH", ()),'',1,'Art/Interface/Buttons/Buildings/Arena.dds',ColorTypes(7),pCaster.getX(),pCaster.getY(),True,True)
			pCaster.kill(True, PlayerTypes.NO_PLAYER)
			pUnit.kill(True, PlayerTypes.NO_PLAYER)
		elif iDefenseTieRnd > 12:
			if iDefenseRnd > iDefenderOdds:
				pUnit.changeExperience((pCaster.getExperience() / 4) + 2, -1, False, False, False)
				pUnit.setDamage(25, pUnit.getOwner())
				pUnit.setHasPromotion(gc.getInfoTypeForString('PROMOTION_CHALLENGER'), False)
				CyInterface().addMessage(pCaster.getOwner(),True,25,CyTranslator().getText("TXT_KEY_MESSAGE_DOVIELLO_DUEL_LOSS", ()),'',1,'Art/Interface/Buttons/Buildings/Arena.dds',ColorTypes(8),pCaster.getX(),pCaster.getY(),True,True)
				if not pUnit.isHasPromotion(iEnchant):
					if pCaster.isHasPromotion(iEnchant):
						pUnit.setHasPromotion(iEnchant, True)
				if not pUnit.isHasPromotion(iPoison):
					if pCaster.isHasPromotion(iPoison):
						pUnit.setHasPromotion(iPoison, True)
				if not pUnit.isHasPromotion(iMithril):
					if pCaster.isHasPromotion(iMithril) and gc.getUnitInfo(pUnit.getUnitType()).getWeaponTier() >= 3:
						pUnit.setHasPromotion(iMithril, True)
						pUnit.setHasPromotion(iIron, False)
						pUnit.setHasPromotion(iBronze, False)
						pUnit.setHasPromotion(iRust, False)
					elif not pUnit.isHasPromotion(iIron):
						if pCaster.isHasPromotion(iIron) and gc.getUnitInfo(pUnit.getUnitType()).getWeaponTier() >= 2:
							pUnit.setHasPromotion(iIron, True)
							pUnit.setHasPromotion(iBronze, False)
							pUnit.setHasPromotion(iRust, False)
						elif not pUnit.isHasPromotion(iBronze) and gc.getUnitInfo(pUnit.getUnitType()).getWeaponTier() >= 1:
							if pCaster.isHasPromotion(iBronze):
								pUnit.setHasPromotion(iBronze, True)
								pUnit.setHasPromotion(iRust, False)
				if pUnit.isHasPromotion(iRust):
					if not pCaster.isHasPromotion(iRust):
						if (pUnit.isHasPromotion(iMithril) and pCaster.isHasPromotion(iMithril)) or (pUnit.isHasPromotion(iIron) and pCaster.isHasPromotion(iIron)) or (pUnit.isHasPromotion(iBronze) and pCaster.isHasPromotion(iBronze)):
							pUnit.setHasPromotion(iRust, False)
				pCaster.kill(True, PlayerTypes.NO_PLAYER)
			if iDefenseRnd < iDefenderOdds:
				pCaster.changeExperience((pUnit.getExperience() / 4) + 2, -1, False, False, False)
				pCaster.setDamage(25, pCaster.getOwner())
				CyInterface().addMessage(pCaster.getOwner(),True,25,CyTranslator().getText("TXT_KEY_MESSAGE_DOVIELLO_DUEL_WIN", ()),'',1,'Art/Interface/Buttons/Buildings/Arena.dds',ColorTypes(8),pCaster.getX(),pCaster.getY(),True,True)
				if not pCaster.isHasPromotion(iEnchant):
					if pUnit.isHasPromotion(iEnchant):
						pCaster.setHasPromotion(iEnchant, True)
				if not pCaster.isHasPromotion(iPoison):
					if pUnit.isHasPromotion(iPoison):
						pCaster.setHasPromotion(iPoison, True)
				if not pCaster.isHasPromotion(iMithril):
					if pUnit.isHasPromotion(iMithril) and gc.getUnitInfo(pCaster.getUnitType()).getWeaponTier() >= 3:
						pCaster.setHasPromotion(iMithril, True)
						pCaster.setHasPromotion(iIron, False)
						pCaster.setHasPromotion(iBronze, False)
						pCaster.setHasPromotion(iRust, False)
					elif not pCaster.isHasPromotion(iIron):
						if pUnit.isHasPromotion(iIron) and gc.getUnitInfo(pCaster.getUnitType()).getWeaponTier() >= 2:
							pCaster.setHasPromotion(iIron, True)
							pCaster.setHasPromotion(iBronze, False)
							pCaster.setHasPromotion(iRust, False)
						elif not pCaster.isHasPromotion(iBronze) and gc.getUnitInfo(pCaster.getUnitType()).getWeaponTier() >= 1:
							if pUnit.isHasPromotion(iBronze):
								pCaster.setHasPromotion(iBronze, True)
								pCaster.setHasPromotion(iRust, False)
				if pCaster.isHasPromotion(iRust):
					if not pUnit.isHasPromotion(iRust):
						if (pCaster.isHasPromotion(iMithril) and pUnit.isHasPromotion(iMithril)) or (pCaster.isHasPromotion(iIron) and pUnit.isHasPromotion(iIron)) or (pCaster.isHasPromotion(iBronze) and pUnit.isHasPromotion(iBronze)):
							pCaster.setHasPromotion(iRust, False)
				pUnit.kill(True, PlayerTypes.NO_PLAYER)
	if iChallengerOdds > iDefenderOdds:
		iChallengeRnd = CyGame().getSorenRandNum(100, "Doviello Duel Challenger")
		iChallengerTieRnd = CyGame().getSorenRandNum(100, "Doviello Duel Challenger Tie")
		if iChallengerTieRnd < 10:
			pCaster.changeExperience(pUnit.getExperience() / 10, -1, False, False, False)
			pCaster.setDamage(75, pCaster.getOwner())
			pUnit.changeExperience(pCaster.getExperience() / 10, -1, False, False, False)
			pUnit.setDamage(75, pUnit.getOwner())
			pUnit.setHasPromotion(gc.getInfoTypeForString('PROMOTION_CHALLENGER'), False)
			CyInterface().addMessage(pCaster.getOwner(),True,25,CyTranslator().getText("TXT_KEY_MESSAGE_DOVIELLO_DUEL_DRAW", ()),'',1,'Art/Interface/Buttons/Buildings/Arena.dds',ColorTypes(8),pCaster.getX(),pCaster.getY(),True,True)
		elif (iChallengerTieRnd < 12 and iChallengerTieRnd >= 10):
			CyInterface().addMessage(pCaster.getOwner(),True,25,CyTranslator().getText("TXT_KEY_MESSAGE_ARENA_DEATH", ()),'',1,'Art/Interface/Buttons/Buildings/Arena.dds',ColorTypes(7),pCaster.getX(),pCaster.getY(),True,True)
			pCaster.kill(True, PlayerTypes.NO_PLAYER)
			pUnit.kill(True, PlayerTypes.NO_PLAYER)
		elif iChallengerTieRnd > 12:
			if iChallengeRnd < iChallengerOdds:
				pUnit.changeExperience((pCaster.getExperience() / 4) + 2, -1, False, False, False)
				pUnit.setDamage(25, pUnit.getOwner())
				pUnit.setHasPromotion(gc.getInfoTypeForString('PROMOTION_CHALLENGER'), False)
				CyInterface().addMessage(pCaster.getOwner(),True,25,CyTranslator().getText("TXT_KEY_MESSAGE_DOVIELLO_DUEL_LOSS", ()),'',1,'Art/Interface/Buttons/Buildings/Arena.dds',ColorTypes(8),pCaster.getX(),pCaster.getY(),True,True)
				if not pUnit.isHasPromotion(iEnchant):
					if pCaster.isHasPromotion(iEnchant):
						pUnit.setHasPromotion(iEnchant, True)
				if not pUnit.isHasPromotion(iPoison):
					if pCaster.isHasPromotion(iPoison):
						pUnit.setHasPromotion(iPoison, True)
				if not pUnit.isHasPromotion(iMithril):
					if pCaster.isHasPromotion(iMithril) and gc.getUnitInfo(pUnit.getUnitType()).getWeaponTier() >= 3:
						pUnit.setHasPromotion(iMithril, True)
						pUnit.setHasPromotion(iIron, False)
						pUnit.setHasPromotion(iBronze, False)
						pUnit.setHasPromotion(iRust, False)
					elif not pUnit.isHasPromotion(iIron):
						if pCaster.isHasPromotion(iIron) and gc.getUnitInfo(pUnit.getUnitType()).getWeaponTier() >= 2:
							pUnit.setHasPromotion(iIron, True)
							pUnit.setHasPromotion(iBronze, False)
							pUnit.setHasPromotion(iRust, False)
						elif not pUnit.isHasPromotion(iBronze) and gc.getUnitInfo(pUnit.getUnitType()).getWeaponTier() >= 1:
							if pCaster.isHasPromotion(iBronze):
								pUnit.setHasPromotion(iBronze, True)
								pUnit.setHasPromotion(iRust, False)
				if pUnit.isHasPromotion(iRust):
					if not pCaster.isHasPromotion(iRust):
						if (pUnit.isHasPromotion(iMithril) and pCaster.isHasPromotion(iMithril)) or (pUnit.isHasPromotion(iIron) and pCaster.isHasPromotion(iIron)) or (pUnit.isHasPromotion(iBronze) and pCaster.isHasPromotion(iBronze)):
							pUnit.setHasPromotion(iRust, False)
				pCaster.kill(True, PlayerTypes.NO_PLAYER)
			if iChallengeRnd > iChallengerOdds:
				pCaster.changeExperience((pUnit.getExperience() / 4) + 2, -1, False, False, False)
				pCaster.setDamage(25, pCaster.getOwner())
				CyInterface().addMessage(pCaster.getOwner(),True,25,CyTranslator().getText("TXT_KEY_MESSAGE_DOVIELLO_DUEL_WIN", ()),'',1,'Art/Interface/Buttons/Buildings/Arena.dds',ColorTypes(8),pCaster.getX(),pCaster.getY(),True,True)
				if not pCaster.isHasPromotion(iEnchant):
					if pUnit.isHasPromotion(iEnchant):
						pCaster.setHasPromotion(iEnchant, True)
				if not pCaster.isHasPromotion(iPoison):
					if pUnit.isHasPromotion(iPoison):
						pCaster.setHasPromotion(iPoison, True)
				if not pCaster.isHasPromotion(iMithril):
					if pUnit.isHasPromotion(iMithril) and gc.getUnitInfo(pCaster.getUnitType()).getWeaponTier() >= 3:
						pCaster.setHasPromotion(iMithril, True)
						pCaster.setHasPromotion(iIron, False)
						pCaster.setHasPromotion(iBronze, False)
						pCaster.setHasPromotion(iRust, False)
					elif not pCaster.isHasPromotion(iIron):
						if pUnit.isHasPromotion(iIron) and gc.getUnitInfo(pCaster.getUnitType()).getWeaponTier() >= 2:
							pCaster.setHasPromotion(iIron, True)
							pCaster.setHasPromotion(iBronze, False)
							pCaster.setHasPromotion(iRust, False)
						elif not pCaster.isHasPromotion(iBronze) and gc.getUnitInfo(pCaster.getUnitType()).getWeaponTier() >= 1:
							if pUnit.isHasPromotion(iBronze):
								pCaster.setHasPromotion(iBronze, True)
								pCaster.setHasPromotion(iRust, False)
				if pCaster.isHasPromotion(iRust):
					if not pUnit.isHasPromotion(iRust):
						if (pCaster.isHasPromotion(iMithril) and pUnit.isHasPromotion(iMithril)) or (pCaster.isHasPromotion(iIron) and pUnit.isHasPromotion(iIron)) or (pCaster.isHasPromotion(iBronze) and pUnit.isHasPromotion(iBronze)):
							pCaster.setHasPromotion(iRust, False)
				pUnit.kill(True, PlayerTypes.NO_PLAYER)
	if iChallengerOdds == iDefenderOdds:
		iEqualRnd = CyGame().getSorenRandNum(100, "Doviello Duel Equals")
		iEqualTieRnd = CyGame().getSorenRandNum(100, "Doviello Duel Equals Tie")
		if iEqualTieRnd < 10:
			pCaster.changeExperience(pUnit.getExperience() / 10, -1, False, False, False)
			pCaster.setDamage(75, pCaster.getOwner())
			pUnit.changeExperience(pCaster.getExperience() / 10, -1, False, False, False)
			pUnit.setDamage(75, pUnit.getOwner())
			pUnit.setHasPromotion(gc.getInfoTypeForString('PROMOTION_CHALLENGER'), False)
			CyInterface().addMessage(pCaster.getOwner(),True,25,CyTranslator().getText("TXT_KEY_MESSAGE_DOVIELLO_DUEL_DRAW", ()),'',1,'Art/Interface/Buttons/Buildings/Arena.dds',ColorTypes(8),pCaster.getX(),pCaster.getY(),True,True)
		elif (iEqualTieRnd < 12 and iEqualTieRnd >= 10):
			CyInterface().addMessage(pCaster.getOwner(),True,25,CyTranslator().getText("TXT_KEY_MESSAGE_ARENA_DEATH", ()),'',1,'Art/Interface/Buttons/Buildings/Arena.dds',ColorTypes(7),pCaster.getX(),pCaster.getY(),True,True)
			pCaster.kill(True, PlayerTypes.NO_PLAYER)
			pUnit.kill(True, PlayerTypes.NO_PLAYER)
		elif iEqualTieRnd > 12:
			if iEqualRnd < 50:
				pUnit.changeExperience((pCaster.getExperience() / 4) + 2, -1, False, False, False)
				pUnit.setDamage(25, pUnit.getOwner())
				pUnit.setHasPromotion(gc.getInfoTypeForString('PROMOTION_CHALLENGER'), False)
				CyInterface().addMessage(pCaster.getOwner(),True,25,CyTranslator().getText("TXT_KEY_MESSAGE_DOVIELLO_DUEL_LOSS", ()),'',1,'Art/Interface/Buttons/Buildings/Arena.dds',ColorTypes(8),pCaster.getX(),pCaster.getY(),True,True)
				if not pUnit.isHasPromotion(iEnchant):
					if pCaster.isHasPromotion(iEnchant):
						pUnit.setHasPromotion(iEnchant, True)
				if not pUnit.isHasPromotion(iPoison):
					if pCaster.isHasPromotion(iPoison):
						pUnit.setHasPromotion(iPoison, True)
				if not pUnit.isHasPromotion(iMithril):
					if pCaster.isHasPromotion(iMithril) and gc.getUnitInfo(pUnit.getUnitType()).getWeaponTier() >= 3:
						pUnit.setHasPromotion(iMithril, True)
						pUnit.setHasPromotion(iIron, False)
						pUnit.setHasPromotion(iBronze, False)
						pUnit.setHasPromotion(iRust, False)
					elif not pUnit.isHasPromotion(iIron):
						if pCaster.isHasPromotion(iIron) and gc.getUnitInfo(pUnit.getUnitType()).getWeaponTier() >= 2:
							pUnit.setHasPromotion(iIron, True)
							pUnit.setHasPromotion(iBronze, False)
							pUnit.setHasPromotion(iRust, False)
						elif not pUnit.isHasPromotion(iBronze) and gc.getUnitInfo(pUnit.getUnitType()).getWeaponTier() >= 1:
							if pCaster.isHasPromotion(iBronze):
								pUnit.setHasPromotion(iBronze, True)
								pUnit.setHasPromotion(iRust, False)
				if pUnit.isHasPromotion(iRust):
					if not pCaster.isHasPromotion(iRust):
						if (pUnit.isHasPromotion(iMithril) and pCaster.isHasPromotion(iMithril)) or (pUnit.isHasPromotion(iIron) and pCaster.isHasPromotion(iIron)) or (pUnit.isHasPromotion(iBronze) and pCaster.isHasPromotion(iBronze)):
							pUnit.setHasPromotion(iRust, False)
				pCaster.kill(True, PlayerTypes.NO_PLAYER)
			if iEqualRnd >= 50:
				pCaster.changeExperience((pUnit.getExperience() / 4) + 2, -1, False, False, False)
				pCaster.setDamage(25, pCaster.getOwner())
				CyInterface().addMessage(pCaster.getOwner(),True,25,CyTranslator().getText("TXT_KEY_MESSAGE_DOVIELLO_DUEL_WIN", ()),'',1,'Art/Interface/Buttons/Buildings/Arena.dds',ColorTypes(8),pCaster.getX(),pCaster.getY(),True,True)
				if not pCaster.isHasPromotion(iEnchant):
					if pUnit.isHasPromotion(iEnchant):
						pCaster.setHasPromotion(iEnchant, True)
				if not pCaster.isHasPromotion(iPoison):
					if pUnit.isHasPromotion(iPoison):
						pCaster.setHasPromotion(iPoison, True)
				if not pCaster.isHasPromotion(iMithril):
					if pUnit.isHasPromotion(iMithril) and gc.getUnitInfo(pCaster.getUnitType()).getWeaponTier() >= 3:
						pCaster.setHasPromotion(iMithril, True)
						pCaster.setHasPromotion(iIron, False)
						pCaster.setHasPromotion(iBronze, False)
						pCaster.setHasPromotion(iRust, False)
					elif not pCaster.isHasPromotion(iIron):
						if pUnit.isHasPromotion(iIron) and gc.getUnitInfo(pCaster.getUnitType()).getWeaponTier() >= 2:
							pCaster.setHasPromotion(iIron, True)
							pCaster.setHasPromotion(iBronze, False)
							pCaster.setHasPromotion(iRust, False)
						elif not pCaster.isHasPromotion(iBronze) and gc.getUnitInfo(pCaster.getUnitType()).getWeaponTier() >= 1:
							if pUnit.isHasPromotion(iBronze):
								pCaster.setHasPromotion(iBronze, True)
								pCaster.setHasPromotion(iRust, False)
				if pCaster.isHasPromotion(iRust):
					if not pUnit.isHasPromotion(iRust):
						if (pCaster.isHasPromotion(iMithril) and pUnit.isHasPromotion(iMithril)) or (pCaster.isHasPromotion(iIron) and pUnit.isHasPromotion(iIron)) or (pCaster.isHasPromotion(iBronze) and pUnit.isHasPromotion(iBronze)):
							pCaster.setHasPromotion(iRust, False)
				pUnit.kill(True, PlayerTypes.NO_PLAYER)




The <PromotionInStackPrereq> tag was originally meant for the spells that let a unit take equipment from another unit. I believe that it allows the spell to be cast only if another unit in the stack which belongs to the same player as the caster has the promotion and if the caster itself does not have the promotion. In this case, obviously, it means that a unit cannot accept a challenge unless there is a challenger.


(I'd forgotten just how ugly this python code is. It is far more complicated that it needs to be. I think I'll rewrite it for the next release. A large part of this spell has to do with letting the winner steal the weapons from the looser. Since this is used so often, both here and under def onCombatResult(self, argsList): in CvEventManager.py, I think it is best to move that to CustomFunctions.py. Perhaps it should steal equipment too.)


The for loop finds which unit is the challenger. The break statement prevents it from continuing once it has found a challenger.

I believe that getCombatOdds(pUnit, pCaster) returns the percentage odds that pUnit has of defeating pCaster. This is the number that would be displayed in the mouseover when considering whether to attack an enemy unit.

The function that actually returns what the result of combat would be is not exposed to python like getCombatOdds is. Since I didn't want the spell to be that deterministic, I introduced random number generators too.

edit:
This code is much more elegant:
Spoiler :

CvSpellInterface.py
Code:
def spellAcceptChallenge(pCaster):
	iPlayer = pCaster.getOwner()
	pPlot = pCaster.plot()
	iX = pPlot.getX()
	iY = pPlot.getY()

	#Find the challenge issuer
	iChallengerProm = gc.getInfoTypeForString('PROMOTION_CHALLENGER')
	for i in range(pPlot.getNumUnits()):
		pUnit = pPlot.getUnit(i)
		if pUnit.getOwner() == iPlayer:
			if pUnit.isHasPromotion(iChallengerProm):
				pUnit.setHasPromotion(iChallengerProm, False)
				break#The challenge issuer has been found, so there is no reason to keep looking

	sCasterName ="Your " + pCaster.getName()
	sUnitName = "Your " + pUnit.getName() + " (the challenge issuer)"

	iOdds = getCombatOdds(pCaster, pUnit)/10# getCombatOdds seems to return a value between 0 and 1000, but I prefer to work with percents
	iResult = CyGame().getSorenRandNum(iOdds+1, "Doviello Duel result")

	#Defender Kills Challenger
	if iResult < 30:
		cf.stealWeapons(pCaster,pUnit)

		pCaster.changeExperience(1+ iOdds//10, -1, False, False, False)
		pCaster.setDamage(25, iPlayer)
		pCaster.setMadeAttack(True)

		pUnit.kill(True, iPlayer)

		CyInterface().addMessage(iPlayer,True,25,CyTranslator().getText("TXT_KEY_MESSAGE_DOVIELLO_DUEL_SLAY", (sCasterName,sUnitName,)),'',1,'Art/Interface/Buttons/Buildings/Arena.dds',ColorTypes(8),iX,iY,True,True)

	#Challenger Kills Defender
	elif iResult > 70:
		cf.stealWeapons(pUnit,pCaster)

		pCaster.kill(True, iPlayer)

		pUnit.changeExperience(1+ (100-iOdds)//10, -1, False, False, False)
		pUnit.setDamage(25, iPlayer)
		pUnit.setMadeAttack(True)

		CyInterface().addMessage(iPlayer,True,25,CyTranslator().getText("TXT_KEY_MESSAGE_DOVIELLO_DUEL_DEFENDER_SLAIN", (sCasterName,sUnitName,)),'',1,'Art/Interface/Buttons/Buildings/Arena.dds',ColorTypes(8),iX,iY,True,True)

	#Defender defeats but spares Challenger
	elif iResult < 40:
		cf.stealWeapons(pCaster,pUnit)

		pCaster.changeExperience(1+ iOdds//10, -1, False, False, False)
		pCaster.setDamage(40, iPlayer)
		pCaster.setMadeAttack(True)

		pUnit.changeExperience(1+ iOdds//10, -1, False, False, False)
		pUnit.setDamage(80, iPlayer)
		pUnit.setMadeAttack(True)

		CyInterface().addMessage(iPlayer,True,25,CyTranslator().getText("TXT_KEY_MESSAGE_DOVIELLO_DUEL_SPARE", (sCasterName,sUnitName,)),'',1,'Art/Interface/Buttons/Buildings/Arena.dds',ColorTypes(8),iX,iY,True,True)

	#Challenger defeats but spares Defender
	elif iResult > 60:
		cf.stealWeapons(pUnit,pCaster)

		pCaster.changeExperience(1+ (100-iOdds)//10, -1, False, False, False)
		pCaster.setDamage(80, iPlayer)
		pCaster.setMadeAttack(True)

		pUnit.changeExperience(1+ (100-iOdds)//10, -1, False, False, False)
		pUnit.setDamage(40, iPlayer)
		pUnit.setMadeAttack(True)

		CyInterface().addMessage(iPlayer,True,25,CyTranslator().getText("TXT_KEY_MESSAGE_DOVIELLO_DUEL_SPARE", (sCasterName,sUnitName,)),'',1,'Art/Interface/Buttons/Buildings/Arena.dds',ColorTypes(8),iX,iY,True,True)

	#Draw - Both Live
	elif iResult % 2:#iResult is an odd number
		pCaster.changeExperience(1+ iOdds//15, -1, False, False, False)
		pCaster.setDamage(80, iPlayer)
		pCaster.setMadeAttack(True)

		pUnit.changeExperience(1+ iOdds//15, -1, False, False, False)
		pUnit.setDamage(80, iPlayer)
		pUnit.setMadeAttack(True)

		CyInterface().addMessage(iPlayer,True,25,CyTranslator().getText("TXT_KEY_MESSAGE_DOVIELLO_DUEL_DRAW_SURVIVE", (sCasterName,sUnitName,)),'',1,'Art/Interface/Buttons/Buildings/Arena.dds',ColorTypes(8),iX,iY,True,True)

	#Draw - Both die
	else:
		pCaster.kill(True, iPlayer)
		pUnit.kill(True, iPlayer)

		CyInterface().addMessage(iPlayer,True,25,CyTranslator().getText("TXT_KEY_MESSAGE_DOVIELLO_DUEL_DRAW_DEATH", (sCasterName,sUnitName,)),'',1,'Art/Interface/Buttons/Buildings/Arena.dds',gc.getInfoTypeForString('COLOR_RED'),iX,iY,True,True)
CustomFunctions.py
Code:
	def stealWeapons(self, pWinner, pLoser):
		iBronze = gc.getInfoTypeForString('PROMOTION_BRONZE_WEAPONS')
		iIron = gc.getInfoTypeForString('PROMOTION_IRON_WEAPONS')
		iMithril = gc.getInfoTypeForString('PROMOTION_MITHRIL_WEAPONS')
		iRust = gc.getInfoTypeForString('PROMOTION_RUSTED')
		iEnchant = gc.getInfoTypeForString('PROMOTION_ENCHANTED_BLADE')
		iPoisonBlade = gc.getInfoTypeForString('PROMOTION_POISONED_BLADE')
		iTier = gc.getUnitInfo(pWinner.getUnitType()).getWeaponTier()

		if not pWinner.isHasPromotion(iMithril):
			if pLoser.isHasPromotion(iMithril) and iTier >= 3:
				pLoser.setHasPromotion(iMithril, False)
				pWinner.setHasPromotion(iMithril, True)
				pWinner.setHasPromotion(iIron, False)
				pWinner.setHasPromotion(iBronze, False)
				pWinner.setHasPromotion(iRust, False)
			elif not pWinner.isHasPromotion(iIron):
				if pLoser.isHasPromotion(iIron) and iTier >= 2:
					pLoser.setHasPromotion(iIron, False)
					pWinner.setHasPromotion(iIron, True)
					pWinner.setHasPromotion(iBronze, False)
					pWinner.setHasPromotion(iRust, False)
				elif not pWinner.isHasPromotion(iBronze) and iTier >= 1:
					if pLoser.isHasPromotion(iBronze):
						pLoser.setHasPromotion(iBronze, False)
						pWinner.setHasPromotion(iBronze, True)
						pWinner.setHasPromotion(iRust, False)
		if pWinner.isHasPromotion(iRust):
			if not pLoser.isHasPromotion(iRust):
				if (pWinner.isHasPromotion(iMithril) and pLoser.isHasPromotion(iMithril)) or (pWinner.isHasPromotion(iIron) and pLoser.isHasPromotion(iIron)) or (pWinner.isHasPromotion(iBronze) and pLoser.isHasPromotion(iBronze)):
					pWinner.setHasPromotion(iRust, False)


		if pLoser.isHasPromotion(iEnchant) and not pWinner.isHasPromotion(iEnchant):
			pWinner.setHasPromotion(iEnchant, True)
		if pLoser.isHasPromotion(iPoisonBlade) and not pWinner.isHasPromotion(iPoisonBlade):
			pWinner.setHasPromotion(iPoisonBlade, True)

		for iProm in range(gc.getNumPromotionInfos()):
			if pLoser.isHasPromotion(iProm) and not pWinner.isHasPromotion(iProm):
				if gc.getPromotionInfo(iProm).isEquipment():
					pLoser.setHasPromotion(iProm, False)
					pWinner.setHasPromotion(iProm, True)

-----

In order to make Seas of Sorrows and normal Coasts some anywhere close to blending properly, I had to switch Coast artwork so that it would not include the sandy shore. I borrowed this from another mod. I think it was Master of Mana, but I am not sure.

Seas of Sorrows differ from Coasts only in that I desaturated and darkened their <Detail> texture.

I believe that my Oceans of Despair are just like Oceans except their <Detail> texture is just solid black.

I couldn't really do anything more extreme with the artwork without having really ugly interfaces where different terrain met.

One important thing to remember when you add new terrain types is the <LayerOrder>. You don't want any terrains to have the same value here unless you want really sharp divides between the tiles.
 
Hey MagisterCultuum, great modmod. I have a quick question. When I summon Bridgit the model is pretty big. Is this supposed to be like that? If it is, how would I go about changing the model size to something smaller?
 
Open C:\Program Files (x86)\Firaxis Games\Sid Meier's Civilization 4\Beyond the Sword\Mods\Magister Modmod for FfH2\Assets\XML\Art\CIV4ArtDefines_Unit.xml

Go tot about line #3507
Code:
		<UnitArtInfo>
			<Type>ART_DEF_UNIT_BRIGIT</Type>
			<Button>Art/Interface/Buttons/Units/Brigit.dds</Button>
			[COLOR="Red"]<fScale>1</fScale>[/COLOR]
			<fInterfaceScale>0.2</fInterfaceScale>
			<bActAsLand>0</bActAsLand>
			<bActAsAir>0</bActAsAir>
			<NIF>Art/Units/Heroes/Brigit/Brigit.nif</NIF>
			<KFM>Art\Units\Unique_Sumeria_Vulture\Unique_Sumerian_Vulture blood.kfm</KFM>
			<ShadowDef>
				<ShadowNIF>Art/Units/01_UnitShadows/UnitShadow.nif</ShadowNIF>
				<ShadowAttachNode>BIP Pelvis</ShadowAttachNode>
				<fShadowScale>1</fShadowScale>
			</ShadowDef>
				<fBattleDistance>0.35</fBattleDistance>
				<fRangedDeathTime>0.31</fRangedDeathTime>
				<bActAsRanged>0</bActAsRanged>
				<TrainSound>AS2D_UNIT_BUILD_UNIT</TrainSound>
			<AudioRunSounds>
				<AudioRunTypeLoop/>
				<AudioRunTypeEnd/>
			</AudioRunSounds>
		</UnitArtInfo>

Change that 1 to a smaller value.

<fScale> determines how big it appears in the game. <fInterfaceScale> modifies that to determine how big it should appear in the 'pedia and in the corner of the screen when the unit is selected. If you want to make the unit a lot smaller but don't want it to look tiny in the pedia, it should be increased.

Base FfH2 uses <fScale>0.52</fScale> and<fInterfaceScale>0.8</fInterfaceScale> for Brigit.

I wanted to make her bigger, and may have done so a bit too much.
 
Top Bottom