Stendhal Quest Coding: Difference between revisions
Jump to navigation
Jump to search
Content deleted Content added
imported>MiguelAngelBlanchLardin No edit summary |
imported>Kribbel m replace old link |
||
| (315 intermediate revisions by 16 users not shown) | |||
| Line 1: | Line 1: | ||
{{Navigation for Stendhal Top|Contributing}} |
|||
Adding quests is perhaps the most complex step on any game. |
|||
{{Navigation for Stendhal Contributors}} |
|||
{{ TODO | Update page for changes in quest coding }} |
|||
So please before even trying, read these docs and make sure you understand the basis: |
|||
* [[HowToAddItemsStendhal|How to add items]] |
|||
* [[HowToAddMapsServerStendhal|How to add new maps to server]] |
|||
* [[HowToAddMapsServerStendhal#Adding_NPC|How to create NPC]] |
|||
It would be even better if you try yourself and play a bit with it. |
|||
{{Stendhal Quests}} |
|||
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 |
|||
__TOC__ |
|||
<pre> |
|||
package games.stendhal.server.maps.quests; |
|||
If you have ideas for new quests or are interested in helping to refine quest ideas, please have a look at the [[Stendhal Quest Contribution|Quest Contributor's Guide]] or the [[Stendhal Quest Ideas]]. |
|||
public class LookBookforCeryl implements IQuest |
|||
{ |
|||
public LookBookforCeryl(StendhalRPWorld w, StendhalRPRuleProcessor rules) |
|||
{ |
|||
} |
|||
} |
|||
</pre> |
|||
The constructor '''ALWAYS''' has two parameters: World object and Rules object. |
|||
== Before you start == |
|||
Then let see how to add a quest by example. |
|||
This page describes how to code a quest. You don't need to know a lot about Java. You should, however, already have [[Configure a development environment (IDE)|setup an IDE]] and be able to compile and start a local Stendhal server. |
|||
<pre> |
|||
package games.stendhal.server.maps.quests; |
|||
This tutorial assumes that the new quest only uses NPCs and items that already exist in Stendhal. To add a new NPC, see [[Stendhal NPC Coding]]. |
|||
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; |
|||
== Creating a quest skeleton == |
|||
import java.util.*; |
|||
This tutorial is based on the quest "Beer For Hayunn". As this quest already exists, you may want to delete the java file locally in order to follow this tutorial. |
|||
import marauroa.common.game.IRPZone; |
|||
</pre> |
|||
Quest files are put into the package games.stendhal.server.maps.quests. (If you are new to Java: this refers to the folder stendhal/src/games/stendhal/server/maps/quests). |
|||
Just java wording. Import whatever you need. |
|||
It is an accept good practique to import only what you really need. |
|||
Please create a new file in that folder called BeerForHayunn.java. (Note: The upper / lower case spelling is important, even on Microsoft Windows): |
|||
<pre> |
|||
/** |
|||
* 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. |
|||
*/ |
|||
</pre> |
|||
<source lang="java"> |
|||
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. |
|||
package games.stendhal.server.maps.quests; |
|||
import games.stendhal.server.entity.npc.*; |
|||
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. |
|||
import games.stendhal.server.entity.npc.action.*; |
|||
import games.stendhal.server.entity.npc.condition.*; |
|||
import games.stendhal.server.entity.player.*; |
|||
import java.util.*; |
|||
public class BeerForHayunn extends AbstractQuest { |
|||
<pre> |
|||
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(); |
|||
} |
|||
public static final String QUEST_SLOT = "beer_hayunn"; |
|||
</pre> |
|||
@Override |
|||
The constructor is called by the Quest system to create the quest. |
|||
public void addToWorld() { |
|||
So instead of writting the quest inside the constructor we have split it in steps. |
|||
super.addToWorld(); |
|||
} |
|||
@Override |
|||
Think of your quests as a set of steps that need to be done in order for it to be completed. |
|||
public String getSlotName() { |
|||
return QUEST_SLOT; |
|||
} |
|||
@Override |
|||
Let's see the first step: ''Talk with Ceryl to activate the quest.'' |
|||
public String getName() { |
|||
return "BeerForHayunn"; |
|||
} |
|||
public List<String> getHistory(final Player player) { |
|||
<pre> |
|||
final List<String> res = new ArrayList<String>(); |
|||
private void step_1() |
|||
return res; |
|||
} |
|||
StendhalRPZone zone=(StendhalRPZone)world.getRPZone(new IRPZone.ID("int_semos_library")); |
|||
} |
|||
</pre> |
|||
</source> |
|||
Don't worry, if you don't understand a word of this; just copy it. We will explain the important parts and extend this skeleton in the following sections. |
|||
We get the zone we are going to work in. |
|||
In order for the Stendhal server to pick up this file, it has to be registered in the file StendhalQuestSystem.java in the package games.stendhal.server.core.rp by adding two lines at the appropriate places: |
|||
Ceryl lives at Library so we get library. |
|||
<source lang="java"> |
|||
<pre> |
|||
import games.stendhal.server.maps.quests.BeerForHayunn; |
|||
SpeakerNPC npc=npcs.get("Ceryl"); |
|||
</pre> |
|||
// [...] |
|||
This works because in games/stendhal/server/maps/Semos.java we defined Ceryl NPC. |
|||
loadQuest(new BeerForHayunn()); |
|||
Now we simply get it to start adding dialogues to it. |
|||
</source> |
|||
Of course in the case of this tutorial the two lines for BeerForHayunn are already there. |
|||
<pre> |
|||
Behaviours.addQuest(npc,"I am looking for a very special #book"); |
|||
</pre> |
|||
== Teaching the NPC to talk == |
|||
There two ways of adding chat to a NPC. |
|||
The first and simpler one is using the Behaviour class. |
|||
Okay, we have now completed the preparation. Our first task is to get Hayunn to reply to the word "quest". |
|||
This class has a set of predefined triggers that you can use: |
|||
* addGreeting(npc, text)<br>Replies to anyone that greet this NPC with the given text.<br>The trigger condition is ''hi'', ''hello'', ''hola''. To start any conversation with a NPC the player '''MUST''' first greet the NPC. |
|||
* addGoodbye(npc, text)<br>It is what NPC writes when listen to '''bye''' or '''adios'''. |
|||
* addReply(npc, trigger, text)<br>Reply the attended player with text when NPC listen the keyword trigger or a word that contains the keyword. |
|||
* addQuest(npc, text)<br>Show text to player when NPC listen the keyword ''quest'' or ''task'' |
|||
* addJob(npc, text)<br>Show NPC job explained in text when listen the keyword ''job'' or ''work'' |
|||
* addHelp(npc, text)<br>When NPC listen to ''help'' or ''ayuda'' it says text. |
|||
* addSeller |
|||
* addBuyer |
|||
* addHealer<br>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. |
|||
Therefore we add a new method called prepareQuestStep() at the end of the file BeerForHayunn, just above the last closing "}". This method makes Hayunn reply to the word "quest" with the answer "My mouth is dry, but I can't be seen to abandon this teaching room!" |
|||
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. |
|||
<source lang="java"> |
|||
The other way of creating a NPC dialog is by adding states to the FSM. |
|||
public void prepareQuestStep() { |
|||
// get a reference to the Hayunn npc |
|||
The best way of describing a dialog is using a [http://en.wikipedia.org/wiki/Finite_state_machine Finite state machine]. Have a look to the link to make sure you understand the idea. |
|||
SpeakerNPC npc = npcs.get("Hayunn Naratha"); |
|||
// add a reply on the trigger phrase "quest" to Hayunn |
|||
The first set of rules about states is that: |
|||
npc.addReply("quest", "My mouth is dry, but I can't be seen to abandon this teaching room!"); |
|||
* 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. |
|||
</source> |
|||
* 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. |
|||
There is one little step left before we can test it: We need to tell the server to execute our new method. There is already a method called "addToWorld" which will be executed on server start. So we add a call to our method in "addToWorld": |
|||
To add a state to NPC we use the method: |
|||
<source lang="java"> |
|||
public void add(int state, String trigger, ChatCondition condition, int next_state, String reply, ChatAction action) |
|||
@Override |
|||
public void addToWorld() { |
|||
super.addToWorld(); |
|||
prepareQuestStep(); |
|||
} |
|||
</source> |
|||
Okay, all done? Please start the server (depending on whether you are using an IDE or not you might have to compile or build first). Go to Hayunn and say "quest", after starting the conversation with "hi". He should now respond with the sentence "My mouth is dry, but I can't be seen to abandon this teaching room!". |
|||
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. |
|||
== Commonly used conversation phrases == |
|||
Let's see how it works. |
|||
Good, Hayunn now replies to the trigger "quest". He does not, however, reply to "task". All other NPCs accept both words as synonym. A simple solution would be to add a second ''npc.addReply'' line. But there is a better way which makes it very easy to add additional synonyms later. We predefined lists of commonly used [https://github.com/arianne/stendhal/blob/master/src/games/stendhal/server/entity/npc/ConversationPhrases.java ConversationPhrases]. If there are already conversation phrases defined for the triggers that you would like to add, you should use the phrases. |
|||
First we need to create a message to greet the player and attend it. |
|||
We add a hi event |
|||
Let's adjust the above sample by using ''ConversationPhrases.QUEST_MESSAGES'' instead of the hard coded word "quest": |
|||
add(0, "hi", null, 1, "Welcome player!", null) |
|||
<source lang="java"> |
|||
State 0 is the initial state, so once NPC is in that state and listen "hi", |
|||
public void prepareQuestStep() { |
|||
it will say "Welcome player!" and pass to state 1. |
|||
// get a reference to the Hayunn npc |
|||
We can personalize more the message like: |
|||
SpeakerNPC npc = npcs.get("Hayunn Naratha"); |
|||
<pre> |
|||
add(0, "hi", null, 1, null, new ChatAction() |
|||
{ |
|||
public void fire(Player player, String text, SpeakerNPC engine) |
|||
{ |
|||
engine.say("Welcome "+player.getName()+"!"); |
|||
} |
|||
}) |
|||
</pre> |
|||
// add a reply on quest related trigger phrases to Hayunn |
|||
If we add these two states the NPC will choose randomly between them because both of them are suitable to start a conversation. |
|||
npc.addReply(ConversationPhrases.QUEST_MESSAGES, |
|||
"My mouth is dry, but I can't be seen to abandon this teaching room!"); |
|||
} |
|||
</source> |
|||
Please compile and restart your server. Hayunn should now respond to "quest", "task", and "favor", after you started the talk by saying "hi". |
|||
Let's add more states. |
|||
== Blue trigger words == |
|||
Now let's add some options when player is in state 1 like job, offer, buy, sell, etc. |
|||
As you probably know NPCs can say words in blue, words that they expect to be repeated by the player. We want to add such words for "beer" and "tavern". As we have done before, we will add npc.addReply lines for those words. |
|||
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) |
|||
So, how do we get the words colored blue? Simple, add a "#" in front of them. If you actually want to include a #-character, you need to repeat it. |
|||
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. |
|||
<source lang="java"> |
|||
Easy, isn't it? |
|||
public void prepareQuestStep() { |
|||
// get a reference to the Hayunn npc |
|||
Now let's add something harder. |
|||
SpeakerNPC npc = npcs.get("Hayunn Naratha"); |
|||
Let's make the NPC ask a question and let's expect a reply to the question. |
|||
// ask for a beer and explain it |
|||
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. |
|||
npc.addReply(ConversationPhrases.QUEST_MESSAGES, |
|||
"Please bring me a #beer."); |
|||
// explain blue words |
|||
add(1, "fun", null, 50, "Do you want me to convert you in a heady bare elf?", null) |
|||
npc.addReply("beer", "Margaret sells beers in the #tavern."); |
|||
npc.addReply("tavern", "If you don't know where the inn is, you could ask old Monogenes."); |
|||
// an example for escaping # |
|||
If player says ''fun'' then NPC will ask the question. Note that new state is 50 instead of 1. |
|||
npc.addReply("trading", "http://stendhal.game-host.org/wiki/index.php/StendhalFAQ##Trading"); |
|||
Now let's add two new states for the reply: yes and no. |
|||
} |
|||
<pre> |
|||
</source> |
|||
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); |
|||
</pre> |
|||
You know the drill: Compile, restart and try it out. |
|||
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!, 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> |
|||
== Second Part of this Tutorial == |
|||
See? This way is simpler and you save having to adding two states. |
|||
Congratulations if you made it this far. You are now able to code basic dialogs with NPCs. The next section of this tutorial will describe advanced techniques. Please make sure the steps on this page work before you continue to [[Stendhal Quest Coding - Part 2|the second part of this tutorial]]. |
|||
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. |
|||
[[Category:Stendhal]] |
|||
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> |
|||
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. |
|||
<pre> |
|||
/** 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"); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
</pre> |
|||