Stendhal Quest Coding: Difference between revisions

From Arianne
Jump to navigation Jump to search
Content deleted Content added
imported>MiguelAngelBlanchLardin
No edit summary
imported>MiguelAngelBlanchLardin
No edit summary
Line 183: Line 183:
Now let's add some options when player is in state 1 like job, offer, buy, sell, etc.
Now let's add some options when player is in state 1 like job, offer, buy, sell, etc.


add(1, "job", 1, "I work as a part time example showman",null)
add(1, "job", null, 1, "I work as a part time example showman",null)
add(1, "offer", 1, "I sell best quality swords",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
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.
the NPC can listen something like job.


Easy, isn't it?
add(1, "buy", 20, null, new ChatAction()

{
Now let's add something harder.
public void fire(Player player, String text, SpeakerNPC engine)
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.
<pre>
add(50, "yes", new ChatCondition()
{
{
public boolean fire(Player player, String text, SpeakerNPC engine)
int i=text.indexOf(" ");
String item=text.substring(i+1);
if(item.equals("sword"))
{
{
return player.equals(HEADY_BARE_ELF);
engine.say(item+" costs 10 GP. Do you want to buy?");
}
}
else
},
1, "Sorry!, you are already a heady bare elf!", null);
</pre>

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.
<pre>
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, I don't sell "+item);
{
engine.setActualState(1);
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);
}
}
}
}
});
</pre>
});

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

Let add the no state to complete it.
<pre>
add(50, "no", null, 1, "Oh! :-(", null);
</pre>

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

We could add a help message in case player get blocked.
<pre>
add(50, "help", null, 50, "Do you want me to convert you in a bare elf?", null);
</pre>


Now the hard part, we listen to buy so we need to process the text, and for that we use the
ChatAction class, we create a new class that will handle the event.
Also see that we move to a new state, 20, because we are replying to a question, so
only expect two possible replies: yes or no.


Finally we want to finish the conversation, so whatever state we are we want to finish a conversation with Bye.
add(20, "yes", 1, null, null); // See Behaviours.java for exact code.
add(20, "no", 1, null, null); // See Behaviours.java for exact code.


add(-1, "bye", 0, "Bye!.", null);
Whatever the reply is, return to state 1 so we can listen to new things.
Finally we want to finish the conversation, so whatever state we are we want to finish a conversation
with Bye.


We use -1 as a wildcard, so it text is bye the transition happens.
add(-1, "bye", 0, "Bye!.", null);


Let's continue with our example and I comment anything that is really important beyond this point.
We use -1 as a wildcard, so it text is bye the transition.happens.


<pre>
<pre>

Revision as of 19:11, 4 March 2006

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.

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

package games.stendhal.server.maps.quests;

public class LookBookforCeryl implements IQuest 
  {
  public LookBookforCeryl(StendhalRPWorld w, StendhalRPRuleProcessor rules)
    {
    }
  }

The constructor ALWAYS has two parameters: World object and Rules object.

Then let see how to add a quest by example.

package games.stendhal.server.maps.quests;

import games.stendhal.server.*;
import games.stendhal.server.maps.*;
import games.stendhal.server.entity.Player;
import games.stendhal.server.entity.item.Item;
import games.stendhal.server.entity.item.StackableItem;
import games.stendhal.server.entity.creature.Sheep;
import games.stendhal.server.entity.npc.Behaviours;
import games.stendhal.server.entity.npc.NPC;
import games.stendhal.server.entity.npc.SpeakerNPC;
import games.stendhal.server.pathfinder.Path;

import java.util.*;

import marauroa.common.game.IRPZone;

Just java wording. Import whatever you need. It is an accept good practique to import only what you really need.

/** 
 * 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 completly. 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 
  {
  private StendhalRPWorld world;
  private NPCList npcs;
  
  public LookBookforCeryl(StendhalRPWorld w, StendhalRPRuleProcessor rules)
    {
    this.npcs=NPCList.get();
    this.world=w;
    
    step_1();
    step_2();
    step_3();
    }

The constructor is called by the Quest system to create the quest. So instead of writting the quest inside the constructor 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 step_1()
    {
    StendhalRPZone zone=(StendhalRPZone)world.getRPZone(new IRPZone.ID("int_semos_library"));

We get the zone we are going to work in.

Ceryl lives at Library so we get library.

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

This works because in games/stendhal/server/maps/Semos.java we defined Ceryl NPC.

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

    Behaviours.addQuest(npc,"I am looking for a very special #book");

There two ways of adding chat to a NPC. The first and simpler one is using the Behaviour class.

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

  • addGreeting(npc, 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.
  • addGoodbye(npc, text)
    It is what NPC writes when listen to bye or adios.
  • addReply(npc, trigger, text)
    Reply the attended player with text when NPC listen the keyword trigger or a word that contains the keyword.
  • addQuest(npc, text)
    Show text to player when NPC listen the keyword quest or task
  • addJob(npc, text)
    Show NPC job explained in text when listen the keyword job or work
  • addHelp(npc, text)
    When NPC listen to help or ayuda it says text.
  • addSeller
  • addBuyer
  • addHealer
    These three tasks are so common among ours NPC that we havew promoted the code to a method. Just add the items it sell or it buy or how much does it charge for healing.

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

The other way of creating a NPC dialog is by adding states to the FSM.

The best way of describing a dialog is using a Finite state machine. Have a look to 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.
  • State 1 is the state where only one player can talk to NPC. Any other player that try to talk to NPC will see only the text set with addWaitMessage.
  • State -1 is used for jump from any state when the trigger is present. 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

 add(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.

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");
        }
      },
        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");
        }
      });

    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);
    }
  
  private void step_2()
    {
    StendhalRPZone zone=(StendhalRPZone)world.getRPZone(new IRPZone.ID("int_orril_jynath_house"));

    SpeakerNPC npc=npcs.get("Jynath");    
    
    /** 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");
        }
      },
        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 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);

    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);      
    }
  
  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");
          }              
        }
      });
    }
  }