tinygiant98 / quest

5 stars 2 forks source link

Acknowledgments

Although primarily authored by tinygiant, this system has been influenced by many others. The base idea for the quest system's structure came from a similiar implementation (though in C#) by Zunath in his SWLOR server. Additionally, the style and accessor function content are directly influenced by the coding style of squattingmonk, in addition to using several of his helper utilities (see the next paragraph). Finally, this system was extensively tested by Corinne on the Legends of Aurica server. Much of the additional functionality beyond the 1.0 release is directly sourced from Corinne (such as random quests, minimum objectives and variables as prerequistes and [p]rewards). Other feature ideas were sourced from Tonden and Skarltar from the Dark Sun team. Many thanks to those that helped brainstorm, add features, review and test this system!

Note: The quest system files will not function without other utility includes from squattingmonk's sm-utils. These utilities can be sourced from this repo under the 'utils' folder. However, when this system reaches its final resting place, you might have to visit squattingmonk's nwn-core-framework or sm-utils repo to obtain these files. Specificially, the following files are required: util_i_color.nss, util_i_csvlists.nss, util_i_debug.nss, util_i_math.nss, util_i_string.nss

WARNING This documentation is still a work-in-progress. If anything in this documentation doesn't work the way you expect, refer to the code or find me on the Neverwinter Vault Discord...

Changelog

1.2.3

Table of Contents

Description:

This system is designed to allow builders/scripters to fully define quests within script without the need for game journal editing. The greatest use of this utility comes from pairing it with NWNX journal functions, which completely obviates the need for editing journal entries in the toolset. Since there are many modules that cannot or will not use NWNX, I've included functionality for interfacing with the game's journal system.

NOTE The base game currently contains a bug that prevents NWN journal entries from being persistently stored on the PC. In order to re-apply journal quests on the PC after login, run UpdateJournalQuestEntries(oPC).

NOTE NWNX functions have not yet been implemented due to some idosynchrasies in the code and how it interfaces with the game. When those wrinkles have been ironed out, NWNX functionality will be added. ETA is unknown, so all references to NWNX journal functionality below is future-growth.

NOTE This system makes extensive use of Quest IDs, which are defined and used internally. You never need to know a Quest's ID number to utilize this system. All QuestIDs are associate with a user-supplied Quest Tag (sQuestTag) which is provided when a quest is added. All user-facing function use this quest tag to identify the appropriate quest for modification.

AddQuest("myQuestTag");

Because each Quest Tag must be unique, the quest system can internally convert between QuestID and Quest Tag when required. If a user absolutely requires the conversion for other uses, two functions are provided:

string GetQuestTag(int nQuestID);  // will return the Quest Tag associated with nQuestID
int GetQuestID(string sQuestTag);  // will return the Quest ID associated with sQuestTag

WARNING All non-PC quest data is held in volatile memory and will be lost on server reset. Do not save QuestIDs persistently as they may change in the future with no ability to associate a changed ID with a Quest Tag. If you must save persistent quest data, identify it via the Quest Tag, not the QuestID. All quest data stored in the persistent PC sqlite database is identified with quest tags.

NWN Journal Entries: This utility can be used with either the standard NWN or NWNX journal functions. If you elect to use the standard NWN journal functions, you must build the quests within the game's journal editor and then enter the quest's properties into the build properties for each quest in the system. Examples of how to do this, as well as use NWNX journal functions, are included below.

Reserved Words and Characters:

Usage Notes:

This system makes extensive use of NWN's organic sqlite capability. All static quest data is held in volatile memory in the module object's sqlite database. All persistent quest data associated with individual player-characters are stored in the PC's persistent sqlite database, which is saved to the character's .bic file.

The text entries in this system can store colorized text, however, there are no functions included in this utility to accomplish colorized text. If you wish to have your journal titles or journal entries colored, the text must be pre-processed before storing the values on the quest or quest step. The utility script util_i_color has several functions to accomplish this.

This primary functionality of this utility resides in the ability to set various properties on quests and quest steps. These properties include quest prerequisites, step rewards, step prewards and step objectives. Most properties can be "stacked" (more than one added). Examples of this will follow.

Module-Level Quest-Associated Variables:

There are several functions that allow the user to associate Int and String variables with any quest. These variables are stored in the volatile module-associated sqlite database in a separate table and referenced to the associated quest by questID. These functions allow for a convenient place to store custom quest-associated variables and can be accessed by any module script as long as util_i_quest is included.

    GetQuestInt("myQuestTag", "myVariableName")
    SetQuestInt("myQuestTag", "myVariableName", myIntegerValue)
    DeleteQuestInt("myQuestTag", "myVariableName")

    GetQuestString("myQuestTag", "myVariableName")
    SetQuestString("myQuestTag", "myVariableName", "myStsringValue")
    DeleteQuestString("myQuestTag", "myVariableName")

PC-Level Quest-Associated Variables:

In addition to the module-level varibale function above, there is an optional variables table held in the PC's persistent sqlite database. To store a variable into the PC variable table, use the following functions.

NOTE For all [Get|Set|Delete]PCQuest* functions below, nStep is an optional parameter

    GetPCQuestInt(oPC, "myQuestTag", "myVarName", nStep);
    SetPCQuestInt(oPC, "myQuestTag", "myVarName", nValue, nStep);
    DeletePCQuestInt(oPC, "myQuestTag", "myVarName", nStep);

    GetPCQuestString(oPC, "myQuestTag", "myVarName", nStep);
    SetPCQuestString(oPC, "myQuestTag", "myVarName", sValue, nStep);
    DeletePCQuestString(oPC, "myQuestTag", "myVarName", nStep);

Quest-Level Properties:

Each quest contains the following properties. Not all properties are required.

Quest Prerequisites

Requirements a PC must meet before a quest can be assigned. You can add any number of prerequisites to each quest to narrow down which PCs can be assigned specific quests. All prequisites are checked when requested and the PC must pass all required checks before being assigned a quest. Party Member characteristics cannot be used to satisfy quest prerequisites.

Quest Step-Level Properties

Each quest step contains the following properties. Not all properties are required.

Definining Quests

Tactics, Techniques and Procedures (TTPs)

Definition Example:

Following is a complete usage example for creating a sequential three-step quest that:

Note In the scripts below, nStep values are assumed to be sequential, however, if the step id values in the NWN journal entries are not sequential, you can supply your own step ids. The only requirement is that the step ids increase as steps are added. Additionally, each quest MUST have an AddStepResolutionSuccess(). All other steps are optional. A quest with only a resolution step could be used to display quests in the journal that don't have completion steps, such as a module update note, or general module familiarity/instructions for the PC. This type of quest can be used, for example, in the start area for new PCs, giving them an instructional quest entry as well as a few starting items/gold/xp.

void DefineRogueQuest()
{
    // Create the quest and set the tag
    AddQuest("quest_rogue");

    // Set the quest prerequisites
    SetQuestPrerequisiteRace(RACIAL_TYPE_HALFLING);
    SetQuestPrerequisiteClass(CLASS_TYPE_ROGUE, 3);
    SetQuestTimeLimit(CreateTimeVector(0, 0, 0, 24, 0, 0));

    // Step 1 - Find Maps
    AddQuestStep();
    SetQuestObjectiveGather("map_rogue1");
    SetQuestObjectiveGather("map_rogue2");
    // Note these maps can be gathered in any order, but both maps must be found before
    // the step is complete

    // Step 2 - Break into the houses
    AddQuestStep();
    SetQuestStepObjectiveDiscover("trigger_house1");
    SetQuestStepObjectiveDiscover("trigger_house2");
    SetQuestStepObjectiveDiscover("trigger_house3");
    // Like Step 1 above, these triggers can be discovered in any order, but all three
    // must be discovered before the step is considered complete

    SetQuestStepPrewardItem("lockpicks_10", 3);
    // Since this item is a preward, it will be given to the PC as soon as this step
    // is reached.

    // Step 3 - Go tell the guild
    AddQuestStep();
    SetQuestStepObjectiveSpeak("guild_master");

    // Create a step for successful completion and rewards
    AddQuestResolutionSuccess();
    SetQuestStepRewardGold(1000);
    SetQuestStepRewardXP(500);
    SetQuestStepRewardAlignment(ALIGNMENT_CHAOS, 10);
    // If successfull, the PC will receive 1000gp, 500xp and an alignment change

    // Since there is a failure condition, create a step for quest failure
    AddQuestResolutionFail();
    SetQuestStepRewardGold(-500);
    SetQuestStepRewardXP(100);
    // If not successful, the PC will lost 500gp, but gain a small amount of XP
}

In order to make this quest work, an event of some type has to signal the quest system to check if the PC correctly accomplished the steps. In the Rogue Quest example above, it would be necessary to have checks for OnAcquireItem (for the maps), OnTriggerEnter (for breaking into the houses) and OnCreatureConversation (for speaking with the NPC).

This is accomplished by sending a signal to the to the quest system through the SignalQuestStepProgress() function. If you want to do any pre-processing before calling this function, the events are the correct scripts to do that in. For example, if you set a quest step objective to kill a creature, but you only want to count it if that creature is killed with a specific weapon or weapon type, you would check those custom prerequisites in your script before calling SignalQuestStepProgress().

This is a simple example of requesting a quest step advance from the quest system when a creature is killed:

// In the OnCreatureDeath script
void main()
{
    SignalQuestStepProgress(GetLastKiller(), GetTag(OBJECT_SELF), QUEST_OBJECTIVE_KILL);
}

// The first parameter should reference the PC.  The quest system will never trigger off of an
//   NPC or associate, but if one is passed, the system will attempt to identify its master PC
//   before continuing, so users can safely use internal NWN functions such as GetLastKiller().

// The second parameter identifies the triggering object, in this case the slain monster.  The data
//   that is passed here must match the data use when defining the quest, but it does not matter
//   if that was the object's tag, resref, or any other method of identification.

// The third parameter is the objective type.  In this case, a kill is signalled.  This is requried
//   because the quest system attempts to match as many quests as possible that reference the string
//   from the second parameter, but must be able to differentiate between speaking to a specified
//   object or killing the same object.

The quest system will then evaluate whether the PC has fulfilled the requirements to move forward in the quest. If so, the quest is advanced. If not, the kill is noted, but the quest does not advance. If the PC has fulfilled a failure condition, such as killing a protected creature, the quest will immediately fail and go to the Failure Resolution step, if it exists.

The system can also run scripts for each quest event type -> Accept, Advance, Complete and Fail. Before the script is run, up to three variables are stored on the module: the current quest tag, the current quest step and the current quest event. You can retrieve these by using GetCurrentQuest(), GetCurrentQuestStep() and GetCurrentQuestEvent(). GetCurrentQuestStep() is only available during the OnAdvance event and will return the step the PC is starting. These values will allow builder's to run quest-specific code. Additionally, OBJECT_SELF in all run scripts is the PC.

Here's a short example that creates a single goblin creature at waypoint "quest_test" when the PC reaches the first step of the quest with the tag "myFirstQuest".

void quest_OnAdvance()
{
    string sCurrentQuest = GetCurrentQuest();
    int nCurrentStep = GetCurrentQuestStep();

    if (sCurrentQuest == "myFirstQuest")
    {
        if (nCurrentStep == 1)
        {
            object oWP = GetWaypointByTag("quest_test");
            location lWP = GetLocation(oWP);
            object oTarget = CreateObject(OBJECT_TYPE_CREATURE, "nw_goblina", lWP);
        }
    }
}

Quest Step Partial Completion

To prevent having to create a new quest for every possible combination and permutation of PC characterstic and interaction, SetQuestStepObjectiveMinimum() allows builder to require the PC to complete less than the total number of objectives assigned to any specific step.

For example, if you have a common quest, but want the last SPEAK objective require that each different class type speak to a different NPC to complete their quest, you can assign all of the available NPCs to this step, but set the minimum completion requirement to 1 with SetQuestStepObjectiveMinimum(1), which allows the quest step to be completed when the PC speaks to only one of the total number of NPCs assigned to that step.

This property applies to any type of quest step objective. If you want to give the ability for the PC to advance the quest by either killing five ogres OR two goblins, you can set it up this way:

AddQuestStep();
SetQuestStepObjectiveKill("ogre_a", 5);
SetQuestStepObjectiveKill("goblin_a", 2);
SetQuestStepObjectiveMinimum(1);

The PC will complete the step when either one of the two objectives are complete. There is no limit to the number of objectives you can assign to a single step

Partial Randomization

Many modules use small, "throw-away" repeatable quests that allows PCs to gain some experience at lower levels. To prevent having to create many different quests for this capability, builders can use SetQuestStepObjectiveRandom() to select a specified number of step objectives that the PC has to complete to advance the quest. For example in the snippet below, a quest step is created that selects two of the possible six objectives and assigns them to the PC. Additionally, combining this capability with partial completion above, the PC is only required to complete one of the two assigned objectives.

AddQuestStep();
SetQuestStepObjectiveKill("ogre_a", 5);
SetQuestStepObjectiveKill("goblin_a", 10);
SetQuestStepObjectiveKill("nw_bat", 15);
SetQuestStepObjectiveKill("nw_rat", 15);
SetQuestStepObjectiveKill("rabid_cow", 3);
SetQuestStepObjectiveKill("death_rabbit", 2);
SetQuestStepObjectiveRandom(2);
SetQuestStepObjectiveMinimum(1);

When partial randomization is used, a custom preward message is required if you want to give the PC feedback on what needs to be accomplished. Because the objective string has to be dynamically built, it can't be inserted into the game's journal system, so SetQuestJournalHandler() should be set to QUEST_JOURNAL_NONE for semi-random quests. Additionally, you must provide a message preward to start the custom message with, as well as objective descriptors and descriptions to be included in the custom message. Here's an example of how to modify the above code to make that happen, as well as an example custom message that will be generated:

AddQuest();
SetQuestJournalHandler(QUEST_JOURNAL_NONE);

AddQuestStep();
SetQuestStepPrewardMessage("Welcome!  We have quite the adventure waiting for you!");

SetQuestStepObjectiveKill("ogre_a", 5);
SetQuestStepObjectiveDescriptor("ogre");

SetQuestStepObjectiveKill("goblin_a", 10);
SetQuestStepObjectiveDescriptor("goblin");

SetQuestStepObjectiveKill("nw_vampire", 15);
SetQuestStepObjectiveDescriptor("vampire");
SetQuestStepObjectiveDescription("with a wooden stake");

SetQuestStepObjectiveKill("nw_troll", 15);
SetQuestStepObjectiveDescriptor("troll");
SetQuestStepObjectiveDescription("and burn their bodies");

SetQuestStepObjectiveKill("rabid_cow", 3);
SetQuestStepObjectiveDescriptor("rabid cow");

SetQuestStepObjectiveKill("death_rabbit", 2);
SetQuestStepObjectiveDescriptor("cute furry rabbit");

SetQuestStepObjectiveRandom(3);
SetQuestStepObjectiveMinimum(1);

AddQuestResolutonSuccess();
SetQuestStepRewardXP(500);
SetQuestStepRewardGold(250);

The randomization process will select three of the six objectives, create a custom message, then assign the three objectives to the PC and display the custom message. In the above snippet, if the system selectes the first (ogre), third (vampire) and sixth (rabbit) objectives, the created message will look like this:

Welcome!  We have quite the adventure waiting for you!  You must complete 1 of
the following 3 objectives:
  KILL 2 ogres
  KILL 3 vampires with a wooden stake
  KILL 2 cute furry rabbits

NOTE Because the objective selection is random, the objectives may not be selected in the order in which they were created. Additionally, the system pluralizes the descriptor if the quantity is greater than one, but it does so only by adding an 's' as it is outside the scope of this system to be a grammar checker.

If the number of randomly selected objectives as defined in SetQuestStepObjectiveRandom() is the same as the minimum required as defined in SetQuestStepObjectiveMinimum(), the message will replace 'You must complete x of the following y objectives' with 'You must complete the following objective[s]'.

The custom message is saved to the PC's sqlite database when the PC starts the step, so the message is persistent across resets/logouts. If you need to access the custom message for this quest, you can obtain it with GetRandomQuestCustomMessage(oPC, sQuestTag); and the customized message will be returned, if it exists.