Your First Story Script

From Divinity Engine Wiki
Jump to: navigation, search

Note: this tutorial assumes you have already created a mod project, and have a general idea on Osiris syntax. For the purposes of this tutorial, we will be using a basic "Addon" project that targets "Story".

Required Reading

The following is required reading, as it covers the Story Editor, and Osiris syntax and concepts:

Recommended Reading

API Docs

Overview

While some basic concepts are covered here, Osiris syntax and defintitions should be referred to in Osiris Overview, as the goal of this tutorial is to guide you in creating a foundation for your code to run, via a parent script and child script.

Starting Tips

Before we get started, some basic tips to keep in mind:

Generate Definitions

When opening the story editor in a new project, always generate definitions (File -> Generate Definitions) and exit/re-open the story editor. This will generate the files necessary for auto-complete and syntax highlighting.

Prefixing

Prefix your goals (a.k.a. your script files), databases, queries, and procs in a way that will make then unique and least likely to conflict with other mods. For example, if our mod is called "Better Cupcakes", and our username is "SuperChef", we could prefix our mod entries with SCBC, or SC_BetterCupcakes, or some combination of the two.

This concept also applies for entries in the stats editor, as multiple entries with the same name will conflict.

Implementing a Basic Story Script

Getting Started


  1. Open the Story editor by clicking the icon that looks like a book (StoryEditor Icon.png) under the "Editors" section, at the top-right of the Divinity Engine 2 editor.
  2. Inside the story editor, Click "File -> Generate Definitions and Build" on the top left.
    • This is an important step in generating the appropriate files. Any time you create new definitions (databases, PROCs, or queries), you need to generate definitions.
    • Building is as equally important, as your changes won't compile until you build.
    • Reloading the story is necessary to see your changes in-game. You can either do this from the story editor ("File -> Reload Story"), or from the main editor with "File -> Reload Level and Story".
  3. Close the story editor and open it again to essentially reload the syntax highlighting and auto-complete features.

Creating a Parent Goal


For organizational purposes, creating a parent story goal is recommended. For the sake of this tutorial, we'll be parenting our parent goal to __Start, which is a goal provided by Shared.

  1. Inside the story editor, at the goal list on the left, right click "__Start" and select "Add New Sub Item...".
Right click the goal and click "Add New Sub Item..."
  1. Name this script something unique to your mod in a prefix format (Username_ModName), such as "SuperChef_BetterCupcakes". This has the best chance of not potentially conflicting with any other mod.
  2. After clicking OK, your script will automatically open. Hit CTRL+S to automatically save this script. Scripts with unsaved changes display and asterisk (*) next to their name.

Tip: Story Script Naming


Goals in the story are ran in alphabetical order. This is important to keep in mind if you use a particular story goal as a sort of "top-level" script that has methods and databases your other goals reference to.

With proper naming, you can control the order your scripts are run. For example, naming your top-level goal "WT_FS__Main", and your second goal "WT_FS_Skills" ensures "WT_FS__Main" will always run before "WT_FS_Skills".

WT_FS__Main will always run before WT_FS_Skills.

Completing the Parent Goal


Parent goals need to "complete" before their sub-goals will run. This is done by calling "GoalCompleted;" within the parent script. We have a few options for how to go about this:

Parenting to a DOS2 Goal


Instead of handling the completion events yourself, simply parenting your parent goal to some specific goals provided by Larian is an easy way to ensure your goal runs at the right time.

Parenting to __Start (No Origins Dependency)
If your mod doesn't depend on Origins (see: Dependencies, parenting your parent goal to __Start will ensure that it runs when the game is ready.
Parenting to Start (Origins Dependency)
Start (not to be confused with __Start) is a top-level goal in Origins that completes when character creation is ready, or if a mode other than "Campaign" is started. Parenting to this goal requires your mod to have Origins as a dependency.
Manual Completion
Rather than relying on an existing parent goal, it's possible to make a top-level goal that completes on its own. See more on that here: Manually Completing Parent Goals.

Parenting Example


Inside of the INIT section of our new goal, enter the following:

DB_WikiTutorial_ModStarted(1);

Next, inside the KB section of your goal, enter the following:

IF
DB_WikiTutorial_ModStarted(1)
THEN
DebugBreak("[WT_FS] WikiTutorial_FirstStory has initialized.");
GoalCompleted;

The goal should look like so:

FirstStoryScript ParentGoal.png
  1. Save, then select "File -> Build and Reload".
  2. Minimize the story editor.
  3. Inside the message log, you should see the message we added with DebugBreak.
The debug message appears in the message log once the game is started.

The way this example works is, as soon as __Start calls GoalCompleted; (which occurs when the event GameEventSet("GAMEEVENT_GameStarted") fires), our goal then sets a database value, which then triggers our own code for GoalCompleted.

Additionally, if this mod is added to an existing save, where __Start was already completed, our parent goal will complete as well, due to the way the parent-child goal relationship works (if a parent is already complete when a new child is added, that child will run).

Adding a Sub-Goal


With the parent goal completing properly, parented sub-goals will now run.

Creating a Sub-Goal


Let's create a basic script that will display some text when a skill is cast.

  1. Double click on your parent goal to have it selected.
    • Currently, there's a bug where not having your parent goal open may cause a sub-item created to not properly parent under the right goal. If this happens, simply click and drag the new goal to your parent goal, and it should move underneath it.
  2. Click "Add New Sub Item...", then name it something appropriate, like "WikiTutorial_FirstStory_Skills".
  3. Save the new script (CTRL+S).

Creating a Rule


In the KB section of our new script, add the following:

IF
DB_IsPlayer(_Player)
AND
CharacterHasSkill(_Player, "Shout_InspireStart", 0)
THEN
CharacterAddSkill(_Player, "Shout_InspireStart");
DebugBreak("[WT_FS] Added Encourage to player.");

IF
SkillCast(_Character, "Shout_InspireStart", _, _)
AND
DB_IsPlayer(_Character)
THEN
DisplayText(_Character, "<font color='#00FF00' size='30'>Good job!</font>");
CharacterResetCooldowns(_Character);

This rule will run when the player casts "Encourage", displaying some text, and resetting their cooldowns. The Encourage skill is added automatically to the player if they lack it.

  1. Save, then "Generate Definitions and Build".
  2. In the main editor, select "File -> Reload Level and story".
  3. Hit the "Switch Game/Editor" button to get in-game.
    Click this button to switch between game and editor mode.
  4. In whatever level you're working in, your provided dummy character should spawn and be given the Encourage skill.
  5. Cast the skill to test that your script is working properly.
Our script in action.
Notes

HTML Font Formatting

  • HTML font formatting can be used in DisplayText, CharacterStatusText, and in various text entries (skill descriptions, status names/descriptions, book text, and more).

SkillCast

  • This event runs when the skill's animation reaches the CastTextEvent value specified by the skill stats (which is "cast" in most cases).
  • The textkey "cast" is a common textkey used in skill cast animations.
  • To view the textkeys present in an animation, open it within the Content Browser.
Open the Content Browser with this button.
  • Example: Open the oontent browser and search for the "Humans_Hero_Female_ABC_Skill_Cast_Aoe_Air_01_Cast" animation under the Shared folder. Double click on the resource to open the animation preview to see the different textkeys.
    The cast textkey.

DB_IsPlayer

  • This is a base game database that is populated with all the current player characters when the game is started.
  • By passing in the _Character value from the event parameters to DB_IsPlayer, we're matching the character against the database of player characters, to have the following code only run if the caster is a player-controlled character.
  • Using DB_IsPlayer as an event makes the subsequent code run when a character is added to the DB_IsPlayer database.

Taking the Script Further

Now that we have a basic script heirarchy working, let's take the script further.

Randomization


We can add randomization to the displayed text in our previous script to make the result more interesting.

We'll do this by creating a custom procedure.

Procedure Tips
Procedures are declared with a "PROC" at the top of the block, and follow the same flow as a rule.
Parameters for a procedure can be customized. You must declare their type.
Procedures are called within the "call" section of a rule or a custom query.

Add this to the INIT section:

//DB_WikiTutorial_FirstStory_RandomText(_Ran, _Text, _FontColor, _FontSize)
DB_WikiTutorial_FirstStory_Skills_RandomText(0, "Woo!", "00CED1", "23");
DB_WikiTutorial_FirstStory_Skills_RandomText(1, "Good job!", "00FF00", "30");
DB_WikiTutorial_FirstStory_Skills_RandomText(2, "I did it!", "DEB887", "20");
DB_WikiTutorial_FirstStory_Skills_RandomText(3, "I'm da best!", "A52A2A", "26");

Change the code in the KB section to the following:

IF
DB_IsPlayer(_Player)
AND
CharacterHasSkill(_Player, "Shout_InspireStart", 0)
THEN
CharacterAddSkill(_Player, "Shout_InspireStart");
DebugBreak("[WT_FS] Added Encourage to player.");

PROC
WikiTutorial_FirstStory_Skills_DisplayRandomMessage((CHARACTERGUID)_Character)
AND
Random(4, _Ran)
AND
DB_WikiTutorial_FirstStory_Skills_RandomText(_Ran, _Text, _FontColor, _FontSize)
AND
StringConcatenate("<font color='#", _FontColor, _Str1)
AND
StringConcatenate(_Str1, "' size='", _Str2)
AND
StringConcatenate(_Str2, _FontSize, _Str3)
AND
StringConcatenate(_Str3, "'>", _Str4)
AND
StringConcatenate(_Str4, _Text, _Str5)
AND
StringConcatenate(_Str5, "</font>", _Message)
AND
IntegertoString(_Ran, _RanStr)
AND
StringConcatenate("[WT_FS] Rolled a ", _RanStr, _DebugMessage)
THEN
DisplayText(_Character, _Message);
DebugBreak(_DebugMessage);

IF
SkillCast(_Character, "Shout_InspireStart", _, _)
AND
DB_IsPlayer(_Character)
THEN
WikiTutorial_FirstStory_Skills_DisplayRandomMessage(_Character);
CharacterResetCooldowns(_Character);
  1. Save, then "File -> Generate Definitions, Build and Reload".
  2. Switch back to the main editor and hit the play button to get in-game.
  3. Cast the Encourage skill a few times in game mode to ensure the script is working properly. You should see the different messages, font colors, and font sizes.
Notes

Procedures

  • WikiTutorial_FirstStory_Skills_DisplayRandomMessage is a custom subroutine call, called a PROC. By splitting our message logic off into a separate procedure, this ensures the calls for this rule are ran regardless of the result of our message logic (so CharacterResetCooldowns will always run if a player casts this skill). This also allows us to use this procedure elsewhere, and possible extend it further.

Databases

  • We create our "RandomText" settings in the INIT section by using a database we create, called "DB_WikiTutorial_FirstStory_Skills_RandomText". Databases are a powerful tool for making your scripting logic extensible.

Random

  • Random returns a random integer value between 0 and (_Modulo - 1). Since our DB_WikiTutorial_FirstStory_Skills_RandomText has 4 entries, we want our random number to be between 0 and 3, so we use 4 as our _Modulo.

Using Queries


Mastering queries is essential for creating complex conditions for your rules. "OR" logic is done within Osiris with multiple rules, subscribing to the same events, that look for different conditions through the use of queries.

Let's add to our skills script. Add this to the KB section, so your SkillCast rules look like this:

IF
SkillCast(_Character, "Shout_InspireStart", _, _)
AND
DB_IsPlayer(_Character)
THEN
WikiTutorial_FirstStory_Skills_DisplayRandomMessage(_Character);
CharacterResetCooldowns(_Character);

IF
SkillCast(_Character, "Shout_InspireStart", _, _)
AND
NOT DB_IsPlayer(_Character)
THEN
DisplayText(_Character, "Win... Please! Ahhhh!");
ApplyStatus(_Character, "FEAR", 1.0, 1);

By simpling adding a NOT before our database check, our second rule will now run on any character (who's not a player) that casts Encourage, and apply fear. You can test this by loading an inherited level that contains some human enemies, or adding human enemies yourself to your test level.

Creating a Custom Query


Taking this idea further, we'll create a custom query.

Custom queries are defined with the keyword "QRY" at the top of a block, and follow the same flow as a rule.

Add the following code to your KB section:

QRY
WikiTutorial_FirstStory_QRY_CanApplyBonus((CHARACTERGUID)_Character)
AND
IsTagged(_Character, "HUMAN", 1)
THEN
DB_NOOP(1);

QRY
WikiTutorial_FirstStory_QRY_CanApplyBonus((CHARACTERGUID)_Character)
AND
IsTagged(_Character, "DWARF", 1)
THEN
DB_NOOP(1);

By adding two query blocks with the same name, we're effectively adding an "OR" condition. This query will evaluate as true if the character is tagged as either a human or a dwarf.

"DB_NOOP(1);" is a dummy database fact. Since rules must have a call, DB_NOOP is used to have a rule succeed without using any actual calls.

Implementing the Custom Query


Below our query code, in the KB section, add the following:

IF
CharacterStatusApplied(_Character, "ENCOURAGED", (CHARACTERGUID)_Causee)
AND
WikiTutorial_FirstStory_QRY_CanApplyBonus(_Character)
AND
CharacterConsume(_Character, "POTION_Minor_Perception_Potion", _Handle)
THEN
DB_WikiTutorial_FirstStory_ConsumeHandles(_Character, _Handle, "POTION_Minor_Perception_Potion");

IF
CharacterStatusApplied(_Character, "ENCOURAGED", (CHARACTERGUID)_Causee)
AND
NOT WikiTutorial_FirstStory_QRY_CanApplyBonus(_Character)
AND
CharacterConsume(_Character, "POTION_Minor_Constitution_Potion", _Handle)
THEN
DB_WikiTutorial_FirstStory_ConsumeHandles(_Character, _Handle, "POTION_Minor_Constitution_Potion");

IF
CharacterStatusRemoved(_Character, "ENCOURAGED", (CHARACTERGUID)_Causee)
AND
DB_WikiTutorial_FirstStory_ConsumeHandles(_Character, _Handle, _Potion)
THEN
CharacterUnconsume(_Character, _Handle);
NOT DB_WikiTutorial_FirstStory_ConsumeHandles(_Character, _Handle, _Potion);
  1. Save, then "File -> Generate Definitions, Build and Reload". Since we added a query and another database, we must generate the definitions again.
  2. Go in-game and test out the changes by casting Encourage on an human or dwarf (it will work on yourself as well).
Notes

NOT

  • By adding two rules for the same event, and checking for the opposite of our query, we're effectively adding an OR condition to this event.

CharacterConsume/CharacterUnconsume

  • Since CharacterConsume returns a value, it's a query instead of a call. We store the out value from the query (_Handle) in a database to later unconsume the potion effect when ENCOURAGED ends.

Removing Database Entries

  • When unconsuming the potion handle, notice we also remove that entry from the database by calling "NOT DB_WikiTutorial_FirstStory_ConsumeHandles(_Character, _Handle, _Potion);" as a call. Using NOT before a database entry (in the call section) removes that entry.
  • By using DB_WikiTutorial_FirstStory_ConsumeHandles(_Character, _Handle, _Potion) in the query section, the _Handle and _Potion value is returned with all entries that match the value we pass in (_Character), effectively unconsuming every potion handle we added for that character to the database.

Using a Query as a Subroutine


While queries can be used as conditions, they can also be used to call specific procedures/calls before the main rule reaches that section of the flow.

Let's try a simple example that runs when we unconsume a potion. Change our status removed event code to this:

IF
CharacterStatusRemoved(_Character, "ENCOURAGED", (CHARACTERGUID)_Causee)
AND
WikiTutorial_FirstStory_QRY_ZombieCheck(_Character)
AND
DB_WikiTutorial_FirstStory_ConsumeHandles(_Character, _Handle, _Potion)
THEN
CharacterUnconsume(_Character, _Handle);
NOT DB_WikiTutorial_FirstStory_ConsumeHandles(_Character, _Handle, _Potion);

QRY
WikiTutorial_FirstStory_QRY_ZombieCheck((CHARACTERGUID)_Character)
AND
IsTagged(_Character, "UNDEAD", 1)
AND
CharacterGetDisplayName(_Character, _, _Name)
AND
StringConcatenate("[WT_FS] ", _Name, _Str1)
AND
StringConcatenate(_Str1, " is a zombie! Ahh! Burn it!", _Message)
THEN
ApplyStatus(_Character, "BURNING", 1.0);
DebugBreak(_Message);

QRY
WikiTutorial_FirstStory_QRY_ZombieCheck((CHARACTERGUID)_Character)
THEN
DB_NOOP(1);

Which this, our query will apply burning if the character is tagged as UNDEAD. Note how we added a second QRY rule that always evaluates as true - this is to keep the rest of our rule in the CharacterStatusRemoved event running, undead or not.

Practically speaking, using queries as subroutines is ideal when:

  • You need specific calls/procedures to run before iterating through a database. Calling your query before retrieving variables from a database ensures that those calls will only run once.
  • You need to debug a rule to ensure it reaches a certain section of the conditions. If you're not sure if a specific condition is passing, adding a query with a DebugBreak message right before/after it is a good way to debug that situation.

Bonus Tips

  • In the story editor, select "File -> Open Story Header" for a list of all available events, queries, and calls. Heavily reference this file when you find yourself unsure if a specifc method exists, as not all methods appear in the auto-complete.
  • If you use databases to store global mod settings, and your mod users have played with that mod, changing that information post-mod install will not automatically remove that data from existing saves - you need to handle this update internally within your mod.
  • By using custom procedures and queries, you can reduce your rule redundancy and extend desired calls/conditions to multiple rules.
  • To make your life with Osiris easier, wrap your rule blocks in regions with the following:
//REGION MY_REGION_NAME

//END_REGION
  • In the story editor, right click inside your script sections (INIT, KB, or EXIT) and "Expand All" or "Collapse All" to expand/collapse all regions, respectively.
  • Procedures can be called from the EXIT and INIT section of scripts.
  • Use the INIT section of your scripts as a way to store commented references to your databases. As an example from our previous script, placing this inside your INIT section is a good way to remember such a database exists (since we don't initialize it in the INIT section):
//DB_WikiTutorial_FirstStory_ConsumeHandles(_Character, _Handle, _Potion)

Then, when you wish to use this database, you have a quick way to copy/paste that code into other sections of your script.

Manually Completing Parent Goals


Instead of parenting your parent goal to one of the Start goals, it's possible to handle goal completion yourself, using a few different events:

The following events are recommended to use for completing your parent goals:

GameStarted
Thrown when a level has been loaded and is ready at both the server and client side. This will ensure all the base game scripts have already initialized.
RegionStarted
Thrown when the level has been loaded and is ready at the server side, but not yet at the client side.

Both of these events can be used with specific level names, as a way to make your goal only complete once that level is loaded (as a way to create level-specific logic).

GameEventSet("GAMEEVENT_GameStarted")
The first story event that gets thrown after the game engine has been initialised when starting a new game. This particular event will only fire the first time a new game is started, so additional rules are needed for cases where your mod is added to an existing save.
Osiris/API/SavegameLoaded
Fires when a save is being loaded on an existing save. This can be used to complete your goal on existing saves that just added your mod.

Example Goal Completion


The following is an example of handling initial goal completion yourself:

INIT:

MyMod_Internal_CompleteIfStarted();

KB:

PROC
MyMod_Internal_Start()
THEN
DebugBreak("[MyMod] Parent goal complete.");
GoalCompleted;

IF
GameEventSet("GAMEEVENT_GameStarted")
THEN
MyMod_Internal_Start();

PROC
MyMod_Internal_CompleteIfStarted()
AND
DB_StoryStarted(_)
THEN
DebugBreak("[MyMod:CompleteIfStarted] Story was already started.");
MyMod_Internal_Start();

// For saves where this goal is active, but wasn't completed
IF
SavegameLoaded(_,_,_,_)
AND
DB_StoryStarted(_)
THEN
DebugBreak("[MyMod:SavegameLoaded] Mod added to an existing save.");
MyMod_Internal_Start();