Talk:Stendhal Quest Coding

From Arianne
Jump to navigation Jump to search


Stendhal Quests

Adding quests is perhaps the most complex step on any game.

So please before even trying, read these docs and make sure you understand the basis:

It would be even better if you try yourself and play a bit with it.

Implementation of an example quest (part 1)

Ok, now lets create a new Java file at games/stendhal/server/maps/quest. It will be named like the quest you are going to do, for example: LookBookforCeryl

package games.stendhal.server.maps.quests;

public class LookBookforCeryl extends AbstractQuest {


Then let see how to add a quest by example.


/** 
 * QUEST: Look book for Ceryl
 * PARTICIPANTS: 
 * - Ceryl
 * - Jynath
 * 
 * STEPS: 
 * - Talk with Ceryl to activate the quest.
 * - Talk with Jynath for the book.
 * - Return the book to Ceryl
 *
 * REWARD: 
 * - 100 XP
 * - 50 gold coins
 *
 * REPETITIONS:
 * - As much as wanted.
 */

This is mandatory! Describe the quest to your best at the top of the class so others can read it and spot bugs or test it completely. This way you ease the development of the whole game.

Many of you think content shouldn't be open source because it removed the fun of discovering. Well, we, on Arianne, don't agree with that statement, and we think that Stendhal is as fun as any other closed source code.

public class LookBookforCeryl implements IQuest {
     @Override
     public void addToWorld() {
         super.addToWorld();

         step1LearnAboutQuest();
         step2getBook();
         step3returnBook();
    }

This method is called by the Quest system to create the quest. So instead of writing the quest inside this method we have split it in steps.

Think of your quests as a set of steps that need to be done in order for it to be completed.

Let's see the first step: Talk with Ceryl to activate the quest.

     private void step1LearnAboutQuest() {

We need to get the npc-object representing "Ceryl" in order to be able to work with it:

    SpeakerNPC npc=npcs.get("Ceryl");

This works because in games/stendhal/server/maps/semos/library/LibrarianNPC.java we have already defined Ceryl NPC.

Creating dialogs

Now we simply get it to start adding dialogs to it.

Simple dialogs

There two ways of adding chat to a NPC. The first and simpler one is using a named addXXX method of the SpeakerNPC class.

  addGreeting("hi")

This class has a set of predefined triggers that you can use:

  • npc.addGreeting(text)
    Replies to anyone that greet this NPC with the given text.
    The trigger condition is hi, hello, hola. To start any conversation with a NPC the player MUST first greet the NPC.
  • npc.addGoodbye(text)
    It is what NPC writes when listen to bye or adios.
  • npc.addReply(trigger, text)
    Reply the attended player with text when NPC listen the keyword trigger or a word that contains the keyword.
  • npc.addQuest(text)
    Show text to player when NPC listen the keyword quest or task
  • npc.addJob(text)
    Show NPC job explained in text when listen the keyword job or work
  • npc.addHelp(text)
    When NPC listen to help or ayuda it says text.

We use a very simple method to denote special keywords by placing a # before it. The client renders the next word in a blue color.

Advanced dialog

The following guide is not complete. Please add the newer methods if necessary.



NOTE: THE STUFF BELOW THIS LINE IS OUTDATED AND NEEDS TO BE ADJUSTED


The other way of creating a NPC dialogue is by adding states to the Finite state machine (FSM). Have a look at the link to make sure you understand the idea.

The first set of rules about states is that:

  • State 0 is always the initial state (ConversationStates.IDLE).
  • State 1 is the state where only one player can talk to NPC (ConversationStates.ATTENDING). Any other player that tries to talk to NPC will only see the text set with addWaitMessage.
  • State -1 is used for jump from any state when the trigger is present (ConversationStates.ANY). For example very helpful for bye keyword.
  • States from 2 to 50 are reserved for Behaviours uses.
  • States above 50 are free at your disposal.

If you use twice the same state with the same trigger and the same condition NPC will advise you on server startup.

To add a state to NPC we use the method:

 public void add(int state, String trigger, ChatCondition condition, int next_state, String reply, ChatAction action)

This add a new state that is run when listen the trigger text and the condition is run and evaluated to true or it is null. If this happen then NPC moves to a new state set by next_state and says reply and if it is different of null run the action code.

It is a wise thing to make sure that condition code DOES NOT modify anything at player or NPC.

Let's see how it works.

First we need to create a message to greet the player and attend it. We add a hi event

 addGreating(0, "hi", null, 1, "Welcome player!", null)

State 0 is the initial state, so once NPC is in that state and listen "hi", it will say "Welcome player!" and pass to state 1.

We can personalize more the message like:

add(0, "hi", null, 1, null, new ChatAction()
  {
  public void fire(Player player, String text, SpeakerNPC engine)
    {
    engine.say("Welcome "+player.getName()+"!");
    }
  }) 

If we add these two states the NPC will choose randomly between them because both of them are suitable to start a conversation.

Let's add more states.

Now let's add some options when player is in state 1 like job, offer, buy, sell, etc.

 add(1, "job", null, 1, "I work as a part time example showman",null)
 add(1, "offer", null, 1, "I sell best quality swords",null)

Ok, two new events: job and offer, they go from state 1 to 1, because after listening to them the NPC can listen something like job.

Easy, isn't it?

Now let's add something harder. Let's make the NPC ask a question and let's expect a reply to the question.

I hope you see that this means adding a new state different of 1, because while we are waiting for the reply we are not interested in anything else.

 add(1, "fun", null, 50, "Do you want me to convert you in a heady bare elf?", null)

If player says fun then NPC will ask the question. Note that new state is 50 instead of 1. Now let's add two new states for the reply: yes and no.

  add(50, "yes", new ChatCondition()
    {
    public boolean fire(Player player, String text, SpeakerNPC engine)
      {
      return player.equals(HEADY_BARE_ELF);
      }
    },
     1, "Sorry!, you are already a heady bare elf!", null);

Heh! We add a condition on the yes to handle the case of the player being already a heady bare elf. This condition is perhaps handle better on the Fire part as we really want to execute something.

  add(50, "yes", null,
      1, null, new ChatAction()
    {
    public void fire(Player player, String text, SpeakerNPC engine)
      {
      if(player.equals(HEADY_BARE_ELF))
        {
        engine.say("Sorry!, you are already a heady bare elf!");
        }
      else
        {
        engine.say("Ok! But there is no return way! KAAABOOOM!");
        player.setOutfit("0");
        world.modify(player);
        }
      }
    });

See? This way is simpler and you save having to adding two states.

Let add the no state to complete it.

  add(50, "no", null, 1, "Oh! :-(", null);

Now NPC will only accept on state 50 either yes or no.

We could add a help message in case player get blocked.

  add(50, "help", null, 50, "Do you want me to convert you in a bare elf?", null);


Finally we want to finish the conversation, so whatever state we are we want to finish a conversation with Bye.

 add(-1, "bye", 0, "Bye!.", null);

We use -1 as a wildcard, so it text is bye the transition happens.

Implementation of an example quest (part 2)

Let's continue with our example and I comment anything that is really important beyond this point.

    
    /** In case Quest is completed */
    npc.add(1,"book",new SpeakerNPC.ChatCondition()
      {
      public boolean fire(Player player, SpeakerNPC npc)
        {
        return player.isQuestCompleted("ceryl_book");

The isQuestCompleted method returns true if the quest is already complete or false in other case. Each quest has a name (ceryl_book) and a state. Your quest can have as many subquests as you wants.

    
        }
      },
        1,"I already got the book. Thank you!",null);
        
    /** If quest is not started yet, start it. */      
    npc.add(1,"book",new SpeakerNPC.ChatCondition()
      {
      public boolean fire(Player player, SpeakerNPC npc)
        {
        return !player.hasQuest("ceryl_book");
        }
      },
        60,"Could you ask #Jynath for a #book that I am looking?",null);
        
    npc.add(60,"yes",null,
        1,null,new SpeakerNPC.ChatAction()
      {
      public void fire(Player player, String text, SpeakerNPC engine)
        {
        engine.say("Great!. Start the quest now!");
        player.setQuest("ceryl_book","start");
        }
      });

In this case the quest is not started because hasQuest was false. So we start the new quest. A good initial state for a quest is start and we set it using setQuest method.

    
    npc.add(60,"no",null,1,"Oh! Ok :(",null);

    npc.add(60,"jynath",null,60,"Jynath is a witch that lives at south of Or'ril castle. So will you get me the #book?",null);

    /** Remind player about the quest */
    npc.add(1,"book",new SpeakerNPC.ChatCondition()
      {
      public boolean fire(Player player, SpeakerNPC npc)
        {
        return player.hasQuest("ceryl_book") && player.getQuest("ceryl_book").equals("start");
        }
      },
        1,"I really need that #book now!. Go to talk with #Jynath.",null);

    npc.add(1,"jynath",null,1,"Jynath is a witch that lives at south of Or'ril castle. So will you get me the #book?",null);
    }

Here you have a example of what not to do :) Two different states with exactly the same trigger and the same text.

Better rewrite the offending lines as:

   npc.add(new int[]{1,60},"jynath",null,1,"Jynath is a witch that lives at south of Or'ril castle. So will you get me the #book?",null);

This way you have less probability of forgetting to change the other line.

It is duplicated because player may ask who is Jynath before accepting the quest and after accepting it.

This completes the first step of the quest. As you see there is only one zone and one NPC involved.

Now let's see the second step of the quest: Talk with Jynath for the book.

  
  private void step_2()
    {
    StendhalRPZone zone=(StendhalRPZone)world.getRPZone(new IRPZone.ID("int_orril_jynath_house"));

    SpeakerNPC npc=npcs.get("Jynath");    
    

As previously explained we get the zone where we are going to work in and we get the NPC that is going to be the main actor of this step.

  
    /** If player has quest and is in the correct state, just give him the book. */
    npc.add(1,"book",new SpeakerNPC.ChatCondition()
      {
      public boolean fire(Player player, SpeakerNPC npc)
        {
        return player.hasQuest("ceryl_book") && player.getQuest("ceryl_book").equals("start");

This condition is just more complex because we want to check that the quest is started, so we need to check if player hasQuest and that the quest state is the one we are expecting.

  
        }
      },
        1,null,new SpeakerNPC.ChatAction()
      {
      public void fire(Player player, String text, SpeakerNPC engine)
        {
        player.setQuest("ceryl_book","jynath");
        engine.say("Here you have the book Ceryl is looking for.");

        Item book=world.getRuleManager().getEntityManager().getItem("book_black");            
        player.equip(book);

If the quest is in the correct state we tell player that we give him the book and we add it to player inventory. Each item knows where it should equip itself. So just write player.equip(book) and done.

Perhaps you will be interested in handle extreme conditions like inventory full or similar ones.

  
        }
      });

    /** If player keep asking for book, just tell him to hurry up */
    npc.add(1,"book",new SpeakerNPC.ChatCondition()
      {
      public boolean fire(Player player, SpeakerNPC npc)
        {
        return player.hasQuest("ceryl_book") && player.getQuest("ceryl_book").equals("jynath");
        }
      },
        1,"Hurry up! Grab the book to #Ceryl.", null);

Perhaps player didn't notice about the book. So tell it that book is already there and to hurry to grab the book to Ceryl.

  

    npc.add(1,"ceryl",null,1,"Ceryl is the book keeper at Semos's library",null);

    /** Finally if player didn't started the quest, just ignore him/her */
    npc.add(1,"book",new SpeakerNPC.ChatCondition()
      {
      public boolean fire(Player player, SpeakerNPC npc)
        {
        return !player.hasQuest("ceryl_book");
        }
      },
        1,"Shhhh!!! I am working on a new potion!.", null);      
    }

Now the last step of our quest: Return the book to Ceryl

  
  private void step_3()
    {
    StendhalRPZone zone=(StendhalRPZone)world.getRPZone(new IRPZone.ID("int_semos_library"));

    SpeakerNPC npc=npcs.get("Ceryl");
        
    /** Complete the quest */        
    npc.add(1,"book",new SpeakerNPC.ChatCondition()
      {
      public boolean fire(Player player, SpeakerNPC npc)
        {
        return player.hasQuest("ceryl_book") && player.getQuest("ceryl_book").equals("jynath");
        }
      },
        1,null,new SpeakerNPC.ChatAction()
      {
      public void fire(Player player, String text, SpeakerNPC engine)
        {
        Item item=player.drop("book_black");
        if(item!=null)
          {
          engine.say("Thanks!");
          StackableItem money=(StackableItem)world.getRuleManager().getEntityManager().getItem("money");            

          money.setQuantity(50);
          player.addXP(100);

          world.modify(player);

          player.setQuest("ceryl_book","done");
          }
        else
          {
          engine.say("Where did you put #Jynath's #book?. You need to start again the search.");
          player.removeQuest("ceryl_book");
          }              
        }
      });
    }
  }
 

Finally, when you have finished your quest Java file, you have to edit the StendhalQuestSystem.java Java file at games/stendhal/server and add your quest to it to make the Quest System "aware" of the existence of your newly added quest. In this example, our Java quest file is LookBookForCeryl.java so we add its name at the end of the list of already existent quests:

package games.stendhal.server;
 
import marauroa.common.Log4J;
import org.apache.log4j.Logger;
import games.stendhal.server.maps.quests.IQuest;
 
public class StendhalQuestSystem 
  {
  /** the logger instance. */
  private static final Logger logger = Log4J.getLogger(StendhalQuestSystem.class);
 
  StendhalRPWorld world; 
  StendhalRPRuleProcessor rules;
 
  public StendhalQuestSystem(StendhalRPWorld world, StendhalRPRuleProcessor rules)
    {
    this.world=world;
    this.rules=rules;
 
    loadQuest("SheepGrowing");
    loadQuest("OrcishHappyMeal");   
    loadQuest("LookBookforCeryl");
    }