Aardwolf MUD Home Page Link

Location: Home / Lua Coding / Goal System

The goal system allows you to define a high level goal that a player must achieve and one or more tasks to complete that goal. Each task can track status to allow progression through a quest and when all tasks in a goal are completed, the goal itself is considered complete. Each goal / task can also have multiple hints associated with it that are made available to players as they are uncovered. See the task log system page for more on this.

A goal can be a single task such as Lands of Legend, or can be a complex area with multiple tasks and multiple status types per task. For example, the Academy uses a task for each trainer, but each trainer itself uses many different programs. The tasks are used to track current progress through each lesson, commands typed, vipers killed, etc. Being able to reset a task allows for options like the Academy 'restart' option.

To create a goal table with tasks, you need to define your goal and what the tasks will be and then discuss with Lasher. Your task definitions are changeable online once set up - chances are your plans will change at least a little during actual development.

Goal / Task Design

The first thing to decide with a goal is what unlocks it. In the Academy, just moving 'north' to the recruiter's room triggers the goal. In Lands of Legend, boarding the train triggers the goal.

Other goals may be more difficult to find - a goal does not have to be the full area quest, it can be a little side task in the area that rewards a piece of equipment or opens another goal. Some goals may actually never be discovered - as long as completing them is not essential for other parts of the area or questing, this is fine.

The next thing to decide is what each task will be, and whether or not all tasks are visible when the goal is unlocked. For example, The academy lessons only become visible as the previous lesson is completed.

For each tasks, you need to determine:

  • What is the task itself?
  • What action(s) trigger success of the task?
  • What action(s) trigger opening the task? Usually completion of some other task.
  • Should there be any level, class, race, stat or other restrictions on being eligible for the task?
  • What other mobs act differently based on this task being open or completed?

Program Outline

The basic flow of progs for a goal is outlined below. This is also usually the flow for individual tasks and status within each task.

1. On the mob that opens the goal:


if the player has already completed this goal/task then
   (optional) say or do something different. Guard might bow to you etc.
   return ... 
end

if the player already has the goal/task open then
   (optional) say or do something different. Academy trainers invite you to continue their lesson.
   return ... 
end

if the player is eligible for the goal/task then
   (optional) say or do something to introduce the goal/task.
   set the task as open on the player.
end

2. Other mobprog may now check for specific tasks being open and/or the status.

For example, the vipers in Lidnesh will only ever load a skin if you have the Economy Training task open, are at the appropriate part of Vladia's lesson (using status codes) and have killed at least 5 vipers so far (using task data - more on that later).

3. Other mobs may update the status and data of any task or set flags that can be checked later. One mob needed to complete a task may give you a task of its own before it will cooperate. Whether you use a new 'task' in the quest itself to do this or just track status is a design choice - if the Academy had used only 'tasks' then 'goals academy' would be over 500 lines long.

That is pretty much all there is to it. Designing the goals and tasks is harder than coding them if you already know the aardwolf lua prog system. A summary of the appropriate commands, followed by a working example conversion of an area is below.

Goal / Task related functions

Most goal/task functions take a goal number and then the task number within each goal. Goals can also be accessed by name, but using numbers is more efficient so all examples will use that. You will know your goal number when your goal is created. See the walkthrough after this list of functions for example use.

addtask - syntax: addtask(CH,goal,[task],opts)
This function adds a task or a goal to a player. If the player has already completed the goal, or already has the task open, the action will fail. Otherwise, the task is added and if the goal the task belongs to is not already open on the player, it is also added. The only valid option is LP_GROUP : Any other players in the room that are in the same group as ch, and have the goal open, will also get the task added.
completetask - syntax: completetask(CH,goal,task,opts)
This function flags a task as completed on a player. If the task is not open, or has already been completed, the action fails. If completing this task means that all tasks on a goal are now complete, the entire goal is also completed. Note that once an entire goal is completed, information stored on the player about specific tasks is no longer maintained - only the record that they completed the overall goal. This is for performance reasons, storing the status of every task on every goal indefinitely is not practical.
There may be situations in which you want to complete an entire goal even if all of the tasks that belong to the goal are not completed. To do this, add a fourth argument of LP_FORCE.
You may also want to close a goal that has no tasks, or even close a goal that was never opened on the player. To do this, use a task of 'nil' with LP_FORCE. For example: completetask(ch,1,nil,LP_FORCE). This allow goals to be hidden until they are actually completed.
The only other valid option is LP_GROUP - the program will attempt to complete the task for any other players in the room grouped with ch. Note that builders need to be aware of the potential for players to group with their friends immediately before completing a task so that they can "get the flag" when using this option.
taskdone - syntax: taskdone(CH,goal,[task])
When called without a task id, this function returns true if the player has completed the overall goal. When called with a task id, returns true if the player has that specific task completed. If the player has completed the overall goal, all tasks within it are considered done.
taskopen - syntax: taskopen(CH,goal,[task])
When called without a task id, this function returns true if the player has the overall goal open. When called with a task id, returns true if the player has that specific task open.
taskseen - syntax: taskseen(CH,goal,[task])
Many progs want to introduce a task or prompt a player only if they don't already have that task open and have not completed it - the task is new to them. Taskseen is a shortcut way of checking taskopen and taskdone at the same time. If the player has completed the task (or its overall goal), or has the specific task already open, this function returns true, otherwise it returns false.
The academy recruiter uses this function to determine whether or not to prompt someone to enlist.


The remaining three functions are more focused on managing task data once a task is open than on opening/closing tasks. Each task on a goal has three pieces of data that are stored on the player as they proceed through a task.

taskstatus - syntax: taskstatus(CH,goal,task,[new status])
This function either returns or sets the status of a task. Without the fourth argument, the current status of the task is returned. If the task is not open, or already completed, 0 is returned. If the fourth argument is added, this sets the status on an already open task.
For a simple task such as 'bring me a red sword' there is no need to track status - the task is open, when the player brings the sword, it is closed. For tasks that have multiple steps (or subtasks) status can be used to track which part of a task the player is on.
The academy uses task status extensively to track which lecture or assignment you are on within a lesson. When you "restart", status is set back to 1 for that lesson. When you skip ahead to the quiz, your status for that task is moved up to the status that means you're on the quiz. When you type "next" or complete an action, task status is moved forward. When you "repeat" a lecture, it goes back to the status for the last lecture.
taskdata - syntax: taskdata(CH,goal,task,[new data])
This function either returns or sets data on a task. Without the fourth argument, current task data is returned. If the task is not open, or already completed, 0 is returned. If the fourth argument is added, this sets the data value an already open task.
Task data is similar to status - it is a numeric field on the task used to track progress. Task data will not be used often, but it is a second level of information that can be stored. Most of the time, whatever you would use task data for, 'status' will work just fine.
Even in the Academy, task data is only used in a single place - to track the number of vipers killed in the hunting lesson. We could not use status for that as 'status' was already in use to track progress through the lesson. If the viper quest had been a single "Go kill 100 vipers for me" mini-quest, we'd have just used status to count mobkills on the vipers.
taskflag - syntax: taskflag(CH,goal,task,flag#,[1,0])
This function either returns or sets that value of a specific flag on a task. Flag numbers can be 0 through 31. Without the fifth argument, the status of the flag is returned. If the fifth argument is 0 or 1, the flag is set/unset.
Task flags are similar to task data, but are used to remember a simple yes/no on whether or not an action has already taken place. When you only need to record yes/no, task flags are better because you can track multiple flags per task.
Again, these will not be used often. Task flags are used in the Academy in two places - if a player repeats the appropriate lessons, task flags being set the first time around prevents repeating the note posted by Aaeron and prevents loading a second leaflet during the auction lesson.
taskhint - syntax: taskhint(CH,goal,task,hint#,[1,0])
This function controls a set of flags similar to taskflags, but with the purpose of controlling which hints a player can see on a goal. Full details can be seen on the task hint system page.

Using the task system - Call of Heroes

As you can see, the functions to interface with the task itself are not particularly complex. How you use them in your programs can be as complex or simple as needed.

At the time of writing, the "Call of Heroes" area by Rezit has been converted to Lua, but its area quest has not been converted to the goal system. Converting the Call of Heroes quest will provide a good working example.

Defining the goal and tasks

There are two main quests in Call of Heroes. The first is to return the three banners from the warring factions for a 20% chance to receive one of three reboot-only pieces of equipment. The second is to retrieve the swords from the battlefield and give Mystlin the correct combination to receive the reboot-only portal, if it has not already been claimed.

For the first quest, we have a design choice to make here:

  1. Keep the chances to load exactly the same, goals can be completed without ever getting the special items.
  2. Keep the chances to load exactly the same, but also always reward completion of the tasks with one of the items.
  3. Remove the old logic and have the items / portal available only while completing the goal, making them effectively once per player ever.

There is no right or wrong answer, but to leave the original design of the area intact as much as possible, we chose option 2 - Mystlin will always reward one of the pieces of equipment when the banners are returned and will always reward the reboot-only portal when the goal is completed.

The old Call of Heroes logic will remain in place. Players not working through the goal will have the same chances to load the items as before. This does also complicate the conversion a little - most areas won't need to work this way.

We are changing the second quest slightly - you will be required to give all 8 swords to Mystlin. Once all 8 are given, the player will receive an owned version of the portal. As the portal is tied to goals, it willl be available once only. The chance to get a reboot portal independently of goals can be left in place.

We will use task flags to track each of the 8 swords and whether or not they have already been given to Mystlin. Using flags means we won't require the player to have all 8 swords in their inventory at once - they can be given to Mystlin in any order.

Based on reviewing the area so far, our tasks for this goal are (numbers start at 0):

  1. Return a golden eagle standard to Mystlin
  2. Return a skull of the moruk to Mystlin
  3. Return a phoenix banner to Mystlin
  4. Return Mystlin's collection of swords from the battlefields.

Determine goal trigger

The next thing to decide is what unlocks the overall goal. As this is a low level goal it won't be hidden. Mystlin is already quite spammy on entry, so we'll have her mention the goal in her greet prog if the player has not already seen it, but will require "accept" to be typed to open the goal. "Accept" will be a room trigger. The Call of Heroes overall goal is goal number 1.

While we're in here, let's do cleanup on Mystlin's speech on entry to delay it a little, and add room triggers so that players can just type 'friend', 'foe' or 'fun' as well as saying it. Mystlin will also tell them what to type.


Goal Introduction Code

The main greet prog on Mystlin then becomes:

--- Mystlin's normal friend/foe/fun output before here.
... 

--- If player has already completed or has task open, do nothing.
--- sayto means only this player sees the says.
sayto(ch,"I myself, I'd probably just go there for fun, to loot them all long after they die!")
sayto(ch,"Make your choice "..ch.name.." - Type 'friend$C', 'foe$C' or 'fun$C'.",LP_CR)

if not(taskseen(ch,1,1)) then 
   sayto(ch,"Pssst, I need some help recovering my artifacts from the battlefields.")
   sayto(ch,"Type '@Gaccept@w$C' if you are prepared to help me.")
end

To add the 'friend', 'foe' or 'fun' triggers we don't even need any new code. We just add those as room triggers calling the appropriate mobprogs and requiring Mystlin to be present in order for them to run. Note that this means the actual 'friend' command is no longer useable in this particular room, the room trigger blocks it. The room prog triggers are:

No   Trigger Type Program       Phrase               Actor
==   ============ ============= ==================== ==============
 1 - Command      callhero-41   accept               callhero-0 (Mystlin)
 2 - Command      callhero-1    friend               callhero-0 (Mystlin)
 3 - Command      callhero-2    foe                  callhero-0 (Mystlin)
 4 - Command      callhero-3    fun                  callhero-0 (Mystlin)

When the player types accept, a new prog is needed to check if the player already has the task open, or has already completed it. If not, then add the goal is added to the player. We added a little delayed output script for Mystlin to present the goal. The full 'accept' prog is below. The 'makewait' function is covered in detail later on.

if taskdone(ch,1) then
   sayto(ch,"You have already helped me, "..ch.name..". I may choose to reward items you bring to 
             me from time to time, but I have no tasks for you.")
   return
end

if taskopen(ch,1) then
   sayto(ch,"You are already helping me, "..ch.name..". Type 'goals callhero$C' 
             to see what is required of you.")
   return
end

--- Make sure the player isn't already interracting with this
--- paused prog, otherwise scripts can be spammed over and over.
if progactive(self.gid,ch.gid) then return end

--- Makewait is a special type of function that creates a lua 'thread', allowing
--- the function to be paused and resumed later on. This is always necessary for
--- progs that have timed delays, unless they use the old mobprog style 'adddelay'.

makewait(function()
   echoat(ch,"$n smiles at you.",LP_CAPS)
   --- Pause for a second.
   mpause(1)
   
   --- Make sure the player and actor are still alive and in same room.
   if self == nil or ch == nil or self.room.key ~= ch.room.key then return end

   sayto(ch,"When I was placed in this prison I cast down many artifacts to 
             provoke war between the Legionnaires and the Moruks.")

   mpause(2)
   if self == nil or ch == nil or self.room.key ~= ch.room.key then return end
   
   --- The 'say' is broken into multiple lines below only for web formatting. In
   --- the actual prog it is a single line.
   sayto(ch,"These fools no longer amuse me and I would like my items retrieved. 
             As a test of your worth, I need you to visit the battlefields and 
             retrieve three standards from the warring factions. Return the standards 
             to me and I will trust you with the task to find my most prized weapons.")

   mpause(2)
   if self == nil or ch == nil or self.room.key ~= ch.room.key then return end
   
   --- Now add the actual tasks and open the goal.
   sayto(ch,"Good luck you, "..ch.name..". Do not disappoint me.")
   echoat(ch,"$n cackles insanely.",LP_CAPS)
   addtask(ch,1,0)
   addtask(ch,1,1)
   addtask(ch,1,2)
   
--- The following two lines are very important - they close the makewait function.
end
)  

Managing the tasks

Once the tasks are open, we need programs to manage and update them as the player performs actions necessary to complete each task.

As each of the 3 banners are given to Mystlin, we will check if that specific goal is open and if so complete the task. Once all three are completed, we open task 3. We also don't want players doing the goal to be cheated out of the 20% chance of loading one of the reboot-only items so once the three banners are returned, Mystlin will always load one of the items.

The existing programs on Mystlin to give a 20% chance of receiving one of the reboot-only items do not need to change, if the player does not have the goal open, they will be called instead.

--- Called when eagle banner given to Mystlin. Similar progs in place for
--- the other two banners.

if taskopen(ch,1,0) then
   sayto.("Thank you, "..ch.name..". I am glad you finished off the Legionaires. 
           They have been a thorn in my side since I entered this prison.")
   completetask(ch,1,0)

   --- Check if all three swords have now been given.
   call("callhero-42")
   return
end

--- Old mobprog begins here.
say("Hmm.")
say("Do you really deserve this? I am not too sure.")
social("ponder")
...

When one of banners are given to Mystlin and the tasks are open, we call this function to see if all three banners have now been returned. If so, we can unlock task 3:

--- Check if task is open and all three banners have been given.
if taskdone(ch,1,0) and taskdone(ch,1,1) and taskdone(ch,1,2) then
   --- All 3 banner have been given. But if task 3 is already unlocked/completed, nothing to do.
   if taskseen(ch,1,3) then return end

   sayto(ch,"You have proved your worth and returned my banners, "..ch.name..". 
             Take this as a small token of my favor.")

   --- The obj keys are sequential, so we can just oload a random.
   echoat(ch,"")
   local objkey = "callhero-"..math.random(47,49)
   give(oload(objkey),ch)

   --- WARNING: If there were a pause here, we would have to do the oload
   ---        : at the end. A player can leave half way through a paused script so
   ---        : they would be able to return and load another. The 'point of no
   ---        : return' is not until the final task is set.

   --- Now introduce the final task.
   echoat(ch,"")
   sayto(ch,"I have a much more important task "..ch.name..". I need eight weapons collecting from the 
             battlefields. Placing these implements of destruction in the hands of the mortals amused 
             me for a while, now I want them back. Return these swords to me as you find them. If 
             you return all eight, you will always be welcome in the Call of Heroes.")
   addtask(ch,1,3)
   return
end

This completes the first quest - all we really did is add some code to introduce the goal to the player, added three tasks and closed each of the tasks as the three banners are given.


Quest 2 - Return 8 swords for the portal

Instead of having a task for each of the 8 swords as we did with the banners, we will use a single task with flags 0 through 7 used to track each sword.

Part of the challenge with this task is how to inform the player which items are still needed. We are going to require the player to find the first sword for themselves (it isn't difficult). As each sword is given to Mystlin, she will tell the player which are remaining - similar to how Vladia's shopping list prog works in the Academy. Each time one of the 8 swords is given to Mystlin, she needs to:

  • See if the player has task 3 of the callhero goal open. If not, drop through to the old progs.
  • If the task is open but player has already given us this sword, tell them which are still needed.
  • If the task is open and this sword hasn't been given yet, say thanks and set the flag.
  • After receiving a sword, check if all 8 have now been given and give rewards then close the goal if so.

--- Give prog for each of the 8 swords. The flag used will change based on which sword.
--- Only run this part if the task is open.
if taskopen(ch,1,3) then

   --- Sword 0 - Tor'ia'vic
   if taskflag(ch,1,3,0) == 1 then
      sayto(ch,"You have already given me this sword.")
      --- tell them which are remaining.
      call("callhero-43")
   else
      --- set the task flag.
      taskflag(ch,1,3,0,1)

      --- check to see if all are done. If they aren't done, callhero-44 will call -43 to 
      --- give the list of items remaining.
      call("callhero-44")
   end
   return
end

--- Swords task not open, fall through to old prog...
say("Alas, I was hoping it wouldn't come to this...")

This program is used to list the items still needed:

--- callhero-43 : Show which swords are remaining.
sayto(ch,"I still need you to find and return these items to me:")
echoat(ch,"")

--- check flag 0
if taskflag(ch,1,3,0) == 0 then
   echoat(ch," - Tor'ia'vic")
end

--- check flag 1
if taskflag(ch,1,3,1) == 0 then
   echoat(ch," - Ju'bon'it")
end

--- Simply repeats the above for each of the 8 items.  

This program checks to see if all swords have now been returned. If they have it completes the quest. If they haven't, it calls the program above to report swords still needed.

--- callhero-44: Check if all 8 swords have been returned.

--- If task isnt open, do nothing.
if not(taskopen(ch,1,3)) then return end.
   
--- Use a loop, if ANY of the flags aren't set, we aren't done.
for i=0,7,1 do
  if taskflag(ch,1,3,i) == 0 then
     call("callhero-43")
     return
  end
end

--- If we reach here, all flags must be set, so we're done.
sayto(ch,"You have returned the full set of my weapons, "..ch.name.."! Thank you!")
echoat(ch,"")
sayto(ch,"You will always be welcome in the Call of Heroes, take this for your return journey.")
local tobj = oload("callhero-3",ch)
tobj.owner = ch.name
give(tobj,ch)
giveqp(ch,15)
completetask(ch,1,3)

That's it. Although it looks like a lot to read, the actual code added to Call of Heroes is fairly minor for a moderately complex set of goals.


A simpler example - Orchard Quest

The Call of Heroes tasks/goal are fairly complex. A simpler example is the quest added to the orchard where the head gardener requires you to kill 50 fire ants and 50 green weeds. The whole quest is just 5 progs including handling possible errors when the quest is already completed.

The first prog is called on entry to Kimr's room. If you have never opened his goal, you receive an echo suggesting you listen to him. If you have his goal open, he checks if you are done:

--- orchard-1
if not(taskseen(ch,6,0)) then
   social("sigh")
   emote("seems troubled by something, perhaps you should listen to him?")
end

if taskopen(ch,6,0) then
   --- are we done?
   if taskdata(ch,6,0) == 0 and taskdata(ch,6,1) == 0 then
      sayto(ch,"You did it! Congratulations "..ch.name.."!")
      sayto(ch,"I don't have much to offer you as way of thanks, but this may 
                be useful to you, particularly if you can find an enchanter.")
      local tobj = oload("orchard-15")
      addstats(tobj,"random",3)
      give(tobj,ch)
      giveqp(ch,10)
      social("bow",ch)
      completetask(ch,6,0)
      completetask(ch,6,1)
      return
   end
end

The next prog is called when the player 'listens' to Kimr as suggested. If they have already done the quest or have the quest open, a brief message is given and the prog ends. Otherwise, a small paused output script runs. We could have shortened the progs even further by just having the end of this script open the goal itself:

--- orchard-2
f taskdone(ch,6) then
   sayto(ch,"Thanks for your help with the garden, "..ch.name..". It looks much better now!")
   social("smile",ch)
   return
end
 
if taskopen(ch,6) then
   sayto(ch,"You're not done yet "..ch.name..". Type 'tasks$C' to see how many you have left.",LP_CR)
   return
end
 
makewait(function() 
   mpause(2)
   if self == nil or ch == nil or self.room.key ~= ch.room.key then return end
   sayto(ch,"The weeds and ants are taking over my orchard, "..ch.name..".  
             The apple trees can't grow and the animals are starting to leave.",LP_CR)
  
   mpause(2)
   if self == nil or ch == nil or self.room.key ~= ch.room.key then return end
   echoat(ch,"$n looks at you hopefully.",LP_CAPS)
   sayto(ch,"I know that in this troubled realm, one tiny little orchard doesn't 
             seem terribly important, but I have dedicated my life to maintaining 
             this haven. Will you help me? Please say yes...")
  prompt(ch)
end
)

The third prog is called when the player says 'yes'. This is also a shortcut for those who know the quest - just saying 'yes' without listening to the other stuff opens the goal.:

--- orchard-3
--- Accept the goal
if taskdone(ch,6) then
   sayto(ch,"Thanks for your help with the garden, "..ch.name..". It looks much better now!")
   social("smile",ch)
   return
end
 
if taskopen(ch,6) then
   sayto(ch,"You're not done yet, "..ch.name..". Type 'tasks$C' to see how many you have left.")
   return
end
 
makewait(function() 
   mpause(1)
   social("smile",ch)
   sayto(ch,"I don't expect you to clean up the orchard by yourself, but if you could 
             start to kill the weeds and ants for me that would make a difference. 
             Hopefully I can find some other kind souls to help out and between us 
             we can get the orchard restored to its former beauty.")   
   mpause(2)
   social("bow",ch)
   addtask(ch,6,0)
   addtask(ch,6,1)
   taskdata(ch,6,0,50)
   taskdata(ch,6,1,50)
   sayto(ch,"Thank you, "..ch.name..". I know I can count on you. Type 'tasks$C' to see 
             how many of each you have left to kill and return to me when you are done.",LP_CR)
   prompt(ch)
end
)

)

With progs already in place to manage opening/closing the goal, all we need now is a way to track the kills. Notice in orchard-3 we set task data on the two tasks to 50. Each time a player kills a fire ant or a weed with the task open, data will be decreased. Notice also that in orchard-1, we check for both being 0 to complete the quest. The following are the death programs on the weeds/ants:

--- orchard-4 - Track ant kills.
if taskopen(ch,6,1) then
   local tstat = taskdata(ch,6,1)
   if tstat > 0 then 
     taskdata(ch,6,1,tstat-1)
   end
end

--- orchard-5 - Track weed kills.
if taskopen(ch,6,1) then
   local tstat = taskdata(ch,6,0)
   if tstat > 0 then 
     taskdata(ch,6,0,tstat-1)
   end
end