Game Development Community

MMORPG Tutorial Article 9 Dreamers Theory on Quests

by Dreamer · 06/26/2005 (10:35 am) · 18 comments

After several weeks of coding some very complex Quest systems, I finally had the realization that there are in fact only 4 types of Quests, and we can extend that even further and say that combat aside there are only 4 types of NPC interaction.

These types describe exactly the interaction between the Player and the NPC, here is how they work

ItemForItem (I will trade you a recipie book for some berries, or 10 Coins for a Sword etc)
ItemForFlag (Talk to Zippy to get a Sword)
FlagForItem (Bring me the Head of a Bandit to prove your loyalty)
FlagForFlag (Thanks for the info, now go talk to Zippy)

When I began designing the questing system I was using the now ubiqutious RPGDialog system, but after awhile I realized it had 2 major flaws.

#1 It was too difficult to describe the system in a method that made it friendly to the database. This means that while we do have a nice GUI to design the question answer system with, we are stuck with plain text files which give rise to problem #2

#2 The client always has complete access to the questions and answers that any particular NPC might have, thereby giving those who are so inclined an unfair edge.

Those flaws aside, RPGDialog is a really nice resource for NPC dialog and interactions, with a beautiful GUI, so you could actually take that resource as-is and build a really nice quest system around it if you so choose.

If you want to use my version then follow along.

Whenever I design something thats going to interact with the database I always start with the database, alot of people would probably say that this is backwards, but databases are where I first learned serious programming, so it's where I'm most comfortable.

Another reason for starting with the database on this resource is to show how my system can direct the flow of a quest.

Lets start with table creation.
%query[9] = "CREATE TABLE Quests (QuestGiver VARCHAR(20),QuestType VARCHAR(10),QuestName VARCHAR(50),PreReq VARCHAR(255),Request VARCHAR(255),Response VARCHAR(255),Reward VARCHAR(50),Fullfillment VARCHAR(255))";
		
%query[10] = "CREATE TABLE QuestActions (QuestName VARCHAR(50),ActionList VARCHAR(255))";

In the code above we have created 2 tables the first table "Quests" contains ALL of the information we want or need for the Quest (Note: Anywhere I'm talking about quests I am also speaking of NPC Dialogs).

Lets go over the fields
QuestGiver (Is the NPC who issued the quest, notice there is not any place for the NPC who ends the quest, thats because it's completely irrelevant)

QuestType (is one of the 4 types we spoke of earlier, ItemForItem, ItemForFlag, FlagForItem, FlagForFlag)

QuestName (This is the unique name we give to each Quest, at one point I also added a Stage field after this, but subsequently decided to remove it, due to the default method that Torque uses to handle arrays, rendering QuestName[1] and QuestName1 to be the same thing).

PreReq (For this resource I haven't bothered to implement it, but what the field was intended for is to make sure certain conditions were met before allowing the quest to be given, most especially this could be used to check for a faction flag, so Good NPCs don't give Evil Players quests and things to do, however there are a myriad of other potential uses.)

Request (This is quite literally what the NPC says, I have included 2 flags from RPGDialog <> and <> these will automatically resolve to the NPCs name and the Players name)

Response (These are the possible answers, they are seperated with a \t )

Reward (This is what is given upon the QuestFlag for this quest being set to complete, and is one part of what determines QuestType)

Fullfillment (This is how we tell if the QuestFlag should be set to "COMPLETE" or not, and is the other half of what determines QuestType)

The second table is called QuestActions, and is literally a list of what actions need to be taken for each response.
The fields are as follows
QuestName (This needs to be exactly the same as the quest you have in the above table i.e. Letter_From_Kronk1)
ActionList (This is a tab seperate list of functions with a space seprated list of variables, that determine what actions must be taken for each response)

Ok thats the really long explanation, and was a whole lot to take in, so lets just see some examples to make it clearer, please note that I have intentionally broken the last Quest, and will leave it as a learning exercise to you to fix (It took me nearly a week, but the solution was REALLY simple, if I had seen it earlier would have taken only a few seconds).

function InitQuests(){
  //"CREATE TABLE Quests (QuestGiver VARCHAR(20),QuestType VARCHAR(10),QuestName VARCHAR(50)PreReq VARCHAR(255),Request VARCHAR(255),Response VARCHAE(255)Reward VARCHAR(50),Fullfillment VARCHAR(255))";
	%query[0] = "INSERT INTO Quests VALUES ('Guard Kronk','ItemForItem','Kronk_needs_Berries','','Hello <<PlayerName>> I need some berries do you have some?','<AnswerStart><a:RPGDialog 1>Yes</a><a:RPGDialog 2>No</a>','Recipie_Book 1','Berries 5')";
	%query[1] = "INSERT INTO Quests VALUES ('Guard Kronk','ItemForItem','Bandit_Heads','Kronk_needs_berries COMPLETE','Hello <<PlayerName>> I need you to kill some bandits and bring me thier heads one will suffice for now','<AnswerStart><a:RPGDialog 1>Yes I will</a><a:RPGDialog 2>Not right now</a><a:RPGDialog 3>I already have some</a>','Uber_Sword 1','Bandit_Head 1')";
	%query[2] = "INSERT INTO Quests VALUES ('Guard Kronk','ItemForFlag','Letter_from_Kronk1','Bandit_Heads COMPLETE','Hello <<PlayerName>> Would you please deliver this letter to Guard Stranglethorn?','<AnswerStart><a:RPGDialog 1>Yes</a><a:RPGDialog 2>No</a>','Letter 1','Letter_from_Kronk1 ACCEPT')";
	%query[3] = "INSERT INTO Quests VALUES ('Guard Stranglethorn','FlagForFlag','Letter_from_Kronk2','Letter_from_Kronk1 ACCEPT','Hello <<PlayerName>> do you have something for me?','<AnswerStart><a:RPGDialog 1>Yes</a><a:RPGDialog 2>No</a>','Letter_from_Kronk1 COMPLETE','Letter 1')";
	%query[4] = "INSERT INTO Quests VALUES ('Guard Stranglethorn','FlagForFlag','Letter_from_Kronk3','Letter_from_Kronk1 COMPLETE\tLetter_from_Kronk2 COMPLETE','Oh dear <<PlayerName>> this is terrible news, will you please return this letter to Kronk Im sure he will pay you handsomely','<AnswerStart><a:RPGDialog 1>Yes</a><a:RPGDialog 2>No</a>','','Letter_from_Kronk2 COMPLETE')";
	%query[5] = "INSERT INTO Quests VALUES ('Guard Kronk','FlagForItem','Letter_from_Kronk4','Letter_from_Kronk3 ACCEPT\t','Oh dear <<PlayerName>> this is terrible news, however its none of your concern, here is some coin for your time','<AnswerStart><a:RPGDialog 1>Thank You</a>','Coin 10','Letter_from_Kronk3 ACCEPT')";
	for(%x = 0; %x <= 5; %x++){
		echo(%query[%x]);
		%result = $SqLite.query(%query[%x],0);
		$SqLite.clearResult(%result);
	}
}

Lets go through the first line, each comma int the text corresponds to 1 field in the function above.
It says in effect...

Create a new Quest Given by Kronk, we will trade items for items, Call it Kronk_needs_Berries, Kronk should say something, Give the player 2 response options, either Yes or No. If they complete the Quest give them 1 Recipie_Book, they will need 5 Berries to complete the quest.

Next lets look at the QuestActions
function InitActionList(){
	%query[0] ="INSERT INTO QuestActions VALUES ('Kronk_needs_Berries', 'Quest \"COMPLETE\"\tQuest \"REFUSE\"')";
	%query[1] ="INSERT INTO QuestActions VALUES ('Bandit_Heads', 'Quest \"ACCEPT\"\tQuest \"REFUSE\"\tQuest \"COMPLETE\"')";
	%query[2] ="INSERT INTO QuestActions VALUES ('Letter_from_Kronk1','Quest \"ACCEPT\"\tQuest \"REFUSE\"')";
	%query[3] ="INSERT INTO QuestActions VALUES ('Letter_from_Kronk2','Quest \"COMPLETE\"\tQuest \"REFUSE\"')";
	%query[4] ="INSERT INTO QuestActions VALUES ('Letter_from_Kronk3','Quest \"ACCEPT\"\tQuest \"REFUSE\"')";
	%query[5] ="INSERT INTO QuestActions VALUES ('Letter_from_Kronk4','Quest \"COMPLETE\"')";
	for(%x = 0; %x <= 6; %x++){
		echo(%query[%x]);
		%result = $SqLite.query(%query[%x],0);
		$SqLite.clearResult(%result);
	}
}

This one is really simple, but in the spirit of verbosity lets analyze this as well.
In the first query
This is the Actions for the quest named Kronk_needs_Berries, If the first option is chosen execute the Quest Function and pass it in the "COMPLETE" flag otherwise if the second option is chosen execute the Quest function and pass it the "REFUSE" flag.

Alrighty, thats all the database stuff, the next thing we want to do is to create the NPCs that we will actually be interacting with. Here is the function for that.

function MakeQuestGiver(%name){
   %aiPlayer = AIPlayer::Spawn("Guard "@%name,pickspawnpoint(),"aiOrc");
   %aiPlayer.setInventory(Uber_Sword,1);
   %aiPlayer.mountImage(UberSwordImage,0);
   %aiPlayer.isTargetable = 1;
   %aiPlayer.isQuestGiver = 1;
   //%aiPlayer.setMaxDamage(10000);

    // Player setup
   %aiPlayer.faction = "GOOD";
   %aiPlayer.setMoveSpeed(8);
   %aiPlayer.setEnergyLevel(600);
   //%aiPlayer.Wander(60);
}

You may need to either create an Uber_Sword using the previous tutorial (I created one that caused 100,000 damage, so bored players wouldn't be tempted to kill the important NPCs), or just give him the Sword from MMORPG 4.
You may also need to adjust that AIPlayer::Spawn function to accept a bodytype argument, or just call whatever function you would use to spawn an AIPlayer (There's only about 100 different ways to do this).

Notice we have 2 new flags, .isQuestGiver and .faction, it would be really simple to use these flags to help us control interaction. Specifically, .faction can be used to determine whether or not a player is friend or foe, and .isQuestGiver can be used to indicate to the engine that we need to talk to the player. We will get into this in a few minutes but I wanted to make you aware of thier presence since flags are such an important part of the rest of this tutorial, and will factor in heavily into MMORPG Advanced.

Next we need to Actually call the function somewhere, I've chosen to use game.cs in the startgame() function
MakeQuestGiver("Kronk");
MakeQuestGiver("Stranglethorn");

Ok now lets replace our worn out old targeting function with a nice shiny new one.
//Sets a target for the player
function serverCmdTarget(%client, %mouseVec, %cameraPoint,%TargType){
	//Determine how far should the picking ray extend into the world?
	%selectRange = 200;
	%pi = 3.14159;
	// scale mouseVec to the range the player is able to select with mouse
	%mouseScaled = VectorScale(%mouseVec, %selectRange);
	
	// cameraPoint = the world position of the camera
	// rangeEnd = camera point + length of selectable range
	%rangeEnd = VectorAdd(%cameraPoint, %mouseScaled);
	%TargType = detag(%TargType);
	if(%TargType $= "Player"){
		%searchMasks = $TypeMasks::PlayerObjectType;
	}
	%searchMasks = -1;
	// Search for objects within the range that fit the masks above. If we are
	// in first person mode, we make sure player is not selectable by setting
	// fourth parameter (exempt  from collisions) when calling ContainerRayCast
	%player = %client.player;

	if ($firstPerson){
		%scanTarg = ContainerRayCast (%cameraPoint, %rangeEnd, %searchMasks, %player);
	}else{
	//3rd person - player is selectable in this case
		%scanTarg = ContainerRayCast (%cameraPoint, %rangeEnd, %searchMasks);
	}

	//Fail safe so we get SOME kind of target
	if(%scanTarg $= ""){
		InitContainerRadiusSearch(%cameraPoint, %selectRange / %pi, %searchMasks);
		%scanTarg = containerSearchNext();
	}
	
	// a target in range was found so select it
	if (%scanTarg){
		%targetObject = firstWord(%scanTarg);
		%client.setSelectedObject(%targetObject);
		if(%targetObject.isTargetable == 1){
			CommandToClient(%client,'UpdateTargetDialog','NewTarget',%targetObject.getshapeName());
			if(%targetObject.isTrader == 1){
				CommandToClient(%client,'MultiGUI',%targetObject.getName(),%targetObject.useSkill);
			}
			if(%targetObject.faction $= %player.faction){
				if(%targetObject.isQuestGiver == 1){
					RPGDialogMessageClient(%client); //start dialog.
				}
			}else{
				%targetObject.setAimObject(%client.player);
				ServerCmdAutoAttack(%targetObject);
			}
		}
	echo("Client has selected" SPC %scanTarg);
	//%targetObject.dump();
	}else{
		if(%client.getSelectedObject()){
			%client.clearSelectedObject();
			echo("Client has cleared selection");
			CommandToClient(%client,'UpdateTargetDialog','ClearTarget');
		}
	}
}

The major differences between this one and the one used in MMORPG 8 is the 2 new flags, and the fact that if you target an unfriendly NPC they will now happily beat you to death :)

Notice the RPGDialogMessageClient function above? It's primary function is simply to take Quest information and send it to the client.

function RPGDialogMessageClient(%client)
{
   GetQuest(%client);
   %senderID = %client.getSelectedObject();
   %senderName = %senderID.getShapeName();
   %senderID.RPGDialogBusy=true;
   %senderID.RPGDialogTalkingTo=%client;
   %senderID.setAimObject(%client.player);
   %senderID.setMoveDestination(%senderID.getTransform());
   echo("\n\n\nMessage: "@%client.Player.CurrentMessage);
   echo("\n\n\nMessage: "@%client.Player.CurrentResponse);
   CommandToClient(%client,'RPGDialogMessage',%client.Player.CurrentMessage,%client.Player.CurrentResponse);
   CheckRPGDialogStatus(%client,%senderID);
}

The code above uses another function called GetQuest and here it is.
function GetQuest(%client){
	%targetID = %client.getSelectedObject();
	%targetID.setAimObject(%client.player);
	%QuestGiver = %targetID.getShapeName();
	%query = "SELECT * FROM Quests where QuestGiver = '"@%QuestGiver@"'";
	//echo(%query);
	%result = $SqLite.query(%query,0);
	if(%result){
		while(!$SQLite.EOF(%result)){
			%QuestName = $SqLite.getColumn(%result,"QuestName");
			echo("QuestName is "@%QuestName);
			if(%client.Player.QuestFlags[%QuestName] !$="COMPLETE"){
				%client.Player.CurrentMessage = $SqLite.getColumn(%result,"Request");
				%client.Player.CurrentResponse = $SqLite.getColumn(%result,"Response");
				$SqLite.clearResult(%result);
				%client.Player.CurrentMessage = strreplace(%client.Player.CurrentMessage,"<<Name>>",%QuestGiver);
			        %client.Player.CurrentMessage = strreplace(%client.Player.CurrentMessage,"<<PlayerName>>",%client.Player.getShapeName());
				%client.Player.CurrentQuests[%QuestGiver] = %QuestName; //Store the quest information for later retrieval
				echo(%client.Player.CurrentMessage);
				echo(%client.Player.CurrentResponse);
				return;
			}else{
				$SqLite.NextRow(%result);
			}
		}
		$SqLite.clearResult(%result);
	}
}

Here is whats going on, in the function above.
First we find the ID of the clients selected target
Next we tell that ID to setAimObject to the player (this causes him/her to turn towards the player)
Next we get the name of the Players current target
Then we Select all the quests from the database where the QuestGiver has the same name as the selected object
Now we loop until we reach the end of the result set (or we get what we need)
The loop just compares the current QuestName to the flag stored at QuestFlags[%QuestName], if it exists, as long as the flag is not COMPLETE then we parse that row.
We stick the request field in CurrentMessage and the response field in CurrentResponse, then clear the result set.
Next we check the Message and replace <> and <> with the proper values.
Finally we store the QuestName at CurrentQuests[%QuestGiver] and exit the function.
If the QuestFlags was already COMPLETE then we get the next row.

The next thing we want to do is display the Quest to the client, this is achieved through the CommandToClient function in RPGDialogMessageClient.

That command uses a whole series of smaller functions, but it is the only client side code we need (other than the dialogprofiles, and GUI, that I will share later)
function clientCmdRPGDialogMessage(%Question,%Response){
    //%Message = detag(%Message);
    echo("We got a message and it's "@%Message);
    if(%Question!$="")
    {
       
       %QuestionAnswer = %Question @ %Response;
       if(%QuestionAnswer!$="<InvalidQuestion>")
       {
          %AnswerStart=strPos(%QuestionAnswer,"<AnswerStart>");
          %question=getSubStr(%QuestionAnswer,0,%AnswerStart);
          %answer=getSubStr(%QuestionAnswer,%AnswerStart+13,strLen(%QuestionAnswer));
       }
       else
       {
       %question="ERROR::Invalid Question!!";
       }
    }
    
    if (%question!$="")
    {

       if ((%soundStart = playRPGDialogSound(%question)) != -1)
          %question = getSubStr(%question, 0, %soundStart);

       RPGDialogQuestion.settext(%question);
       ChatHud.addLine($Pref::RPGDialog::ChatHudQuestionColor@%senderName@": "@StripMLControlChars(%question));
    }

    if (%answer!$="")
    {

       %answer=strReplace(%answer,"","\n");

       %line=%answer;
       %i=1;
       while(%i<=$Pref::RPGDialog::MaxOptions) //lets number the options
       {
          %Start=strpos(%line,"<a:RPGDialog "@%i@">");

          if(%Start<0)
          {
             %i=$Pref::RPGDialog::MaxOptions+1;
          }
          else
          {
             %line=getSubStr(%line,%Start,strlen(%line));
             %End=strpos(%line,"</a>")+4;
             %line=getSubStr(%line,%End,strlen(%line));
             %answer=strReplace(%answer,"<a:RPGDialog "@%i@">","<a:RPGDialog "@%i@"> "@%i@" - ");
             %i++;
          }
       }

       
       RPGDialogAnswer.settext(%answer);
    }
    else
    {
       RPGDialogAnswer.settext("<a:RPGDialogNoAnswer>Continue...");
    }
    RPGDialogAnswer.Visible=true;

    Canvas.pushDialog(RPGDialog);
}

function RPGDialogAnswer::onURL(%this, %url)
{
//same as RPGDialogQuestion::onURL, so just forward the call
RPGDialogQuestion::onURL(%this, %url);
}

function RPGDialogQuestion::onURL(%this, %url)
{

      %Answers=%this.gettext();
      %Answers=strReplace(%Answers,restwords(%url)@" - ","");
      %AnswerHeaderSize=strlen("<a:RPGDialog "@restwords(%url)@">");
      %AnswerStart=strpos(%Answers,"<a:RPGDialog "@restwords(%url)@">")+%AnswerHeaderSize;
      %Answers=getSubStr(%Answers,%AnswerStart,strLen(%Answers));
      %AnswerEnd=strpos(%Answers,"</a>")+4;

      ChatHud.addLine($Pref::RPGDialog::ChatHudAnswerColor@"You: "@StripMLControlChars(getSubStr(%Answers,0,%AnswerEnd)));
 
      CommandToServer('RPGDialogAnswer',%url);

      Canvas.popDialog(RPGDialog);
      RPGDialogQuestion.settext("");
      RPGDialogAnswer.settext("");
  
}

All that code simply parses the Message and Response and gets them ready to display to the player in a nice gui, the most important thing here is the CommandToServer in the onURL function. This takes us to our next set of code, again back in the server.
function serverCmdRPGDialogAnswer(%client,%answer){
	%answer = getWord(%answer,1);
	if(%answer !$=""){
		echo("The answer was "@%answer);
		%Action=GetQuestAction(%client,%answer);
		//echo("Action is "@%Action);
		eval(%Action);
	}
	//quit();
}

All this function does is get the second word (a word in this case is is anything seperated by a space), of the answer and pass it to GetQuestAction. If you have done it right %answer will ALWAYS be a number.

Here is the code for GetQuestAction
function getQuestAction(%client, %selection){
	%selection--; //We are doing this because all our fields begin at 0
	%QuestGiverID = %client.getSelectedObject();
	%QuestGiver = %QuestGiverID.getShapeName();
	%QuestName = %client.Player.CurrentQuests[%QuestGiver];
	%query = "SELECT * FROM Quests WHERE QuestName = '"@%QuestName@"'";
	%result = $SQLite.query(%query,0);
	%client.Player.CurrentQuests[%QuestGiver] = "";
	
	for(%x=0; %x <= $SQLite.numColumns(%result); %x++){
		%client.Player.CurrentQuests[%QuestGiver] = %client.Player.CurrentQuests[%QuestGiver]@$SQLite.getColumn(%result,%x)@"\t";
	}
	
	if(%result){
		$SQLite.ClearResult(%result);
	}
	
	%query = "SELECT * FROM QuestActions WHERE QuestName = '"@%QuestName@"'";
		echo(%query);
	%result = $SQLite.query(%query,0);
	
	if(%result){
		if($SqLite.numrows(%result) >=1){
			%ActionList = $SQLite.getColumn(%result,"ActionList");
			$SqLite.clearResult(%result);
		}
	}
	
	%actionStub = getField(%ActionList, %selection);
	%x = 0;
	%action = getWord(%actionStub,%x)@"(";
	%x++;
	while(getword(%actionStub,%x) !$=""){
		%action = %action@getword(%actionStub,%x)@",";
		%x++;
	}
	%action = %action@"%client);";
	echo(%action);
	return %action;
}

Some important things to note, for some reason %selection was always 1 over what I had actually clicked, so rather than track the bug, I just added %selection--; as a hack to get around that fact.
The function above works alot like LoadItems from MMORPG 8, it reads in values from the database, and gets a string ready for the eval function to execute. Lets use and example from the real database. If you first click Kronk and get the Kronk_needs_Berries quest, then select the first action, this function will spit out "Quests("COMPLETE",%client);" Which when returned causes the following to occur eval(Quests("COMPLETE",%client););

In short, it's just a nice way of storing a function call in the database.

Next we use the Quest function, this is the heart of the whole system. And as such I am leaving it to you in an slightly incomplete state, to finish it will only take a few lines of code, but it will give you an excellent feel for how to extend this even further.
function Quest(%Action,%client){
	%QuestGiverID = %client.getSelectedObject();
	%QuestGiver = %QuestGiverID.getShapeName();
	%Quest = %client.Player.CurrentQuests[%QuestGiver];
	%QuestType = getField(%Quest,2);
	%QuestName = getField(%Quest,3);
	%Reward = getField(%Quest,7);
	%Fullfillment = getField(%Quest,8);

	if(%QuestType $= "ItemForItem"){
		echo("Trade Quest");
		%Item = getWord(%Fullfillment, 0);
		%Amount = getWord(%Fullfillment,1);
		echo(%Item@%Amount);
		if( %client.Player.inv[%Item] >= %Amount){
			%client.Player.decInventory(%Item,%Amount);
			%Item = getWord(%Reward, 0);
			%Amount = getWord(%Reward,1);
			echo(%Item@%Amount);
			%client.Player.incInventory(%Item,%Amount);
			%Action = "COMPLETE";
		}else{
			if(%Action $="COMPLETE"){
				MessageClient(%client,'Liar','Dont lie to me!');
				%Action = "ACCEPT";
			}
		}
	}
	
	if(%QuestType $= "ItemForFlag"){
		%Item = getWord(%Fullfillment, 0);
		%Amount = getWord(%Fullfillment,1);
		echo(%Item@%Amount);
		if( %client.Player.inv[%Item] >= %Amount){
			%client.Player.decInventory(%Item,%Amount);
			%Action = "COMPLETE";
		}else{
			if(%Action $="COMPLETE"){
				MessageClient(%client,'Liar','Dont lie to me!');
				%Action = "ACCEPT";
			}
		}
	}

	//You Need to code FlagForItem and put it here
	%client.Player.CurrentQuests[%QuestGiver] = "";
	%client.Player.QuestFlags[%QuestName] = %Action;
	
}

I have left FlagForItem out because it's actually the easiest to code, and I left FlagForFlag out because the code will do that function automatically.

So now you have an essentially complete Quest system, all you are lacking is the clientside gui.

Well here is the clientside gui
new GuiControlProfile ("RPGDialogQuestionProfile")
{
   fontType = "Arial Bold";
   fontSize = 16;
   fontColor = "44 172 181";
   fontColorLink = "255 96 96";
   fontColorLinkHL = "0 0 255";
   autoSizeWidth = true;
   autoSizeHeight = true;
};

new GuiControlProfile ("RPGDialogAnswerProfile")
{
   fontType = "Arial Bold";
   fontSize = 16;
   fontColor = "44 172 181";
   fontColorLink = "255 96 96";
   fontColorLinkHL = "0 0 255";
   autoSizeWidth = true;
   autoSizeHeight = true;
};

new GuiControlProfile ("RPGDialogScrollProfile")
{
   opaque = false;
   border = false;
   borderColor = "0 255 0";
   bitmap = "./demoScroll";
   hasBitmapArray = true;
};

new GuiControlProfile ("RPGDialogBorderProfile")
{
   bitmap = "./chatHudBorderArray";
   hasBitmapArray = true;
   opaque = false;
};

//--- OBJECT WRITE BEGIN ---
new GuiControl(RPGDialog) {
   profile = "GuiModelessDialogProfile";
   horizSizing = "width";
   vertSizing = "height";
   position = "0 0";
   extent = "640 480";
   minExtent = "8 8";
   visible = "1";
   helpTag = "0";

   new GuiControl() {
      profile = "GuiDefaultProfile";
      horizSizing = "center";
      vertSizing = "bottom";
      position = "120 300";
      extent = "400 300";
      minExtent = "8 8";
      visible = "1";
      helpTag = "0";

      new GuiBitmapBorderCtrl(RPGDialogBorder) {
         profile = "ChatHudBorderProfile";
         horizSizing = "width";
         vertSizing = "height";
         position = "0 0";
         extent = "400 300";
         minExtent = "8 8";
         visible = "1";
         helpTag = "0";
            useVariable = "0";
            tile = "0";

         new GuiBitmapCtrl(RPGDialogBackground) {
            profile = "GuiDefaultProfile";
            horizSizing = "width";
            vertSizing = "height";
            position = "8 8";
            extent = "384 292";
            minExtent = "8 8";
            visible = "1";
            helpTag = "0";
            bitmap = "./hudfill.png";
            wrap = "0";
         };
         new GuiScrollCtrl(RPGDialogScrollQuestion) {
            profile = "RPGDialogScrollProfile";
            horizSizing = "width";
            vertSizing = "height";
            position = "89 8";
            extent = "303 94";
            minExtent = "8 8";
            visible = "1";
            helpTag = "0";
            willFirstRespond = "1";
            hScrollBar = "alwaysOff";
            vScrollBar = "dynamic";
            constantThumbHeight = "0";
            childMargin = "0 0";

            new GuiMLTextCtrl(RPGDialogQuestion) {
               profile = "RPGDialogQuestionProfile";
               horizSizing = "width";
               vertSizing = "height";
               position = "1 1";
               extent = "303 16";
               minExtent = "8 8";
               visible = "1";
               helpTag = "0";
               lineSpacing = "0";
               allowColorChars = "0";
               maxChars = "-1";
            };
         };
         new GuiScrollCtrl(RPGDialogScrollAnswer) {
            profile = "RPGDialogScrollProfile";
            horizSizing = "width";
            vertSizing = "height";
            position = "8 100";
            extent = "384 190";
            minExtent = "8 8";
            visible = "1";
            helpTag = "0";
            willFirstRespond = "1";
            hScrollBar = "alwaysOff";
            vScrollBar = "dynamic";
            constantThumbHeight = "0";
            childMargin = "0 0";

            new GuiMLTextCtrl(RPGDialogAnswer) {
               profile = "RPGDialogAnswerProfile";
               horizSizing = "right";
               vertSizing = "bottom";
               position = "1 1";
               extent = "384 14";
               minExtent = "8 8";
               visible = "0";
               helpTag = "0";
               lineSpacing = "2";
               allowColorChars = "0";
               maxChars = "-1";
            };
         };
         new GuiBitmapCtrl(RPGDialogPortrait) {
            profile = "GuiDefaultProfile";
            horizSizing = "right";
            vertSizing = "bottom";
            position = "8 8";
            extent = "80 94";
            minExtent = "8 2";
            visible = "1";
            helpTag = "0";
            wrap = "0";
         };
      };
   };
};
//--- OBJECT WRITE END ---

Alright here is an explaination of the flow.

When you enter the game, your player is given a faction (you will need to add this into player creation in game.cs).
If you target an NPC, some checks are made, first for faction, and then to see if he/she is a QuestGiver.

All of that happens in serverCmdTarget, if your factions are not the same the NPC is obliged to attack you, if they are the same, then RPGDialogMessageClient is called, which first calls GetQuest.
GetQuest looks at your QuestFlags for quests marked COMPLETE and if it finds a Quest that is not flagged as complete, then it sticks the Message on your CurrentMessage and Response on your CurrentResponse.
The function then returns and RPGDialogMessageClient takes over again.

RPGDialogMessageClient sends your CurrentMessage and CurrentResponse to the client where it is picked up by clientCmdRPGDialogMessage

clientCmdRPGDialogMessage parses both the message and response and preps them for display, then pushes the RPGDialog gui.

RPGDialog gui, waits until the player clicks an underlined link and then it calls it's OnURL function which passes back the selection to the server.

The server picks this up and handles it with, serverCmdRPGDialogAnswer

serverCmdRPGDialogAnswer removes the extra stuff and leaves us with a number which happens to be the chosen response, this is passed to
getQuestAction, which creates an executable string from the response and actionlist in the DB and passes it back to
serverCmdRPGDialogAnswer, which subsequently evals and executes the string.

For our purposes we made sure that the executable string will always be Quest(FLAG,%client); However you could deffinetely adapt this to exec whatever function you wanted.

serverCmdRPGDialogAnswer for our purposes is calling Quest
Quest looks at a number of factors, but primarily it's looking for the QuestType to determine what to do.
If it's ItemForItem, then we dec the player inventory by fullfillment and inc it by reward.
If it's FlagForItem, then we dec the player inventory by fullfillment and set whatever flag is called for.
If it's FlagForFlag, then we set flags on the player (this will usually happen automatically)
If it's ItemForFlag, then we inc the players inventory by the reward item.

Note that all of the above are what should happen when Quest gets a "COMPLETE", if it gets a refuse it should exit, and an ACCEPT should only set the QuestFlags[%QuestName] to Accept.

Ok folks thats it, the final installment of my MMORPG tutorials series, I hope you've enjoyed working with them as much as I have enjoyed making them.

Thanks for taking the time to read this!

Sincerely
Dreamer!

#1
06/04/2005 (2:30 am)
Due to formatting issues there are yet again a whole lotta line breaks to fix :(
#2
06/04/2005 (11:16 am)
Lookin' good though!
#3
06/05/2005 (2:37 pm)
Once again, another great resource! :)

Sorry to see that this is your last one in the series, but I'm so looking forward to the Advanced .PDF file when it is ready :)
#4
06/13/2005 (8:27 am)
Great job Dreamer. Wonder why this hasn't been approved yet?
#5
06/13/2005 (9:34 am)
It takes them awhile sometimes to approve these things. Thats why I made the link from the previous resource to this one, so people could implement it right away.

Sure wish they could do something about the line break issue though, my pretty code looks like complete crap :(
#6
06/13/2005 (9:55 am)
Code's looking pretty good to me Dreamer...

I'm on WinXP SP 2 with IE 6.
#7
06/26/2005 (2:22 pm)
I see it is approved now, :D.

I have had this thing in my Firefox bookmarks for quite some time.

Robert
#8
06/26/2005 (11:10 pm)
Nice work Dreamer.
When you mention problem number 2 are you refering to function serverCmdRPGDialogAnswer?
I'm hoping to add an enabled bool so that I can turn certain answers on and off.
Anyone have any suggestions on how to fix this?
serverCmd functions can be deadly to fair game play.

Ari Rule
#9
06/27/2005 (5:23 pm)
you can fix that format problem by first copying to wordpad and then recopying from word before you paste.

I understand you have a whole series of these tuts! Do you have them all in one place? Or maybe links to them all in one place?

Im working on a singleplayer example and kinda leaning towards a hybrid of single player and a mmorpg only less of the mmorpg and more of the singleplayer.
#10
06/28/2005 (8:47 am)
Do you ever sleep ? You pump these things out so fast.

I'll give it a 5 just because it's so freaking long.
#11
06/30/2005 (10:33 pm)
All I can say Dreamer is WOW! This is some Awesome Stuff. I had a few hiccups along the way in implementing these, but nothing that was a deal breaker. Thanks to you, I have the core of an MMORPG inside of a week! (Summer Vacation offers lots of free time!). As a request, will you post the Advanced PDF link here when you have it ready? I can hardly wait!.

Brent
#12
06/30/2005 (10:44 pm)
Also one other quick thought, Dreamer. What are you implementing as far as monster spawns? I am thinking on shoehorning these resources in, and expanding on them, and I wondered what you might think, or what your own plans are?
Patroller being the first piece and
Guard being the second.
I thought this was a clever way to go about it, It uses a path marker to determine spawn types and locations, then on mission load it replaces all the markers with guards and patroller AIs, and also uses the markers to track respawn of the AI and all sorts of things.

Brent
#13
07/06/2005 (5:15 pm)
Woohoo! I'm alive! Yeah!!! Sorry for the long absence folks. We switched ISPs, only there was a scheduling conflict and the new ISP wasn't able to hook service back up for nearly a week. After that there was some trouble at the DSLAM and a whole lot of other crap at the central office. Needless to say, other than a quick email scan everyother day or so, and what time I could shoehorn into the library I've been offline nearly a month!

Anyways I'm back and better than ever! MMORPG Advanced will get to the proofreaders this weekend (I still need a couple more proofreaders, if interested drop a line) and hopefully it will be sent to GG by next friday. (It's pretty large, the documentation alone is over 1MB of plain text, and don't even get me started on the zip file:)

Thanx for your patience everyone, and please note that while I'm finishing up MMORPG Advanced I will not be able to support the basic MMORPG tutorials, also Advanced is different enough in so many critical ways, that I may just decide to only support MMORPG Advanced in the future, or as I'm choosing to call it...
Starter.MMORPG
#14
07/11/2005 (5:32 pm)
Great news Dreamer :) Am I still on the list for proofreading? I'm still available ;)

"Starter.MMORPG" I like the sound of that!
#15
07/11/2005 (11:12 pm)
Hey there Rodney, sorry but I lost your email, if your interested in proofreading, email me smorrey@gmail.com and I'll get it right out to ya.
#16
07/12/2005 (10:56 am)
Email sent :)
#17
07/17/2005 (4:39 am)
For those who care to sneak a peak Advanced MMORPG Tutorial
It's not been approved yet, but it's still accessible :)
#18
05/05/2008 (2:56 pm)