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.
#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-
#INCLUDE Base
INIT
USING Base
CHARACTER:__Me
FLOAT3:%PeaceReturnPosition=null
CHARACTER:%currentSetTargetDefault=null
FLOAT:%setTargetDefaultBestScore=10000
INT:%defaultEvaluateTarget=1
INT:%EvaluateScores=0
#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.
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
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-
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-
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-
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.
This single line starts off the EVENTS section. It is always included if there are any EVENTS
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.
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.
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
The various triggers can be found on the wiki.
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.
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-
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-
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-
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.
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.
This starts off the section
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.
This is the other key difference. Reactions can be set to occur either in PEACE (USAGE PEACE) or in COMBAT (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)
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.
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.