Larian Banner: Baldur's Gate Patch 9
Previous Thread
Next Thread
Print Thread
Joined: Dec 2016
Location: United States
member
OP Offline
member
Joined: Dec 2016
Location: United States
There was some interest in the AI improvements that I developed for my upcoming patch to Epic Encounters, which solves the problem of enemies "stalling" during their turns. I will outline here the issues addressed by my modifications and the implementation thereof.

There are other improvements that were previously made (Ice Wall destruction, generic priority improvements), as well as various considerations for performance that I will not discuss here (the scripts get quite long)--this is to present the system's logic. Of course, if anyone is interested in the full scripts, I can upload and provide them

Suggestions are of course welcome smile



The AI stalls when it is asked to move and the pathing algorithm cannot find a valid location to satisfy the request. In theory, the scripting should throw a MovementFailed interrupt, which is caught by the behavior to end and block it. However, the MovementFailed interrupt is usually or never thrown by CharacterMoveInRange(); this is the core of the problem. Generally, CharacterMoveInRange() is called from spell wrapper scripts.

In my scripting, I chose to address this by building my own interrupt for these behaviors. This is done via timer checks during the character's turn--if the character's AP or position doesn't change between ticks, I assume that it is doing nothing. The timer checks if the character has flagged itself as attempting a movement behavior, if it did, then the timer fires an interrupt. This same timer also ends a character's turn if no activity is detected for too long.

Here is the activity detection timer code:
Code
EVENT DefaultActivityDetection
//Used as a means to reliably detect when a character
//is timing out.
VARS
	FLOAT:_AP
	FLOAT3:_Pos
	FLOAT3:_OldPos
ON
	OnTimer("AMER_ActivityDetection")
ACTIONS
	IF "c1&c2"
		GetPosition(__Me, _Pos)
		CharacterGetStat(_AP, __Me, ActionPoints)
	THEN
		IF "c1&c2"
			IsEqual(_Pos, _OldPos)
			IsEqual(_AP, %AMER_TimeoutOldAP)
		THEN
			// Need to check if the character is in dialogue, because
			//some characters will be in combat while also in dialogue.
			IF "!c1"
				IsInDialog(__Me)
			THEN
				Add(%AMER_TimeoutDetection, 1)
				
				//If a behavior has flagged that it may be doing something that
				//could stall, interrupt it, if it has started stalling.
				IF "!c1"
					IsEqual(%AMER_InterruptBehavior, null)
				THEN
					IF "c1"
						IsGreaterThen(%AMER_TimeoutDetection, 2)
					THEN
						//StatusText(__Me, "AMER_TEST_5")	//TEST
						Interrupt(%AMER_InterruptBehavior)
					ENDIF
				ENDIF
				
				//Non-bosses get 4 ticks before timeout. Bosses get 12 ticks before
				//timeout because they tend to have long scripted components.
				IF "c1"
					IsGreaterThen(%AMER_TimeoutDetection, INT:4)
				THEN
					IF "!c1"
						CharacterIsBoss(__Me)
					THEN
						CharacterPlayEffect(__Me, "FX_GP_Status_SkipTurn_A_Icon", "Dummy_OverheadFX")
						StatusText(__Me, "AMER_TEST_TurnTimeout")	//TEST
						CharacterEndTurn()
					ELIF "c1"
						IsGreaterThen(%AMER_TimeoutDetection, INT:12)	//Bosses have extra time.
					THEN
						CharacterPlayEffect(__Me, "FX_GP_Status_SkipTurn_A_Icon", "Dummy_OverheadFX")
						StatusText(__Me, "AMER_TEST_TurnTimeout")	//TEST
						CharacterEndTurn()
					ENDIF
				ENDIF
			ELSE
				//Give extra time if this character is in a dialogue.
				Set(%AMER_TimeoutDetection, INT:-6)
			ENDIF
		ELSE
			Set(%AMER_TimeoutOldAP, _AP)
			Set(_OldPos, _Pos)
			//External behaviors may set this below zero.
			IF "c1"
				IsGreaterThen(%AMER_TimeoutDetection, INT:0)
			THEN
				Set(%AMER_TimeoutDetection, INT:0)
			ENDIF
		ENDIF
	ENDIF


To use this interrupt, we must provide the behavior reaction name before using CharacterMoveInRange():
Code
		SetVar(__Me, "AMER_InterruptBehavior", FIXEDSTRING:CastOnCharacter_$1)
		CharacterMoveInRange(_Target, _minRange, _maxRange, 1)
		SetVar(__Me, "AMER_InterruptBehavior", FIXEDSTRING:null)


Additionally, code to handle the interrupt must be added to a generic INTERRUPT block of the behavior reaction--because OnManualInterrupt() doesn't appear to work.
Code
INTERRUPT
	//OnManualInterrupt doesn't seem to work. Catch the manual
	//interupt behavior in this manner instead.
	IF "c1&!c2"
		GetVar(_Interrupt, __Me, "AMER_InterruptBehavior")
		IsEqual(_Interrupt, null)
	THEN
		//StatusText(__Me, "AMER_TEST_2")	//TEST
		SetVar(__Me, "AMER_InterruptBehavior", FIXEDSTRING:null)
		SetVar(__Me, "AMER_TimeoutDetection", INT:0)	//Prevent timeout while deliberating.
		DelayReaction("CastOnCharacter_$1", 2)
	ENDIF



With the former code in place, our enemies will no longer remain stalled for too long. However this is not yet ideal behavior, as they may still end up passing many turns if they continue to attempt to target the same unreachable target. To remedy this problem, we need to implement a means for the AI to recalculate its target priority while ignoring unreachable targets. I have done this with a blacklist infrastructure.

The blacklist is composed of enough int variables to handle however many player indices might be present in combat--one for each player and one for each of their possible summons--I use twelve to support up to six players. Additionally, there is one generic character variable to support blacklisting of one NPC (relevant for charmed targets or combat with friendly NPCs). These variables should be reset any time the state of pathing could change; this commonly means at the beginning of a turn, but can also occur when the character destroys an Ice Wall or uses a movement spell. This is the code used to blacklist a target--called from interrupts of movement failure:

Code
	INT:%AMER_BlacklistUsed = 0
	INT:%AMER_BlacklistIndex0 = 0
	INT:%AMER_BlacklistIndex1 = 0
	INT:%AMER_BlacklistIndex2 = 0
	INT:%AMER_BlacklistIndex3 = 0
	INT:%AMER_BlacklistIndex4 = 0
	INT:%AMER_BlacklistIndex5 = 0
	INT:%AMER_BlacklistIndex6 = 0
	INT:%AMER_BlacklistIndex7 = 0
	INT:%AMER_BlacklistIndex8 = 0
	INT:%AMER_BlacklistIndex9 = 0
	INT:%AMER_BlacklistIndex10 = 0
	INT:%AMER_BlacklistIndex11 = 0
	CHARACTER:%AMER_BlacklistGeneric = null
	CHARACTER:%AMER_BlacklistCmp = null
	
	
	
EVENT AttackMovementFailedDeliberation
//If my target is an unreachable player, blacklist it
//and flag to reevaluate targets. If all players are
//blacklisted, check for Ice Walls.
VARS
	CHARACTER:_Enemy
	CHARACTER:_Player
	INT:_Count
	INT:_PartySize
	STRING:_Str
	FIXEDSTRING:_BlacklistVar
ON
	OnFunction("AMER_AttackMovementFailedDeliberation")
ACTIONS
	//Prevent timeout when deliberation is happening.
	//Set(%AMER_TimeoutDetection, INT:0)
	
	IF "c1"
		GetPlayerCount(_PartySize)
	THEN
		//StatusText(__Me, "AMER_ChainOne")	//TEST
		IF "c1&c2"
			CharacterGetEnemy(_Enemy, __Me)
			CharacterIsPlayer(_Enemy)
		THEN
			Set(_Count, INT:0)
			WHILE "c1"
				IsLessThen(_Count, _PartySize)
			DO
				IF "c1&c2"
					GetPlayerByIndex(_Player, _Count)
					IsEqual(_Player, _Enemy)
				THEN
					//StatusText(_Player, "AMER_ExtraCrit")	//TEST
					Print(_Str, "AMER_BlacklistIndex[1]", _Count)
					Cast(_BlacklistVar, _Str)
					SetVar(__Me, _BlacklistVar, INT:1)
					Add(%AMER_BlacklistUsed, INT:1)
					
					//Flag to reevaluate targets.
					CharacterSetEnemy(__Me, null)
					Set(%defaultEvaluateTarget, INT:1)
				ENDIF
				
				Add(_Count, INT:1)
			ENDWHILE
		//If it was an NPC and I haven't blacklisted one
		//yet, blacklist it and reevaluate.
		ELIF "c1"
			IsEqual(%AMER_BlacklistGeneric, null)
		THEN
			//StatusText(_Enemy, "AMER_ExtraCrit")	//TEST
			Set(%AMER_BlacklistGeneric, _Enemy)
			//Flag to reevaluate targets.
			CharacterSetEnemy(__Me, null)
			Set(%defaultEvaluateTarget, INT:1)
		ENDIF
	ENDIF


The system checks if the unreachable target was a player and, if it was, obtains the character's playerindex and flags the corresponding blacklist variable. The blacklist variables are considered in the default attack target deliberation routine when any of them have been flagged. Here is the code for this check, called from any target deliberation routine before recording a target:

Code
EVENT DefaultCheckIfBlacklisted
//Compare %AMER_BlacklistCmp against the active player characters
//to see if it has been blacklisted.
VARS
	CHARACTER:_Player
	INT:_Count
	INT:_PartySize
	INT:_IsBlacklisted
	STRING:_Str
	FIXEDSTRING:_BlacklistVar
ON
	OnFunction("AMER_CheckIfBlacklisted")
ACTIONS
	IF "c1"
		GetPlayerCount(_PartySize)
	THEN
		Set(_Count, INT:0)
		WHILE "c1&!c2"
			IsLessThen(_Count, _PartySize)
			IsEqual(%AMER_BlacklistCmp, null)
		DO
			IF "c1&c2"
				GetPlayerByIndex(_Player, _Count)
				IsEqual(_Player, %AMER_BlacklistCmp)
			THEN
				Print(_Str, "AMER_BlacklistIndex[1]", _Count)
				Cast(_BlacklistVar, _Str)
				IF "c1&c2"
					GetVar(_IsBlacklisted, __Me, _BlacklistVar)
					IsEqual(_IsBlacklisted, INT:1)
				THEN
					//Character is blacklisted, set to null to escape
					//loop and denote that character should be ignored.
					Set(%AMER_BlacklistCmp, null)
				ENDIF
			ENDIF
			
			Add(_Count, INT:1)
		ENDWHILE
	ENDIF


With the addition of this "custom" interrupt and the blacklist system, characters will only pass their turns if every potential target is unreachable, and should never be met with a situation that does not generate an interrupt from movement failure. Since the implementation of this AI, I have yet to witness a single creature's turn ever stall, even when considering extremely tight scenarios with many characters.

While this AI does greatly improve combat performance of enemies, the implications of this system are grand; one could tailor movement spells and teleport spells to be used only when high-priority targets are unreachable, for example.

Last edited by Ameranth; 29/03/17 09:23 AM.
Joined: Feb 2010
apprentice
Offline
apprentice
Joined: Feb 2010
Are you Sandpie on Steam? If so, I am testing your mod now. OMG is it awesome! I am using XC encounters actually. Can't wait for new version. I'm so glad someone else is making a nice mod for EE.


ok
Joined: Dec 2016
Location: United States
member
OP Offline
member
Joined: Dec 2016
Location: United States
I am, thanks for playing!


Link Copied to Clipboard
Powered by UBB.threads™ PHP Forum Software 7.7.5