Your First Story Script

From Divinity Engine Wiki
Revision as of 16:30, 19 December 2017 by LaughingLeader (talk | contribs)
Jump to: navigation, search

Note: this tutorial assumes you have already created a mod project. More on that here: Project browser. For the purposes of this tutorial, we will be using a basic Addon project that inherits from Story and targets Story.

Overview

Story scripts are a way to implement global logic with your mod, in a way that is compatible with other mods. Before we get started, some basic guidelines to keep in mind:

Guidelines


  • On new mod projects, always generate definitions (File -> Generate Definitions) and exit/re-open the story editor. This will generate the auto-complete and syntax highlighting for the story editor.
  • 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'd prefix our mod entries with SCBC, or SC_BetterCupcakes, or some combination of the two.
    • This idea should also be used for entries in the stats editor, as multiple entries with the same name will conflict.
  • 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 with your mod.

Recommended Reading

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.

  1. Inside the story editor, at the goal list on the left, right click and select "Add New Item".
FirstStoryScript AddNewItem.png
  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:

Completion 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).

Example Goal Completion


Inside the KB section of your goal, enter the following:

IF
GameStarted(_Level, _EditorMode)
AND
StringConcatenate("[WT_FS] WikiTutorial_FirstStory has initialized. Level[", _Level, _Str1)
AND
StringConcatenate(_Str1, "] EditorMode(", _Str2)
AND
IntegertoString(_EditorMode, _ModeStr)
AND
StringConcatenate(_Str2, _ModeStr, _Str3)
AND
StringConcatenate(_Str3, ")", _Message)
THEN
DebugBreak(_Message);
GoalCompleted;
  1. Save, then select "File -> Generate Definitions, Build and Reload".
  2. Minimize the story editor, then clear the message log by clicking "Clear".
  3. Load a level in the main editor. 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.
Standard Goal Completion

For a regular approach, without any debug messages or string combining, use this:

IF
GameStarted(_,_)
THEN
GoalCompleted;

In this code block, the variables for _Level and _EditorMode are underscores, which means we're ignoring their values.

Adding a Sub-Goal


With the parent goal completing properly, sub-goals that are parented to the previous goal 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 Resource Manager.
  • Example: Open the resource manager 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". Using databases in this way is a powerful way to design your script logic in a way that is 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 unconsume the consumed 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 against 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 your 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.
  • 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.