Difference between revisions of "Your First Story Script"
m (Updated link.) |
m (Updated page link.) |
||
Line 13: | Line 13: | ||
== Recommended Reading == | == Recommended Reading == | ||
* [[Osiris Overview]] | * [[Osiris Overview]] | ||
− | |||
* [[Osiris Design Patterns]] | * [[Osiris Design Patterns]] | ||
* [[Osiris Gotchas]] | * [[Osiris Gotchas]] | ||
+ | * [[Osiris_Tips_and_Examples|Osiris Tips and Examples]] | ||
* [[:Category:Osiris_APIs|Osiris APIs]] | * [[:Category:Osiris_APIs|Osiris APIs]] | ||
Revision as of 12:15, 21 December 2017
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.
Contents
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 tips to keep in mind:
Starting Tips
- 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.
- 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.
Recommended Reading
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.
- Inside the story editor, at the goal list on the left, right click and select "Add New 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:
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;
- Save, then select "File -> Generate Definitions, Build and Reload".
- Minimize the story editor, then clear the message log by clicking "Clear".
- Load a level in the main editor. Inside the message log, you should see the message we added with DebugBreak.
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.
- 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 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.
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". Using databases in this way is a powerful way to design your script logic in a way that is 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.