Stendhal Quest Coding
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", 1, "I work as a part time example showman",null) add(1, "offer", 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.
add(1, "buy", 20, null, new ChatAction()
{
public void fire(Player player, String text, SpeakerNPC engine)
{
int i=text.indexOf(" ");
String item=text.substring(i+1);
if(item.equals("sword"))
{
engine.say(item+" costs 10 GP. Do you want to buy?");
}
else
{
engine.say("Sorry, I don't sell "+item);
engine.setActualState(1);
}
}
});
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.
add(20, "yes", 1, null, null); // See Behaviours.java for exact code. add(20, "no", 1, null, null); // See Behaviours.java for exact code.
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.
add(-1, "bye", 0, "Bye!.", null);
We use -1 as a wildcard, so it text is bye the transition.happens.
/** 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");
}
}
});
}
}