Skip to content

NPC Dialog Field Guide

SmashleyMcNerdface edited this page Aug 7, 2020 · 6 revisions

NPC Dialog Field Guide

When building NPC/quest/reactor content for Hybrasyl, there is a simple structure that we have been using to keep things organized and readable. Always, always, always feel free to make comments in your code, though.

  1. Define text content.
  2. Define the OnSpawn Handler. (pursuits, sequences, dialogs, options, etc.)
  3. Define any other functions. (OnClick(), PassFail(), CompletionAward(), and other functions)
  4. Test, weep bitter tears, drink, fix, repeat.

In the examples posted below, I will first express a simplified version of the code with a concept breakdown, and then I will show how this is used in the current code in the "Faerie Love" lecture as a working sample.


Define Text Content.

By defining the text content in one place, it makes it easy to make quick changes to what is said without scrolling through code which can appear out of order. These are done in an array of character strings that are stored outside of the other functions. Remember to close those quotation marks, curly braces, and that commas are the delimiters.

Example:

array_name = { "blah", "bleh", "blih", "bloh", "bluh", "blsometimesyh" }

Sample:

faerie_love = {

"Hello, Aisling. If you feel the twinkling of love in your heart, perhaps you'll want to know of the faerie glade of lovers.",

"You are considering expressing romantic love. It is not the kind of marriage of the Churches of Mankind. It is an expression of love. It is \z

not an eternal bond, but is certainly a touching of the hearts. If your heart is sincere, then proceed.",

"If your heart is not sincere, then do not soil your good name with this folly. \z

You and your lover need not be of any particular gender, or of opposite gender. However this is a romantic ceremony. If you are not in \z

romantic love, then this is not for you.",

"Finally, you and your lover must have known the 11th insight, which is the insight of One and One together.",

"You, your lover, and a priest must go to the Lover's Glade. All of you must have experienced the 11th insight. Once there, perhaps you'll \z

feed your lover berries or sip on wine. Maybe read poetry to each other, or better yet, speak spontaneously from the heart...",

"Then the Priest will stand in the smaller patch of grass to begin the ceremony. You will be prompted to name your lover, and it will be \z

consummated. Others will see it upon your Legend.",

"Go to the north-eastern corner of the Enchanted Garden. You'll know the Enchanted Garden by the wealth of flowers there."

}

Please note that this string array uses \z a few times. This lets the program know that the dialog on the next line of code is meant to be a continuation of the previous line. I did this so I didn't have to keep using a scroll bar because who wants to do that constantly to check for grammar mistakes? In case you are building a table, or you like how left-adjusted text looks, typing \n will force a new line in an NPC dialog box.


Define the OnSpawn Handler.

First start the function by declaring it. To do this just throw down function OnSpawn() at the very end of this function you will need to write end in order to indicate that the NPC's mind is not completely resigned to the abyssal darkness of Chadul's realm. At least not completely. Also forgetting to end a function will throw the "expected end" error, so there's that.

Example:

function function_name(variable_input_name)

end

Sample:

function OnSpawn()

end

Because the OnSpawn function does not require any input parameters, there does not need to be anything in the parentheses.

OnSpawn is called when any scripted item is spawned (created) in Hybrasyl. For an NPC, this is usually when the server starts.


Pursuits

Make some space between the function call and the function end. This is where the rest of your pursuits are going to live.

I would suggest working on NPC scripts pursuit by pursuit and blocking these off before the mass of code makes your eyes cross. As Kedian has likely explained already, a pursuit is a main menu option. Beyond the main menu they are not called pursuits, they're just options that are tied to sequences. It is a little annoying at first, but you have to remember that all pursuits are sequences, but not all sequences are pursuits. Sequences are just the "choose your own adventure" paths which are accessed by the pursuit the player selects while in the game.

Pursuits and sequences are both started in the same way (because pursuits are sequences), but aside from the accessibility of "pursuit menu displays first" and "sequence options come later" and some self-inflicted naming convention stuff, pursuits are added differently than sequences at the end of each block of code.

Next, let's make a pursuit for the NPC to offer. For the pursuit sequence variable name use the same name you made for the text content, but throw the word lecture at the end. You will then need to add the pursuit. This should be located after we finish what we want the pursuit to access but before the end of the function. For now, let's just put it on the line before the end of the function.

Example:

pursuit_name_lecture = world.NewDialogSequence("What Button Says", {what button does})

associate.AddPursuit(pursuit_name_lecture)

Sample:

faerie_love_lecture = world.NewDialogSequence("On Faerie Love", world.NewJumpDialog("faerie_love_menu"))

associate.AddPursuit(faerie_love_lecture)


Sequences

Make some space between the start and end of the pursuit. This is where the sequences are going to live.

You do actually have to name the sequences twice. Once for registering the sequences (sequence variable name) and another for referencing the registered sequences (sequence callback name). This has been a pretty consistent headache before I started using a naming convention to keep all of these straight. Numbers 1-5 below describe how sequence variables are named, number 6 describes how the actual sequence (or sequence callback) name is given.

  1. Text content are called by what is on the pursuit button: pursuit_name

EX: faerie_love

  1. Sequences which are added as pursuits are called: pursuit_name_lecture

EX: faerie_love_lecture

  1. Sequences which are not used for menus are called: pursuit_name_meaningful_extension_dialog

EX: faerie_love_how_dialog

  1. Sequences which are used for menus are called: pursuit_name_menu_dialog/pursuit_name_meaningful_extension_menu_dialog

EX: faerie_love_menu_dialog

  1. The menu options will be called by the same name as 4, but replace the word dialog with the word options

EX: faerie_love_menu_options

  1. Sequences which are used as callbacks use the same names from 2, 3, or 4, without "dialog"/"lecture". sequence_name

EX: faerie_love_how

Like pursuits were added, sequences must be registered. Rather than registering each sequence immediately after where it is written, I found it cleaner to put them together immediately below where the sequences end and immediately above the pursuit was added. Sequence registering is only slightly different from pursuit adding, but you must do them or the NPC goes bananas.

Example:

associate.RegisterSequence(pursuit_name_meaningful_extension_menu_dialog)

associate.RegisterSequence(pursuit_name_meaningful_extension_2_dialog)

associate.RegisterSequence(pursuit_name_meaningful_extension_3_dialog)

associate.RegisterSequence(pursuit_name_meaningful_extension_4_dialog)

associate.AddPursuit(pursuit_name_lecture)

Sample:

associate.RegisterSequence(faerie_love_menu_dialog)

associate.RegisterSequence(faerie_love_what_dialog)

associate.RegisterSequence(faerie_love_how_dialog)

associate.RegisterSequence(faerie_love_where_dialog)

associate.AddPursuit(faerie_love_lecture)


Dialogs

There are several types of dialogs that sequences can use. Select the type of dialog that you require based on what you are trying to have the NPC "say" at that point in time. In the previous Example/Sample, we have already been introduced to NewDialogSequence and NewJumpDialog. In addition to these, I have been also using NewFunctionDialog extensively. Additionally, because quests often have experience given upon completion, Kedian created the CompletionAward function demonstrated in the example.

1. NewDialog

While most coding languages starts with 0 as the first position in an array, this one uses 1 as the first position in the array. This means that array_name[1] is "blah" and faerie_love[1] would refer to the first line ("Hello, Aisling. If you feel..."). Each of these strings sit in an array index position. When the array index ([1], [2], [3], etc.) is called, then the program will display that string as a single dialog frame.

Example:

pursuit_name_meaningful_extension_dialog = world.NewDialogSequence("pursuit_name_meaningful_extension",

world.NewDialog(pursuit_name[#]),

world.NewDialog(pursuit_name[#+1]),

world.NewJumpDialog("pursuit_name_menu_dialog", {function call})

Sample:

faerie_love_how_dialog = world.NewDialogSequence("faerie_love_how",

world.NewDialog(faerie_love[5]),

world.NewDialog(faerie_love[6]),

world.NewJumpDialog("faerie_love_menu", "invoker.CompletionAward('faerie_love_how', 500, 'She smiles.')"))

Based on the sample and the text content given earlier, we can check that we have referenced the correct array indexes for the dialog frames we want the player to see before moving to the next pre-programmed sequence via the JumpDialog:

...

"You, your lover, and a priest must go to the Lover's Glade. All of you must have experienced the 11th insight. Once there, perhaps you'll \z

feed your lover berries or sip on wine. Maybe read poetry to each other, or better yet, speak spontaneously from the heart...",

"Then the Priest will stand in the smaller patch of grass to begin the ceremony. You will be prompted to name your lover, and it will be \z

consummated. Others will see it upon your Legend.",

...


2. NewJumpDialog

JumpDialogs are how you can call the next sequence you want the player to see. Menu buttons are more often than not used to activate JumpDialogs, but you can also use them at the end of a dialog sequence in the case that you want different sequence options to merge back together (Vorlof Quest: kill the wolf/lure it away options both eventually merge back together), or in the case that you want players to have a particular dialog excerpt that they should see in case they came back later (Vorlof Quest: Did you get Boots/Shield?).

JumpDialogs use the sequence callback names to advance or move backwards through the sequences that are defined in the OnSpawn function. Place your JumpDialogs at the end of your sequences, or you navigate away from the rest of the sequence before it's read.

Example:

pursuit_name_meaningful_extension_1_menu_options.AddOption("ermergerd bertterns", world.NewJumpDialog("pursuit_name_meaningful_extension_2"))

Sample:

faerie_love_menu_options.AddOption("How may I express my love?", world.NewJumpDialog("faerie_love_how"))

Please note that pursuits do not have sequence callback names. Instead, you will only see the text that will be displayed on the pursuit button. In some cases you will want to call back to the dialog a pursuit originally stated. This is why I start almost every pursuit with a JumpDialog. It keeps this broken woman whole.

Example:

pursuit_name_lecture = world.NewDialogSequence("Button says whaaah?", world.NewJumpDialog("pursuit_name_meaningful_extension"))

Sample:

faerie_love_lecture = world.NewDialogSequence("On Faerie Love", world.NewJumpDialog("faerie_love_menu"))


3. NewFunctionDialog

This is where the magic happens, folks. Like, literally, you can tell NPCs to cast spells on players and all of that jazz if you wanted to do that. Functions tell NPCs to do things outside of saying words in the dialog frame, but you will need to write a function for them to carry out outside of the OnSpawn function. This is where prizes, experience, effects, and legend marks are born.

Because the "Faerie Love" lecture uses the CompletionAward() function, I will be using the "Woods" lecture as an example here and in the Function section later in the guide.

Example:

world.NewFunctionDialog("function_name()")

Sample:

world.NewFunctionDialog("woods()") )

You should note that Kedian found that the client is a little prissy about the order where you place this, and it prefers these to not happen immediately before JumpDialogs. In these cases just make the FunctionDialog. There is a way around this if you point the client to the sequence you want to open in the function.


Making Menus/Options

Up until this point sequences have been in any order you wanted them to be in so long as they were inside the OnSpawn function and (hopefully) before the pursuit was added. This changes here.

For menus which are placed within sequences, the options must come before the dialog sequence which will have the menu in it. In the code these buttons will be very similar to pursuits, but they do not require you to add or register them outside of the sequences that they reference or those by which they are referenced.

Example:

pursuit_name_menu_options = world.NewDialogOptions()

pursuit_name_menu_options.AddOption("Button Uno", world.NewJumpDialog("pursuit_name_meaningful_extension_1"))

pursuit_name_menu_options.AddOption("Button Dos", world.NewJumpDialog("pursuit_name_meaningful_extension_2"))

pursuit_name_menu_options.AddOption("Button Tres", world.NewJumpDialog("pursuit_name_meaningful_extension_3"))

pursuit_name_menu_dialog = world.NewDialogSequence("pursuit_name_menu",

world.NewOptionsDialog(pursuit_name[#], pursuit_name_menu_options))

Sample:

faerie_love_menu_options = world.NewDialogOptions()

faerie_love_menu_options.AddOption("What is faerie love?", world.NewJumpDialog("faerie_love_what"))

faerie_love_menu_options.AddOption("How may I express my love?", world.NewJumpDialog("faerie_love_how"))

faerie_love_menu_options.AddOption("Where may I find Lover's Glade?", world.NewJumpDialog("faerie_love_where"))

faerie_love_menu_dialog = world.NewDialogSequence("faerie_love_menu",

world.NewOptionsDialog(faerie_love[1], faerie_love_menu_options))

You are not limited to only JumpDialogs here. You can use functions in case you need to do player status checks such as whether or not they are a peasant, what class the character is, and level they are currently, etc.

Callbacks

Most (if not all) dialogs in Hybrasyl can define callbacks, which are Lua functions that are called when the dialog is displayed. You specify a callback in the dialog constructor (check the API documentation for Dialogs) or you can use SetCallbackHandler. Ordinarily you will set the handler to a function you've already written in a script.


Handlers and You

A script can define any number of different handlers, which will be used by Hybrasyl if defined. For instance: OnSpawn is one such handler, which is automatically run when a script is invoked (generally when an object is instantiated, or created, within the world).

Here is a list of handlers:

Handler Objects Uses & Global Variables Available
OnSpawn All Called when any scripted object is created in the world. For instance: an NPC runs this the first time it is clicked; a reactor would run this when it is placed initially on a map during the world data loading.
OnUse Items Called when a scripted item is used in the world. invoker is the person using the item; item will be set to the item object running the script.
OnDamage Monsters Called when a scripted monster is damaged. damage will have the integer amount of damage done to the monster; source will be an object representing the attacker.
OnHeal Monsters Called when a scripted monster is healed. heal will have the integer amount of healing done to the monster; source will be an object representing the healer.
OnEntry Reactor Called when a creature enters a reactor (a scripted tile). invoker will be the creature entering the reactor; source will be the reactor itself.
OnLeave Reactor Called when a creature leave a reactor (a scripted tile). invoker will be the creature entering the reactor; source will be the reactor itself.
AoiEntry Monsters, NPCs, Reactors Called when an object enters the "area of interest" of a creature. This means the object is visible (e.g. can be seen) by the creature.
AoiDeparture Monsters, NPCs, Reactors Called when an object leaves the "area of interest" of a creature. This means the object is no longer visible (e.g. cannot be seen) by the creature. Imagine something leaving a player' s viewport.
OnDrop Reactors Called when an item is dropped on a reactor (scripted tile). invoker will be the thing that dropped the item (almost always a player). source will be the reactor. item will be the dropped item.
OnTake Reactors Called when an item is picked up off a reactor (scripted tile). invoker will be the thing that dropped the item (almost always a player). source will be the reactor. item will be the picked up item.

Define the Other Functions.

Functions are awesome. You can throw in tons of flavor with infobar messages, giving and taking items, checking player statuses, and all of that great stuff, but they do take some writing and this is likely where the logic testing is going to give you a headache later. If you find that something is getting pretty elaborate in order to work here, then it really helps to stop, take a beat, make a flow chart, and then dive back into it.

Just like we opened and ended the OnSpawn function earlier, do that for your new function.

Example:

function function_name()

end

Sample:

function woods()

end

Create some space between where you opened the function and where you closed it. This is where your code lives.

Most likely, you will be using a bunch of "if"/"elseif"/"else" statements with some logical operators. The "if"/"elseif"/"else" statements work as advertised, though because this is a higher language than you may be used to, remember that you don't close off the "if" or "elseif" statements with an "end", but they will have the word "then" trailing behind the logical true/false test. If you do not want to have an elseif or an else, then you can use an end after "if". No worries.

Example:

if (1==0) then

invoker.SystemMessage("1==0 checks if 1 is equal to 0. This is false.")

invoker.EndDialog()

elseif (1!=1) then

invoker.SystemMessage("1!=1 checks if 1 is not equal to 1. This is also false")

invoker.EndDialog()

elseif (1>2) then

invoker.SystemMessage("1>2 checks if 1 is greater than 2. This is also also false")

invoker.EndDialog()

elseif (2<1) then

invoker.SystemMessage("2<1 checks if 2 is less than 1. This is also also also false")

invoker.EndDialog()

else

invoker.SystemMessage("This is what the player would see.")

end

Sample:

if invoker.HasCookie("woods") then

invoker.EndDialog()

else

invoker.SetCookie("woods", 1)

invoker.GiveExperience(2000)

invoker.EndDialog()

end

Invokers? Cookies? Explain yourself.

By convention, every script that runs in Hybrasyl has access to predefined global variables. These are primarily used to pass access to objects and information between C# and Lua.

invoker - Whatever object or thing caused the currently running script to fire. For a dialog, this is a player interacting with an NPC.
source - This is defined in a variety of scripts as the "origin" of an action. For instance, the source in OnDamage would be the creature or object causing the damage.
world - The hybrasyl world object. This exposes an API that can be used to interact with the world (for instance, creating dialogs, getting the time, etc).

Invokers are the players that are interacting with the NPC. These are the ones that initiate pursuits/sequences/dialogs, and not the ones that are invited in on the conversation later. Invokers have hidden statuses that track their progress with quests, when they last completed quests, last did tasks, etc. These are the cookies.

And I love me some cookies.

Want to signal that a player has completed a quest? Boolean cookie that shit. That's what happened in the woods lecture function above. You have a few functions that will interact with cookies. First, you have to set one.

Example:

invoker.SetCookie("cookie_name", {number or string of characters})

Sample:

invoker.SetCookie("woods", 1)

Want to count how many times the player has seen the woods lecture? That boolean cookie can be an integer cookie. No problem. You just have to modify it by setting it again. Easy. So, you have a cookie that counts. Neat trick, but what do you do with that? Enter GetCookie.

GetCookie reads what is stored by the cookie and you can use it from there. Keep people from doubling down on experience, limit the number of times people can do something in a day (time is checked by the "utility.GetUnixTime() function below), track their current quest partnerships, go nuts.

Example:

if invoker.GetCookie("cookie_name", {number or string of characters})

Sample:

invoker.SetCookie('honey_last_completed', utility.GetUnixTime())

An even quicker way to see if a player has already completed something beforehand is to use HasCookie. Because invokers don't automatically start with cookies, if one was set, they have it, and then the HasCookie check would return true. If they haven't gotten it yet, then they haven't done whatever it is, and then the HasCookie check would return false.

Example:

if invoker.HasCookie("cookie_name") then

Sample:

if invoker.HasCookie("woods") then

invoker.EndDialog()


OKAY, so that's all I have in my brain right now aside from the fact that markdown removes indentation. Absolutely feel free to check out the Devlin script in github for nice indentation and full script examples.

Clone this wiki locally