In order to reduce the need to create scripts to support dialogs, especially for quests, a very simple new scripting language has been added to support most of the common function used inside the dialog line Appears When script. The language support most of the common predicates and actions require to create a working quest and also provides and interface to PMOTE script commands to enable creature animations and other object manipulation in the same area.
Usually, it's possible to modify the behaviour of NWN scripts by passing them information via local variables, but for multi-line dialogues it quickly becomes unmanageable. DQL solves this by storing the extra information in the dialogue line itself. It then removes the information so that the player only sees the dialogue part and not the DQL information intended for the script. To add DQL to a dialogue line just add quest_aw as the Appears When script and enter the commands at the end of the dialogue as follows. These lines could, for example, be added to a door conversation.
The “|IF” part tells quest_aw to treat the result as a predicate. The dialogue “Only the Bishop” will only show for PCs that have made progress in the specified thief guild quest or pass the lore skill check. If they click on that line, they will see “[The panel closes and the door opens.]” and the door will unlock and open for a minute. Other PC's will not be able to open the door using the conversation.
So how do quests actually work? Typically quests are an example of a finiate state machine. It is often possible to track the state of the quest by looking for items in the player's inventory but this can be difficult as you need to create lots of items. It can also look awkward and contrived if you are being passed notes all the time. A more general solution is to store the state of the quest in a local integer variable stored either on:
Each of the above has its own merit and you may well decide to use them all in different circumstances.
Storting the state on the PC is the easiest and cleanest option but keep in mind that the PC object is reset when the server resets. This makes it a poor choice if the quest is very long or the result of the quest is important to NPCs or other quests. Sinfar is often up for many days or even weeks, so this is usually a good choice if the result of the quest is not important.
In many ways, this is the best solution of all. It will survive server resets and does not clutter up the PC object. The major downside is that PC has to carry around an item with no other purpose and if every builder decides to add their own it could become annoying. Obviously, you also need to dump the item on the PC somehow and ideally tell them what it does OOCly so they can use to manage their character quest history. There is already a system in place to address these issues, the quest_census dialogue can be attached to an NPC of your choice or you can just spawn the quest_census NPC. The dialogue will ask a few background questions and ask about SRP lights then place the quest_census item in the PC's inventory. This item can then be used for all your quest status information tracking needs. The main downside is that the PC can't do any quests without a census form, so you will need to trap for this every time you offer a quest.
This is the official NWN mechanism for tracking quest progress. The journal has the advantage that it is properly integrated into the game and allows PC's to see what quests are open and those that are complete. Each journal entry has a space for the quest name, instructions for the player on what to do next and a private state variable that can be used to track the progress made. Sinfar has its own system for storing journal information including persistence past server resets using a database. The only major downside is the PC can end up with hundreds of pointless journal entries. Builders can help by removing journal entries when a quest or quest chain is complete. If you want to avoid journal clutter but still want to track quest history, then consider using both Journal and the quest_census item. Remove the journal entry but leave the quest_census local variable. DQL will actually cache the journal state in the quest_census if found, or the PC is not, so using the quest_census to store quests results is seamless. Include an NPC in the city, perhaps the library, who can clean up all the quests for your island.
Let us design a quest given by a collector, to the PC, to acquire a silver Statue from another collector. The quest has a few paths:
State 0 = Not started: When the PC talks to the collecter A, offer them the quest and if they accept, move to 1 and tell them to obtain the statue from collecter B. If the PC is in the thieves guild and passes a lore and INT check, also suggest that they could obtain a fake statue from guild forger. 1 = Looking for the Statue: Collector A will accept the statue and pay a bonus if it is not stolen, move to state 10 and close the quest. Guild forger will give the PC a fake for some gold, move to state 2, the journal should update to reflect the new objective. Collector B will sell the statue if the PC makes a hard persuasion/intimidate check, move to state 2 and update objective. Collector B will offer to exchange the statue in return for a painting owned by Collector A, move to state 3 and update the objective 2 = Statue Obtained: Collector A will accept the statue and pay a bonus if it is not stolen, move to state 10 and close the quest. Collector A will accept the fake statue and pay for it, move to state 11 and close the quest. 3 = Looking for Painting: Collector A will accept the statue and pay a bonus if it is not stolen, move to state 10 and close the quest. Collector A will sell the painting to the PC if they do not have it and can make hard persuasion check, update objective. Collector B will give the statue to the PC if they have the painting, move to state 2 and update the journal. 10 = Quest Completed: 11 = Quest Completed with fake: There may be consequences for the PC.
Potentially the PC is free to steal the statue in any state but it is good practice to only spawn it when the quest is running, same for the painting. You could also add other requirements such as checking that Collector B is still alive if Collector A is squeamish. Ideally, a quest should offer lots of alternative solutions and alternative paths for completion using different skills/abilities and also allow them alternate moral choices.
The quest_aw script searches for the following three special character sequences. |IF - Use the result from the DQL expression to control if the text appears. |DO - Evaluate the DQL expression and ignore its result, used for actions. |PMOTE - Pass the following text to the pmote script with Self as Subject and the PC Speaker as the partner.
DQL itself has a very simple syntax consisting of a option object reference with a dot and a function name followed by the function arguments inside a pair of parenthesis, optionally followed by an operator and more DQL. For example, if the PC if paying for something we would need to take something, then give something back.
Usually it is not necessary to include the “PC.” part as the PC Speaker is selected by default. The AND operator is very important as DQL evaluates left hand side first and only evaluates the right hand side if the left and side if true. This will stop the merchant from handing over the hammer if the player does not have the gold.
DQL supports the following operators
DQL always returns a string, in a Boolean context it will return TRUE or FALSE, in a numeric context it will return a string holding a decimal number. The operators try their best to figure things out but its not a good idea to mix and match.
By default functions are passed PC speaker as object reference, so there is no need to put “PC.” in front of every function.
Anything that is not one of the above is assumed to be Tag reference. If the Tag begins with “I_”, then it is assumed to be an object in the PC's inventory. For other Tags it first searches for the nearest, failing that it looks globally.
Constants can be used in the place of functions with some limitations. DQL will accept TRUE and FALSE for Boolean values, decimals or floats when numbers are required or strings. Actually everything is a string really, the function should document what it needs. No need for string quotes in function arguments, in fact currently, string quotes will mess things up when used in a function argument. Regex recursion is needed to fix all of this. With the current limitations on string parsing its best to avoid “)” and “,” in your strings. Journal entries have a work around that lets it accept “,”.
While string constants are currently a horrible mess, its not actually much of a problem in practice. Most functions are expecting either a word or a object Tag, seldom free text.
The following attributes are supported and are matched case intensive.
Returns an integer value except for ChallengeRating which is a float.
Same as Get but will return the unmodified base values.
Not the same as Get or even close. Supported attributes
Returns true if successful.
The following attributes are supported and are matched case intensive.
Returns TRUE or FALSE as expected.
Returns true if the creature has enough points in the relevant skill or ability (CHA, CON etc) If a OpposingCreatureTag is supplied then the check is opposed and the creature must beat the opponent score + the value supplied. This function also adds a note to the dialog line to indicate that the relevant Skill or Ability was used.
Same as Check except that a d20 role is added, also for the opponent if opposed.
Returns true if the item was found on the creature.
Returns TRUE unless the item could not be created and placed in the creature's inventory.
Returns TRUE if NumberOfItems was removed. It will always try to remove as many of the items as is specified. In theory, you should always have a dialogue line that counts the number of items before they are taken, but players can cheat so you should also check that Take() is successful before passing on a reward. Unfortunately, this will result in removing any items found without compensation but that is a fair reward for cheating I think?
Same as Take but is added to the action queue to be done later.
Like, Take it searches both the equipment slots and the inventory, no place to hide!
Adds an action to equip an inventory item to the action queue. Atm, Slot must be a number, may support names later on.
Same as AddJournalEntry()
If State is negative it will be ignored and only the text will be updated. Will also make make the complete status false. As with AddJournalEntry() you may include commas.
Same as UpdateJournalEntry()
Same as UpdateJournalEntry() except that the complete status is changed to True.
Same as CompeteJournalEntry()
Returns the current State of the quest in the journal. The value is cached on the PC so you may use without worrying about performance issues.
Same as GetJournalState(PlotId)
Removes the quest from the journal, this will allow the PC to repeat the quest. It is also a good helpful to keep journals uncluttered unless you really need to keep the entry.
Same as RemoveJournalEntry()
Most of these functions have been sorted into other sections.
Add an action to wait this many seconds, can be a float.
Makes the NPC face in the direction of the nearest peaceable. Used for aligning them with chairs and tables.
Returns TRUE unless a the creature was a PC, the creature will leave the area.
Sadly the default behavior for this is terrible. You almost always want to target SELF and select PC as the person to talk to.
Same as ActionTalkTo() but the default object is SELF and it automatically selects PC as the interlocutor. It does actually accept the same arguments but defaults work in the most common case. The main use for this function is to provide a work around for when you need to repeat dialog entries. For DQL to work properly it has to remove itself from the dialog. Consequently if you return to a DQL dialog line it will no longer have the DQL instructions. The work around is to restart the conversation. Its not ideal but seems work well enough. Just add “, TalkTo()” end of the DQL and the NPC will start the conversation again.
Returns TRUE if the object was created.
PMOTE may be a better option.
Passes everything though to PlayAnimation
Same as PlayAnimation() but added to the action queue for later.
Random number generator that simulates a dice throw.
Can be spawned safely anywhere for testing purposes. I have linked into thier conversations.