Difference between revisions of "Your First Story Script"
m |
m (Added a screenshot for the content browser button.) |
||
(19 intermediate revisions by 2 users not shown) | |||
Line 1: | Line 1: | ||
− | Note: this | + | Note: this tutorial assumes you have already [[Project browser|created a mod project]], and have a [[Osiris Overview|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: | ||
+ | * [[Story_editor|Story Editor]] | ||
+ | * [[Osiris Overview]] | ||
+ | |||
+ | == Recommended Reading == | ||
+ | * [[Osiris_Tips_and_Examples|Osiris Tips and Examples]] | ||
+ | * [[Osiris Design Patterns]] | ||
+ | * [[Osiris Gotchas]] | ||
+ | |||
+ | === API Docs === | ||
+ | * [[:Category:Osiris_APIs|Osiris APIs]] | ||
+ | * [[:Category:Osiris_Calls|Osiris Calls]] | ||
== Overview == | == 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 == | == Implementing a Basic Story Script == | ||
Line 33: | Line 41: | ||
=== Creating a Parent Goal === | === Creating a Parent Goal === | ||
---- | ---- | ||
− | For organizational purposes, creating a parent story goal is recommended. | + | 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. |
− | # Inside the story editor, at the goal list on the left, right click and select "Add New Item". | + | |
− | [[File: | + | # Inside the story editor, at the goal list on the left, right click "__Start" and select "Add New Sub Item...". |
+ | [[File:FirstStoryScript_ParentingToStart.png|thumb|Right click the goal and click "Add New Sub Item..."]] | ||
# 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. | # 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. | ||
# 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. | # 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. | ||
Line 50: | Line 59: | ||
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: | 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: [[Project_settings_window#The_Dependencies_Tab|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: [[Your_First_Story_Script#Manually_Completing_Parent_Goals|Manually Completing Parent Goals]]. | ||
− | ==== Example | + | ==== Parenting Example ==== |
---- | ---- | ||
− | Inside the | + | Inside of the INIT section of our new goal, enter the following: |
<pre> | <pre> | ||
− | + | DB_WikiTutorial_ModStarted(1); | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
</pre> | </pre> | ||
− | |||
− | |||
− | |||
− | |||
− | + | Next, inside the KB section of your goal, enter the following: | |
− | |||
− | |||
<pre> | <pre> | ||
IF | IF | ||
− | + | DB_WikiTutorial_ModStarted(1) | |
THEN | THEN | ||
+ | DebugBreak("[WT_FS] WikiTutorial_FirstStory has initialized."); | ||
GoalCompleted; | GoalCompleted; | ||
</pre> | </pre> | ||
− | + | ||
+ | The goal should look like so: | ||
+ | [[File:FirstStoryScript_ParentGoal.png|thumb]] | ||
+ | |||
+ | # Save, then select "File -> Build and Reload". | ||
+ | # Minimize the story editor. | ||
+ | # Inside the message log, you should see the message we added with DebugBreak. | ||
+ | [[File:FirstStoryScript_ParentGoalComplete.png|thumb|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 === | === Adding a Sub-Goal === | ||
---- | ---- | ||
− | With the parent goal completing properly, sub-goals | + | With the parent goal completing properly, parented sub-goals will now run. |
==== Creating a Sub-Goal ==== | ==== Creating a Sub-Goal ==== | ||
Line 122: | Line 126: | ||
IF | IF | ||
− | SkillCast(_Character, "Shout_InspireStart", _) | + | SkillCast(_Character, "Shout_InspireStart", _, _) |
AND | AND | ||
DB_IsPlayer(_Character) | DB_IsPlayer(_Character) | ||
Line 134: | Line 138: | ||
# Save, then "Generate Definitions and Build". | # Save, then "Generate Definitions and Build". | ||
# In the main editor, select "File -> Reload Level and story". | # In the main editor, select "File -> Reload Level and story". | ||
− | # Hit the "Switch Game/Editor" button to get in-game. [[File:FirstStoryScript_GameEditorButton.png]] | + | # Hit the "Switch Game/Editor" button to get in-game. [[File:FirstStoryScript_GameEditorButton.png|thumb|Click this button to switch between game and editor mode.]] |
# In whatever level you're working in, your provided dummy character should spawn and be given the Encourage skill. | # In whatever level you're working in, your provided dummy character should spawn and be given the Encourage skill. | ||
# Cast the skill to test that your script is working properly. | # Cast the skill to test that your script is working properly. | ||
Line 147: | Line 151: | ||
* This event runs when the skill's animation reaches the CastTextEvent value specified by the skill stats (which is "cast" in most cases). | * 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. | * 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| | + | * To view the textkeys present in an animation, open it within the [[Content_browser|Content Browser]]. |
− | * Example: Open the | + | [[File:ContentBrowserButton.png|thumb|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. [[File:FirstStoryScript_SkillCast_Textkey.png|thumb|The cast textkey.]] | ||
'''DB_IsPlayer''' | '''DB_IsPlayer''' | ||
Line 215: | Line 220: | ||
IF | IF | ||
− | SkillCast(_Character, "Shout_InspireStart", _) | + | SkillCast(_Character, "Shout_InspireStart", _, _) |
AND | AND | ||
DB_IsPlayer(_Character) | DB_IsPlayer(_Character) | ||
Line 233: | Line 238: | ||
'''Databases''' | '''Databases''' | ||
− | * We create our "RandomText" settings in the INIT section by using a database we create, called "DB_WikiTutorial_FirstStory_Skills_RandomText". | + | * 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. |
'''[[Osiris/API/Random|Random]]''' | '''[[Osiris/API/Random|Random]]''' | ||
Line 246: | Line 251: | ||
<pre> | <pre> | ||
IF | IF | ||
− | SkillCast(_Character, "Shout_InspireStart", _) | + | SkillCast(_Character, "Shout_InspireStart", _, _) |
AND | AND | ||
DB_IsPlayer(_Character) | DB_IsPlayer(_Character) | ||
Line 254: | Line 259: | ||
IF | IF | ||
− | SkillCast(_Character, "Shout_InspireStart", _) | + | SkillCast(_Character, "Shout_InspireStart", _, _) |
AND | AND | ||
NOT DB_IsPlayer(_Character) | NOT DB_IsPlayer(_Character) | ||
Line 336: | Line 341: | ||
'''Removing Database Entries''' | '''Removing Database Entries''' | ||
− | * When | + | * 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. | * 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. | ||
Line 380: | Line 385: | ||
Practically speaking, using queries as subroutines is ideal when: | 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 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 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 == | == 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. | * 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. | ||
+ | ** More on implementing an update system here: [[Osiris_Tips#Updating_Databases|Manual Mod Updates]]. | ||
* By using custom procedures and queries, you can reduce your rule redundancy and extend desired calls/conditions to multiple rules. | * By using custom procedures and queries, you can reduce your rule redundancy and extend desired calls/conditions to multiple rules. | ||
Line 403: | Line 411: | ||
</pre> | </pre> | ||
Then, when you wish to use this database, you have a quick way to copy/paste that code into other sections of your script. | 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: | ||
+ | ; [[Osiris/API/GameStarted|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. | ||
+ | ; [[Osiris/API/RegionStarted|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). | ||
+ | ;[[Osiris/API/GameEventSet|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/Osiris/API/SavegameLoaded|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: | ||
+ | <pre> | ||
+ | MyMod_Internal_CompleteIfStarted(); | ||
+ | </pre> | ||
+ | |||
+ | KB: | ||
+ | <pre> | ||
+ | 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(); | ||
+ | </pre> | ||
[[Category:Osiris]] | [[Category:Osiris]] |
Latest revision as of 03:01, 27 August 2020
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".
Contents
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
- Open the Story editor by clicking the icon that looks like a book (
) under the "Editors" section, at the top-right of the Divinity Engine 2 editor.
- 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".
- 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.
- Inside the story editor, at the goal list on the left, right click "__Start" and select "Add New Sub Item...".
- 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.
- 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".
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:
- Save, then select "File -> Build and Reload".
- Minimize the story editor.
- Inside the message log, you should see the message we added with DebugBreak.
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.
- 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.
- Click "Add New Sub Item...", then name it something appropriate, like "WikiTutorial_FirstStory_Skills".
- 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.
- Save, then "Generate Definitions and Build".
- In the main editor, select "File -> Reload Level and story".
- Hit the "Switch Game/Editor" button to get in-game.
- In whatever level you're working in, your provided dummy character should spawn and be given the Encourage skill.
- Cast the skill to test that your script is working properly.
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.
- 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.
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);
- Save, then "File -> Generate Definitions, Build and Reload".
- Switch back to the main editor and hit the play button to get in-game.
- 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 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);
- Save, then "File -> Generate Definitions, Build and Reload". Since we added a query and another database, we must generate the definitions again.
- 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.
- More on implementing an update system here: Manual Mod Updates.
- 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();