5. CharScripts and ItemScripts

Anatomy of a charScript/itemScript

When it comes to scripting within Divinity - Original Sin, there are basically two types of scripts: Story scripts and CharScripts/ItemScripts. The syntax and commands used are different for the most part between these two types of scripts.

This section deals with CharScripts/ItemScripts. Technically these are separate things, but they use the same structure and commands. The only difference is that CharScripts are used by Characters and ItemScripts are used by items.

First though is how to get a CharScript/ItemScript into a module in the first place. Outside of the editor, you will want to create a Scripts folder within the Public/(Mod Name)/ for your mod. Within that folder you can make an empty .charScript or .itemScript file with the name of your choice.

Next open up the Editor and load up your mod (if it is not done so already). Go to the Resources manager; you will see a list of folders, including one with your mod's name on it. Click that folder and then click the "Create Package" icon (yellow box with a green plus sign next to it). You can name the package anything you want.

Click your new package, and then click the "Import Resource" button on the top left. In the window that pops up, click the charScript/itemScript icon and navigate it towards the empty script file within the Public/(Mod Name)/Scripts/ folder. Once that is done, your charScript/itemScript is now available for use in your module.

To actually attach a charScript to a character, you need to view a Character Template within the Sidebar and click the "Scripts" button. In the window that pops up, you can add your script to the character. ItemScripts can be added to Item Templates.

Now to look at an actual CharScript. For this part, I will use a much shortened version of the DefaultCharacter.charScript that comes with the game.
Code
#INCLUDE Base
INIT

USING Base

CHARACTER:__Me
FLOAT3:%PeaceReturnPosition=null
CHARACTER:%currentSetTargetDefault=null
FLOAT:%setTargetDefaultBestScore=10000
INT:%defaultEvaluateTarget=1
INT:%EvaluateScores=0


EVENTS
EVENT DontAttackAlliesOrInvisibles // to solve charmed chars getting back to normal but still targeting as if they were charmed
VARS
	CHARACTER:_Target
ON
	OnTurn()
ACTIONS
	IF "c1&(c2|c3)"
		CharacterGetEnemy(_Target,__Me)
		CharacterIsAlly(__Me,_Target)
		CharacterHasStatus(_Target,INVISIBLE)
	THEN
		CharacterSetEnemy(__Me,null)
		Set(%defaultEvaluateTarget,1)
	ENDIF
	
	
BEHAVIOUR 

REACTION ReturnToPeacePosition,15000
USAGE PEACE
CHECK "!c1"
	IsEqual(%PeaceReturnPosition,null)
ACTIONS
	CharacterMoveTo(%PeaceReturnPosition,1,1,1,0)
	CharacterEvent(__Me,"ClearPeaceReturn")
	
REACTION Combat_AttackSetEnemy, 7
USAGE COMBAT
VARS
	CHARACTER:_Enemy
	FLOAT:_dist
CHECK "c1&(c2|(c3&!c4))"
	CharacterGetEnemy(_Enemy,__Me) // returns false if null
	CharacterCanSee(__Me,_Enemy)
	GetInnerDistance(_dist,__Me,_Enemy)
	IsGreaterThen(_dist,4.0)
ACTIONS
	CharacterAttack(_Enemy)
INTERRUPT
ON
	OnMovementFailed(_)
ACTIONS
	DelayReaction("Combat_AttackSetEnemy",3)

There are essentially three major portions to a charScript/itemScript file.
1. INIT section- This is where 'global' variables used by the script are initialized (global means anything within the script can use it)
2. EVENTS section- These store the various events that the character/item can respond to
3. BEHAVIOUR section- These hold REACTIONs, which are how the characters behave ingame.

Let's begin with the INIT section, since this is always at the top of the script file-
Code
#INCLUDE Base
INIT

USING Base

CHARACTER:__Me
FLOAT3:%PeaceReturnPosition=null
CHARACTER:%currentSetTargetDefault=null
FLOAT:%setTargetDefaultBestScore=10000
INT:%defaultEvaluateTarget=1
INT:%EvaluateScores=0


Code
#INCLUDE Base
INIT
USING Base

The #INCLUDE and USING indicate that this file is using the contents of another Script file (in this case, the Base.charScript file). This is the game's way of 'extending' charScript functionality. For the most part these two are optional; they don't need to be included if you're not including contents of another charScript.

The INIT, however, is vital in that it indicates that this is the INIT section. It goes after any #INCLUDEs and before anything else.

Code
CHARACTER:__Me

This is a unique variable declaration for a charScript file. The __Me is referring to whichever character that is using the Script. The equivalent line for an itemScript is
Code
ITEM:__Me


Note that the syntax here is VARIABLETYPE (in capital letters) followed by a colon (:) followed by the name of the variable. This is consistent even with the next variable declarations-
Code
FLOAT3:%PeaceReturnPosition=null
CHARACTER:%currentSetTargetDefault=null
FLOAT:%setTargetDefaultBestScore=10000
INT:%defaultEvaluateTarget=1
INT:%EvaluateScores=0

Note that all of these variable names begin with a percentage sign (%), and the variable names are followed by an =(value). These are declaring a variable and assigning a value whenever the script is first initialized in-game (usually whenever the map that the character using the script appears on is loaded).

Before continuing, there is one other important aspect to know about the variable declarations-
Code
EXTERN FLOAT:%setTargetDefaultBestScore=10000

The EXTERN keyword can be used to allow the value to be set outside of the script when it is being assigned to a character/item.


Events section-
Code
EVENTS
EVENT DontAttackAlliesOrInvisibles
VARS
	CHARACTER:_Target
ON
	OnTurn()
ACTIONS
	IF "c1&(c2|c3)"
		CharacterGetEnemy(_Target,__Me)
		CharacterIsAlly(__Me,_Target)
		CharacterHasStatus(_Target,INVISIBLE)
	THEN
		CharacterSetEnemy(__Me,null)
		Set(%defaultEvaluateTarget,1)
	ENDIF

The EVENTS section is where most of the scripting will occur. This is where most of the items in the AI scripting part of the Wiki can be used.
Code
EVENTS

This single line starts off the EVENTS section. It is always included if there are any EVENTS

Code
EVENT DontAttackAlliesOrInvisibles

This is an event declaration. These always start with EVENT, and are followed by any unique name that you want. Just make sure that this name doesn't overlap with any other EVENT used by the same character.

Code
VARS
	CHARACTER:_Target

Between the EVENT declaration and the ON section (coming next) can be the Variable declaration section. In this section you include the data type and name of any and all variables used within the EVENT (unless they are global variables from the INIT section). The syntax here is similiar to the INIT section, but instead of a (%) an underscore is used before the variable name (and these are not initialized to any value). If the EVENT doesn't use any other variables, this section isn't necessary.

Code
ON
	OnTurn()

The ON section is required for all EVENTS. This is the part that says when the EVENT should trigger. It's not shown here, but multiple things can trigger the event if they are separated by a line
Code
ON
	OnTurn()
	OnInit()

The various triggers can be found on the wiki.

Code
ACTIONS
	IF "c1&(c2|c3)"
		CharacterGetEnemy(_Target,__Me)
		CharacterIsAlly(__Me,_Target)
		CharacterHasStatus(_Target,INVISIBLE)
	THEN
		CharacterSetEnemy(__Me,null)
		Set(%defaultEvaluateTarget,1)
	ENDIF

The second required part of an EVENT is the ACTIONS section, which tells the game what happens when the event triggers.

Code
IF "c1&(c2|c3)"
		CharacterGetEnemy(_Target,__Me)
		CharacterIsAlly(__Me,_Target)
		CharacterHasStatus(_Target,INVISIBLE)

An IF statement can be within the ACTIONS section. All IF statements must contain an IF, THEN, and ENDIF at a minimum. Note how there is "c1&(c2|c3)" within quotes immediately after the IF; these are the conditions that the IF statement is checking before deciding whether to proceed to the THEN section.

But what is "c1&(c2|c3)" referring to? These are the lines immediately following the IF statement-
Code
c1 - CharacterGetEnemy(_Target, __Me)
c2 - CharacterIsAlly(__Me, _Target)
c3 - CharacterHasStatus(_Target, INVISIBLE)

The & within the conditional means AND, and the | means OR. In total, as long as c1 is true and either c2 or c3 are true, the IF statement will trigger.

If you wanted to add another conditional, you could have it like so-
Code
IF "c1&(c2|c3)&c4"
		CharacterGetEnemy(_Target,__Me)
		CharacterIsAlly(__Me,_Target)
		CharacterHasStatus(_Target,INVISIBLE)
		CharacterHasStatus(_Target,INVISIBLE)

Note that certain script commands can only be used within an IF statement (like all of the ones used in this example). The IF statement is where variable values can be obtained from commands.

After the conditionals comes the THEN section-
Code
THEN
		CharacterSetEnemy(__Me,null)
		Set(%defaultEvaluateTarget,1)
ENDIF

Notice that here is where global variables within the script are used. The Set(%defaultEvaluateTarget,1) is how the value of 1 can be assigned to that particular variable.


BEHAVIOUR section- (Full Disclosure: I haven't used BEHAVIOURs before, so take my analysis of this part with a grain of salt)
Lastly there is the BEHAVIOUR section, which houses the different behaviours NPCs can have within the game.
Code
BEHAVIOUR 

REACTION ReturnToPeacePosition,15000
USAGE PEACE
CHECK "!c1"
	IsEqual(%PeaceReturnPosition,null)
ACTIONS
	CharacterMoveTo(%PeaceReturnPosition,1,1,1,0)
	CharacterEvent(__Me,"ClearPeaceReturn")
	
REACTION Combat_AttackSetEnemy, 7
USAGE COMBAT
VARS
	CHARACTER:_Enemy
	FLOAT:_dist
CHECK "c1&(c2|(c3&!c4))"
	CharacterGetEnemy(_Enemy,__Me) // returns false if null
	CharacterCanSee(__Me,_Enemy)
	GetInnerDistance(_dist,__Me,_Enemy)
	IsGreaterThen(_dist,4.0)
ACTIONS
	CharacterAttack(_Enemy)
INTERRUPT
ON
	OnMovementFailed(_)
ACTIONS
	DelayReaction("Combat_AttackSetEnemy",3)


There are a few key differences between this section and the EVENTS section.
Code
BEHAVIOUR

This starts off the section

Code
REACTION Combat_AttackSetEnemy, 7

Here the syntax is REACTION followed by the reaction name, then a comma, and then an integer denoting the "Priority" of the reaction. The Priority is the key difference between this section and the EVENTS. In general, only one Behaviour can be active at a time, and the Behaviour that is active is the one with the highest Priority. Reactions with a priority of less than (or equal to?) 0 will never occur unless something raises the Priority higher than 0. In this example, the reaction with the priority of 15000 will be prioritized over the reaction with a priority of 7.

Code
USAGE COMBAT

This is the other key difference. Reactions can be set to occur either in PEACE (USAGE PEACE) or in COMBAT (USAGE COMBAT).

Code
VARS
	CHARACTER:_Enemy
	FLOAT:_dist
CHECK "c1&(c2|(c3&!c4))"
	CharacterGetEnemy(_Enemy,__Me) // returns false if null
	CharacterCanSee(__Me,_Enemy)
	GetInnerDistance(_dist,__Me,_Enemy)
	IsGreaterThen(_dist,4.0)
ACTIONS
	CharacterAttack(_Enemy)

The VARS and ACTIONS sections are practically identical to how they work in the EVENTS section. However, instead of an ON section, Reactions can use a CHECK section instead which follows most of the same rules as IF statements do with regards to conditionals.

Code
INTERRUPT
ON
	OnMovementFailed(_)
ACTIONS
	DelayReaction("Combat_AttackSetEnemy",3)

The Interrupt is basically an Event-like addon to a Reaction that tells the Character to do something special when an event is triggered.


Story and charScripts/itemScripts working together

Story scripts and charScripts/itemScripts can work together to great effect. This part will be a case study on a short example that I came up with.

charScript code-
Code
INIT

CHARACTER:__Me
INT:%hasRespawned=0

EVENTS
EVENT LetThereBeLife
ON
	OnInit()
ACTIONS
IF "!c1"
	IsEqual(%hasRespawned, 0)
THEN
	CharacterEvent(__Me,"TakeThatWhichWasGiven")
ENDIF
IF "c1&c2"
	IsEqual(%hasRespawned, 0)
	CharacterIsDead()
THEN
	CharacterEvent(__Me, "RezMePlz")
ENDIF
	
	
EVENT LetThereBeDeath
ON
	OnCharacterEvent(__Me, "TakeThatWhichWasGiven")
ACTIONS
	CharacterDie(__Me)


Story code-
Code
IF
CharacterEvent(_Char, "RezMePlz")
THEN
CharacterResurrect(_Char);
CharacterSetVarInteger(_Char, "hasRespawned", 1);


When the charScript is first initialized, %hasRespawned is set to 0. By itself, the variable %hasRespawned will always be 0 because there is nothing in the charScript to change it.

Note that when the character that the script is attached to is dead and the game is loaded up (ie triggering OnInit()), the LetThereBeLife EVENT will trigger the CharacterEvent(__Me, "RezMePlz") command because %hasRespawned is 0. At that time, the Story code picks up that event and executes its code, resurrecting the dead character and setting %hasRespawned to 1.

Code
CharacterSetVarInteger(_Char, "hasRespawned", 1);

The syntax of this line is important in that the % is omitted from the variable name in the middle. The CharacterSetVar commands and CharacterGetVar commands can be used to interact with the Global variables within a given character's charScript.

After %hasRespawned is set to 1, then the next time that the game is loaded (or map changes) then the first part of the LetThereBeLife EVENT will trigger, eventually resulting in the character dying because the change to the %hasRespawned variable persists.


Note: This section of the tutorial doesn't cover everything there is to charScript/itemScripting. Notably, Generics in charScript/itemScripts (ie the stuff that uses _$) are not covered (because I have 0 experience in making them for D:OS). Overwriting Main campaign charScripts are not covered here as well.