Difference between revisions of "How to Make a Quest"
Bluekirby0 (Talk | contribs) |
Bluekirby0 (Talk | contribs) |
||
Line 205: | Line 205: | ||
By making this line an elseif, it has made it impossible for this NPC to accept the trade for our quest unless the quest "Flyers for Regine" is NOT even active! This is bad, because our player could have unwittingly picked up both quests, and decided to do Regine's quest later. Our NPC won't accept the trade (or even acknowledge it) for our quest in this case! The correct fix for this would be to make it an "if" statement and ensure that it is impossible for both trades to be done at the same time so both conditions cannot be satisfied, causing a potential conflict in reactions. The previous quest trade is scripted such that only one of a specific item can be traded for the conditions to be satisfied, so we should make sure that is done here too! | By making this line an elseif, it has made it impossible for this NPC to accept the trade for our quest unless the quest "Flyers for Regine" is NOT even active! This is bad, because our player could have unwittingly picked up both quests, and decided to do Regine's quest later. Our NPC won't accept the trade (or even acknowledge it) for our quest in this case! The correct fix for this would be to make it an "if" statement and ensure that it is impossible for both trades to be done at the same time so both conditions cannot be satisfied, causing a potential conflict in reactions. The previous quest trade is scripted such that only one of a specific item can be traded for the conditions to be satisfied, so we should make sure that is done here too! | ||
+ | |||
+ | Since we are changing the "elseif" to an "if" we also need to close the previous "if". Make sure and add an "end" before the line as well. | ||
For reference, to fix the above line, it should read: | For reference, to fix the above line, it should read: | ||
<pre> | <pre> | ||
+ | end | ||
if (player:getQuestStatus(SANDORIA,LUFET_S_LAKE_SALT) == 1) then | if (player:getQuestStatus(SANDORIA,LUFET_S_LAKE_SALT) == 1) then | ||
</pre> | </pre> | ||
Line 386: | Line 389: | ||
You get the idea by now... | You get the idea by now... | ||
+ | |||
+ | |||
== Making a Quest from Scratch == | == Making a Quest from Scratch == | ||
+ | Okay, now we know absolutely everything there is to know about the scripts involved with writing quests! Erm...okay, not really. I, personally, learn by doing, though. We can reference additional implemented [[Quests]] as we find other things we need examples of how to do. That said, let's get to work! First, check [[Quests]] and make sure you choose one that has not been implemented yet. The list is updated daily, so it should be accurate. If you are worried, you can update your local copy via svn and check scripts/globals/quests.lua for bleeding-edge on what is implemented, and check the [http://dspt.freeforums.org/scripting-f12.html Scripting] section on the forums to see if anyone has submitted the particular quest recently. In my case, I'm going to choose [http://wiki.ffxiclopedia.org/wiki/The_Rumor "The Rumor"] as my first quest! | ||
+ | |||
+ | The quest is started by Novalmauge in Bostaunieux Oubliette, so we find his script in scripts/zones/Bostaunieux_Oubliette/npcs/Novalmauge.lua | ||
+ | |||
+ | Inside we find: | ||
+ | |||
+ | <pre> | ||
+ | ----------------------------------- | ||
+ | -- Area: Bostaunieux Obliette | ||
+ | -- NPC: Novalmauge | ||
+ | -- Involved in Quest: The Holy Crest, Trouble at the Sluice | ||
+ | -- @zone 167 | ||
+ | -- @pos 70 -24 21 | ||
+ | ----------------------------------- | ||
+ | package.loaded["scripts/zones/Bostaunieux_Oubliette/TextIDs"] = nil; | ||
+ | ----------------------------------- | ||
+ | |||
+ | require("scripts/globals/settings"); | ||
+ | require("scripts/globals/titles"); | ||
+ | require("scripts/globals/keyitems"); | ||
+ | require("scripts/globals/shop"); | ||
+ | require("scripts/globals/quests"); | ||
+ | require("scripts/zones/Bostaunieux_Oubliette/TextIDs"); | ||
+ | |||
+ | ----------------------------------- | ||
+ | -- onTrade Action | ||
+ | ----------------------------------- | ||
+ | |||
+ | function onTrade(player,npc,trade) | ||
+ | |||
+ | if(player:getVar("troubleAtTheSluiceVar") == 2) then | ||
+ | if(trade:hasItemQty(959,1) and trade:getItemCount() == 1) then -- Trade Dahlia | ||
+ | player:startEvent(0x0011); | ||
+ | end | ||
+ | end | ||
+ | end; | ||
+ | |||
+ | ----------------------------------- | ||
+ | -- onTrigger Action | ||
+ | ----------------------------------- | ||
+ | |||
+ | function onTrigger(player,npc) | ||
+ | |||
+ | theRumor = player:getQuestStatus(SANDORIA,THE_RUMOR); | ||
+ | troubleAtTheSluice = player:getQuestStatus(SANDORIA,TROUBLE_AT_THE_SLUICE); | ||
+ | TheHolyCrest = player:getVar("TheHolyCrest_Event"); | ||
+ | tatsVar = player:getVar("troubleAtTheSluiceVar"); | ||
+ | |||
+ | -- The Holy Crest Quest | ||
+ | if(TheHolyCrest == 1) then | ||
+ | player:startEvent(0x0006); | ||
+ | elseif(TheHolyCrest == 2) then | ||
+ | player:startEvent(0x0007); | ||
+ | -- Trouble at the Sluice Quest | ||
+ | elseif(tatsVar == 1) then | ||
+ | player:startEvent(0x000f); | ||
+ | player:setVar("troubleAtTheSluiceVar",2); | ||
+ | elseif(tatsVar == 2) then | ||
+ | player:startEvent(0x0010); | ||
+ | -- The rumor Quest | ||
+ | elseif(theRumor == QUEST_AVAILABLE and player:getFameLevel(SANDORIA) >= 3) then | ||
+ | player:startEvent(0x000d); | ||
+ | elseif(theRumor == QUEST_ACCEPTED) then | ||
+ | player:startEvent(0x000b); | ||
+ | else | ||
+ | player:startEvent(0x000a); -- Standard dialog | ||
+ | end | ||
+ | end; | ||
+ | |||
+ | ----------------------------------- | ||
+ | -- onEventUpdate | ||
+ | ----------------------------------- | ||
+ | |||
+ | function onEventUpdate(player,csid,option) | ||
+ | --printf("CSID: %u",csid); | ||
+ | --printf("RESULT: %u",option); | ||
+ | end; | ||
+ | |||
+ | ----------------------------------- | ||
+ | -- onEventFinish | ||
+ | ----------------------------------- | ||
+ | |||
+ | function onEventFinish(player,csid,option) | ||
+ | --printf("CSID: %u",csid); | ||
+ | --printf("RESULT: %u",option); | ||
+ | |||
+ | if(csid == 0x0006) then | ||
+ | player:setVar("TheHolyCrest_Event",2); | ||
+ | elseif(csid == 0x0011) then | ||
+ | player:tradeComplete(); | ||
+ | player:addKeyItem(NEUTRALIZER); | ||
+ | player:messageSpecial(KEYITEM_OBTAINED,NEUTRALIZER); | ||
+ | player:setVar("troubleAtTheSluiceVar",0); | ||
+ | elseif(csid == 0x000d and option == 1) then | ||
+ | player:addQuest(SANDORIA,THE_RUMOR); | ||
+ | elseif(csid == 0x000c) then | ||
+ | if (player:getFreeSlotsCount() == 0) then | ||
+ | player:messageSpecial(ITEM_CANNOT_BE_OBTAINED,4853); -- Scroll of Drain | ||
+ | else | ||
+ | player:tradeComplete(); | ||
+ | player:addItem(4853); | ||
+ | player:messageSpecial(ITEM_OBTAINED, 4853); -- Scroll of Drain | ||
+ | player:setVar("sharpeningTheSwordCS",0); | ||
+ | player:addFame(SANDORIA,SAN_FAME*30); | ||
+ | player:completeQuest(SANDORIA,THE_RUMOR); | ||
+ | end | ||
+ | end | ||
+ | |||
+ | end; | ||
+ | </pre> | ||
+ | |||
+ | === Modifying the Header Comments === | ||
+ | Okay, so first we want to make a note of the quest we are addding to the NPC. In the header comments add the line: | ||
+ | |||
+ | <pre>-- Starts and Finishes Quest: The Rumor</pre> | ||
+ | |||
+ | So someone else will understand why you added the code in later. | ||
+ | |||
+ | === Adjusting Includes === | ||
+ | <pre> | ||
+ | package.loaded["scripts/zones/Bostaunieux_Oubliette/TextIDs"] = nil; | ||
+ | ----------------------------------- | ||
+ | |||
+ | require("scripts/globals/settings"); | ||
+ | require("scripts/globals/titles"); | ||
+ | require("scripts/globals/keyitems"); | ||
+ | require("scripts/globals/shop"); | ||
+ | require("scripts/globals/quests"); | ||
+ | require("scripts/zones/Bostaunieux_Oubliette/TextIDs"); | ||
+ | </pre> | ||
+ | |||
+ | It is laid out a little differently than our example quest, but it does the same thing. Let's go ahead and clean up anything we might have leftover from previous calls to "quests", since we are now going to use this guy to start one. Now we should have: | ||
+ | |||
+ | <pre> | ||
+ | package.loaded["scripts/globals/quests"] = nil; | ||
+ | package.loaded["scripts/zones/Bostaunieux_Oubliette/TextIDs"] = nil; | ||
+ | ----------------------------------- | ||
+ | |||
+ | require("scripts/globals/settings"); | ||
+ | require("scripts/globals/titles"); | ||
+ | require("scripts/globals/keyitems"); | ||
+ | require("scripts/globals/shop"); | ||
+ | require("scripts/globals/quests"); | ||
+ | require("scripts/zones/Bostaunieux_Oubliette/TextIDs"); | ||
+ | </pre> | ||
+ | |||
+ | Note that the only order requirements here are that any package.loaded lines come before the equivalent require line. | ||
+ | |||
+ | A fairly simple change to start out with! | ||
+ | |||
+ | === Tweaking onTrade === | ||
+ | Let's take a look at FFXIclopedia again. See if the quest involves a trade at any point in time. We see the step "Trade Novalmauge a vial of Beastman Blood." on the quest, so we have work to do. | ||
+ | |||
+ | First, let's look up "Beastman Blood" in [[Basic Item IDs]]. It looks like we have a value of "930", so keep track of that number. Our existing onTrade code is as follows: | ||
+ | |||
+ | <pre> | ||
+ | if(player:getVar("troubleAtTheSluiceVar") == 2) then | ||
+ | if(trade:hasItemQty(959,1) and trade:getItemCount() == 1) then -- Trade Dahlia | ||
+ | player:startEvent(0x0011); | ||
+ | end | ||
+ | end | ||
+ | </pre> | ||
+ | |||
+ | Which is a trade involved with a separate quest. We can add in our own code either above or below this, but make sure you don't accidentally put it between one of those two ifs and its matching end statement. Now let's start adding logic! Borrowing from our previous example, we can work out the following: | ||
+ | |||
+ | <pre> | ||
+ | if(player:getQuestStatus(SANDORIA,THE_RUMOR) == QUEST_ACCEPTED) then | ||
+ | </pre> | ||
+ | |||
+ | We want to make sure the trade is only processed if we have started the appropriate quest. Note that QUEST_ACCEPTED is an alias for "1" and means the same thing. We'll use QUEST_ACCEPTED to make it more obvious to anyone that goes behind us and reads it. | ||
+ | |||
+ | <pre> | ||
+ | count = trade:getItemCount(); | ||
+ | </pre> | ||
+ | |||
+ | Like in the previous scenario, we want to make sure someone can't trade and lose items unrelated to the quest, so we will keep track of the total count of items to check later. | ||
+ | |||
+ | <pre> | ||
+ | BeastBlood = trade:hasItemQty(930,1) | ||
+ | <pre> | ||
+ | |||
+ | Remember that number I told you to remember? Here it is! That is how we check exactly what item is being traded. | ||
+ | |||
+ | Make sure we have at least one beastman blood. Note that "BeastBlood" above can be nearly anything you want as long as you use the same name later. It is best to make it obvious what it is used for to keep the script readable. It will actually store a returned true/false value from the trade:hasItemQty function. | ||
+ | |||
+ | <pre> | ||
+ | if(BeastBlood == true and count == 1) then | ||
+ | </pre> | ||
+ | |||
+ | Make sure we satisfy both conditions that: | ||
+ | At least one beastman blood item is being traded. | ||
+ | Exactly one item is being traded. | ||
+ | |||
+ | If we don't do this, people could accidentally trade additional items and lost them permanently. | ||
+ | |||
+ | |||
+ | <pre> | ||
+ | player:startEvent(0x000c); | ||
+ | </pre> | ||
+ | |||
+ | We will play the cutscene that is supposed to activate upon completion of the quest. I am not going to activate the rewards in this section as I prefer to give them at the end of the cutscene to avoid potential exploits, so let's close up this section. | ||
+ | |||
+ | <pre> | ||
+ | end | ||
+ | end | ||
+ | </pre> |
Revision as of 04:52, 6 April 2012
THIS PAGE IS A DRAFT AND DOES NOT NECESSARILY ACCURATELY DESCRIBE THE BEST WAY TO WRITE A QUEST! EXPERIENCED QUEST-WRITERS SHOULD REVIEW THIS PAGE FOR ACCURACY AND MODIFY IT AS NEEDED!
This is a guide explaining how to script quests for use with DarkStar.
The Basics
Quests are scripted by modifying the relevant scripts for the NPCs involved in the quests. NPC scripts are written in LUA, but you don't really need to know LUA before you start to successfully script a quest.
The easiest way to learn is by opening up some scripts for NPCs involved in Quests that are already implemented. You can cross reference FFXIclopedia for information about the particular quest you are studying to help you understand better what is going on in the script. I'll walk you through examining a quest.
You may also want to install a text editor with syntax highlighting, such as Notepad++. That will help you by coloring the code in ways that make it easier to read. You can also use Notepad on windows, but it lacks these helpful features.
Studying an Existing Quest
For an example, I'll walk you through examining a fairly simple quest, Lufet's Lake Salt. Go ahead and open that link so you can see the details of the quest.
The first thing you need to know when looking at the code is where the NPC scripts are stored, and how to find the one you need. NPC scripts are stored in scripts/zones/(area_name)/npcs where (area_name) is the zone the NPC you are looking for occupies. Looking at FFXIclopedia, the only related npc for the quest is Nogelle in Port San d'Oria, so we can navigate to scripts/zones/Port_San_dOria/npcs and find the script Nogelle.lua and open it in a text editor.
For simplifying the tutorial, I'll add the NPC script in below for reference:
----------------------------------- -- Area: Port San d'Oria -- NPC: Nogelle -- Starts Lufet's Lake Salt ----------------------------------- package.loaded["scripts/globals/quests"] = nil; require("scripts/globals/quests"); require("scripts/globals/titles"); package.loaded["scripts/zones/Port_San_dOria/TextIDs"] = nil; require("scripts/zones/Port_San_dOria/TextIDs"); ----------------------------------- -- onTrade Action ----------------------------------- function onTrade(player,npc,trade) -- "Flyers for Regine" conditional script FlyerForRegine = player:getQuestStatus(SANDORIA,FLYERS_FOR_REGINE); if (FlyerForRegine == 1) then count = trade:getItemCount(); MagicFlyer = trade:hasItemQty(MagicmartFlyer,1); if (MagicFlyer == true and count == 1) then player:messageSpecial(FLYER_REFUSED); end elseif (player:getQuestStatus(SANDORIA,LUFET_S_LAKE_SALT) == 1) then count = trade:getItemCount(); LufetSalt = trade:hasItemQty(1019,3); if (LufetSalt == true and count == 3) then player:tradeComplete(); player:addFame(SANDORIA,SAN_FAME*30); player:addGil(GIL_RATE*600); player:setTitle(BEAN_CUISINE_SALTER); player:completeQuest(SANDORIA,LUFET_S_LAKE_SALT); player:startEvent(0x000b); end end end; ----------------------------------- -- onTrigger Action ----------------------------------- function onTrigger(player,npc) LufetsLakeSalt = player:getQuestStatus(SANDORIA,LUFET_S_LAKE_SALT); if (LufetsLakeSalt == 0) then player:startEvent(0x000c); elseif (LufetsLakeSalt == 1) then player:startEvent(0x000a); elseif (LufetsLakeSalt == 2) then player:startEvent(0x020a); end end; ----------------------------------- -- onEventUpdate ----------------------------------- function onEventUpdate(player,csid,option) --printf("CSID: %u",csid); --printf("RESULT: %u",option); end; ----------------------------------- -- onEventFinish ----------------------------------- function onEventFinish(player,csid,option) --printf("CSID: %u",csid); --printf("RESULT: %u",option); if (csid == 0x000c and option == 1) then player:addQuest(SANDORIA,LUFET_S_LAKE_SALT); elseif (csid == 0x000b) then player:messageSpecial(GIL_OBTAINED,GIL_RATE*600); end end;
Breaking it Down
Now, if you've never done any programming before, this will probably look overwhelming. I'll break down what is going on here.
----------------------------------- -- Area: Port San d'Oria -- NPC: Nogelle -- Starts Lufet's Lake Salt -----------------------------------
In LUA, anything starting with two "hyphens" or "minus signs" is a comment. In other words, nothing on the line is read by the interpreter. We put these in for the benefit of people reading the code behind us. Note that this NPC has a comment "Starts Lufet's Lake Salt". Any time you modify an NPC, you should make a note of which quests you have implemented by adding a line like that one as a comment. It helps others later down the road determine what all the extra code you added in is used for without having to reference another source.
package.loaded["scripts/globals/quests"] = nil; require("scripts/globals/quests"); require("scripts/globals/titles"); package.loaded["scripts/zones/Port_San_dOria/TextIDs"] = nil; require("scripts/zones/Port_San_dOria/TextIDs");
The "package.loaded" lines are used to clean up any leftover data from other scripts. Here we see that we need to clear "quests" and "Port San d'Oria TextIDs" before using them. Exactly which packages need to be cleared and which do not depends on whether data from a previous use can cause problems if loaded into the current quest. In this case, "titles" do not need to be cleared, because it does not contain any volatile data. If you know you need a script, but don't know whether or not to "clear" it first, look at another NPC script that uses it for reference.
The "require" lines are used to load functions and variables for use with the NPC. For this NPC, we are loading "quests" since we are using quest-related functions, "titles" since we need a function to grant a title to the player upon completion of the quest, and "Port San d'Oria TextIDs" which are needed for every scripted NPC in Port San d'Oria, and is how we get our NPCs to speak, by telling the client which TextIDs to display while talking to the NPC.
function onTrade(player,npc,trade) -- "Flyers for Regine" conditional script FlyerForRegine = player:getQuestStatus(SANDORIA,FLYERS_FOR_REGINE); if (FlyerForRegine == 1) then count = trade:getItemCount(); MagicFlyer = trade:hasItemQty(MagicmartFlyer,1); if (MagicFlyer == true and count == 1) then player:messageSpecial(FLYER_REFUSED); end elseif (player:getQuestStatus(SANDORIA,LUFET_S_LAKE_SALT) == 1) then count = trade:getItemCount(); LufetSalt = trade:hasItemQty(1019,3); if (LufetSalt == true and count == 3) then player:tradeComplete(); player:addFame(SANDORIA,SAN_FAME*30); player:addGil(GIL_RATE*600); player:setTitle(BEAN_CUISINE_SALTER); player:completeQuest(SANDORIA,LUFET_S_LAKE_SALT); player:startEvent(0x000b); end end end;
This is where things get a bit more complicated. What we have above is how the NPC reacts when you attempt to trade an item to them. In this case, there are actually TWO quests that have a triggered response upon trade, one of which being Flyers for Regine and the other being our quest.
Breaking down onTrade
FlyerForRegine = player:getQuestStatus(SANDORIA,FLYERS_FOR_REGINE);
Every time you attempt to trade with Nogelle, this line will check to see if the quest "Flyers for Regine" is active.
if (FlyerForRegine == 1) then count = trade:getItemCount();
If it is, then count how many items have been traded.
MagicFlyer = trade:hasItemQty(MagicmartFlyer,1);
Next, we see if one of the items you traded is at least one "Magicmart Flyer".
if (MagicFlyer == true and count == 1) then player:messageSpecial(FLYER_REFUSED);
Now me ensure that both conditions are true: At least one "Magic Mart Flyer" was traded. Only one item total was traded.
If both conditions are met, then we retrieve a message "FLYER_REFUSED" indicating that this NPC will not take the flyer.
end
We need to finalize our "if" statement so the script knows that the next line is not part of it.
Breaking down onTrade Part 2
Now, we've completed that chunk of the logic, but we still have more to go.
elseif (player:getQuestStatus(SANDORIA,LUFET_S_LAKE_SALT) == 1) then
Okay, on first glance, this looks good. We've passed the logic for when "Flyers for Regine" is active. "elseif" is used for when the previous condition is not true, though. Wait, then that means we just discovered a bug!
By making this line an elseif, it has made it impossible for this NPC to accept the trade for our quest unless the quest "Flyers for Regine" is NOT even active! This is bad, because our player could have unwittingly picked up both quests, and decided to do Regine's quest later. Our NPC won't accept the trade (or even acknowledge it) for our quest in this case! The correct fix for this would be to make it an "if" statement and ensure that it is impossible for both trades to be done at the same time so both conditions cannot be satisfied, causing a potential conflict in reactions. The previous quest trade is scripted such that only one of a specific item can be traded for the conditions to be satisfied, so we should make sure that is done here too!
Since we are changing the "elseif" to an "if" we also need to close the previous "if". Make sure and add an "end" before the line as well.
For reference, to fix the above line, it should read:
end if (player:getQuestStatus(SANDORIA,LUFET_S_LAKE_SALT) == 1) then
Now, continuing on!
count = trade:getItemCount();
Once again, this gets the total quantity of items traded.
LufetSalt = trade:hasItemQty(1019,3);
We check that at least three "Lufet Salt" are included in the trade.
if (LufetSalt == true and count == 3) then
Now me ensure that both conditions are true: At least three "Lufet Salt" were traded. Only three items total were traded.
player:tradeComplete();
The trade is accepted, the items taken.
player:addFame(SANDORIA,SAN_FAME*30); player:addGil(GIL_RATE*600); player:setTitle(BEAN_CUISINE_SALTER);
Our rewards for this quest: 30 fame added to San d'Oria, 600 gil, and a lovely new title "Bean Cuisine Salter" is set to our player
player:completeQuest(SANDORIA,LUFET_S_LAKE_SALT); player:startEvent(0x000b);
The quest is marked as complete, and a relevant event is triggered.
end end end;
Make sure we close that if! We are completely done with the function onTrade now, so let's go ahead and close the rest out, too. Note how each "end" is indented to match the "if" it goes with, and how each line of code within the "if" statement is indented one step further than the "if". This is done to make the code more readable, and you should follow this practice as well. It helps both the person writing the code and anyone who comes behind them later with reading it.
Breaking down onTrigger
function onTrigger(player,npc) LufetsLakeSalt = player:getQuestStatus(SANDORIA,LUFET_S_LAKE_SALT); if (LufetsLakeSalt == 0) then player:startEvent(0x000c); elseif (LufetsLakeSalt == 1) then player:startEvent(0x000a); elseif (LufetsLakeSalt == 2) then player:startEvent(0x020a); end end;
onTrigger events activate when you "activate" an NPC (generally by getting close to one and clicking on them, but can also be accomplished by selecting them with "tab" or a controller, and then hitting "enter" or "confirm". We mostly use "events" to progress things when we activate an NPC, so the list of Event IDs may be helpful in finding out which events belong to which NPCs (though the list is currently missing som stuff, it should assist you in most cases).
LufetsLakeSalt = player:getQuestStatus(SANDORIA,LUFET_S_LAKE_SALT);
Checks to see what the status of the quest "Lufet's Lake Salt" is.
if (LufetsLakeSalt == 0) then player:startEvent(0x000c);
If we have not started the quest, then it plays the event "0x0c" which is the trigger event for the quest. On quests with fame requirements, we should have an additional fame test and even to use when fame is not high enough here.
elseif (LufetsLakeSalt == 1) then player:startEvent(0x000a);
If we have started the quest, but have yet to complete it, event (0x0a) will play instead.
elseif (LufetsLakeSalt == 2) then player:startEvent(0x020a);
After the quest has been completed, if you talk to the npc again, they will trigger event (0x020a)
end end;
I'm sure you know the drill by now.
Note that the event that triggers UPON completion of the quest is not included here. That is because we activated it above with the completion of the successful trade with the code player:startEvent(0x000b);
Using onEventUpdate
function onEventUpdate(player,csid,option) --printf("CSID: %u",csid); --printf("RESULT: %u",option); end;
Note that the only code contained in this function is commented out (those two hyphens at the beginning of the line make it a comment). This is because these are used as debugging output to assist in problems with modifying the script, but cause a lot of normally useless junk to display on the server console. If you are testing your quest script on your own server, try un-commenting these lines and see what happens! Do not submit scripts with these lines not commented, though. It causes unnecessary load on the server and makes it harder to find more critical problems.
Breaking down onEventFinish
function onEventFinish(player,csid,option) --printf("CSID: %u",csid); --printf("RESULT: %u",option); if (csid == 0x000c and option == 1) then player:addQuest(SANDORIA,LUFET_S_LAKE_SALT); elseif (csid == 0x000b) then player:messageSpecial(GIL_OBTAINED,GIL_RATE*600); end end;
This function triggers at the end of every "event" or cutscene started by the NPC. Let's break it down!
--printf("CSID: %u",csid); --printf("RESULT: %u",option);
As above, we have our debugging lines that we can un-comment for quest testing. Let's skip that and move on to the meat of the function.
if (csid == 0x000c and option == 1) then
Okay, here we see if the cutscene is (0x0c) which should look familiar. That is the cutscene that plays when you talk to Nogelle and you do not have the quest activated.
player:addQuest(SANDORIA,LUFET_S_LAKE_SALT);
If that is the case, then here we trigger the quest (which will in turn change what event Nogelle will use when talking to her).
elseif (csid == 0x000b) then
Another familiar looking cutscene! This one is the one that is activated upon a successful trade of 3 Lufet Salts.
player:messageSpecial(GIL_OBTAINED,GIL_RATE*600);
After that event, we give a message to the player about their gil reward. Note that this does not actually grant gil to the player, as that is done above, BEFORE the cutscene is actually triggered. If it is possible to cancel the event, you may want to consider giving the rewards upon completion of the event here rather than before activating the event.
Also, do not grant a reward, activate an event, and then mark the quest as complete upon completion of the event, or else someone could exploit your quest for the reward as many times as they can satisfy the conditions. It isn't too game-breaking in this case, since you would still need 3 Lufet Salt each time, but for some quests, all you need to do to complete it is to talk to someone.
end end;
You get the idea by now...
Making a Quest from Scratch
Okay, now we know absolutely everything there is to know about the scripts involved with writing quests! Erm...okay, not really. I, personally, learn by doing, though. We can reference additional implemented Quests as we find other things we need examples of how to do. That said, let's get to work! First, check Quests and make sure you choose one that has not been implemented yet. The list is updated daily, so it should be accurate. If you are worried, you can update your local copy via svn and check scripts/globals/quests.lua for bleeding-edge on what is implemented, and check the Scripting section on the forums to see if anyone has submitted the particular quest recently. In my case, I'm going to choose "The Rumor" as my first quest!
The quest is started by Novalmauge in Bostaunieux Oubliette, so we find his script in scripts/zones/Bostaunieux_Oubliette/npcs/Novalmauge.lua
Inside we find:
----------------------------------- -- Area: Bostaunieux Obliette -- NPC: Novalmauge -- Involved in Quest: The Holy Crest, Trouble at the Sluice -- @zone 167 -- @pos 70 -24 21 ----------------------------------- package.loaded["scripts/zones/Bostaunieux_Oubliette/TextIDs"] = nil; ----------------------------------- require("scripts/globals/settings"); require("scripts/globals/titles"); require("scripts/globals/keyitems"); require("scripts/globals/shop"); require("scripts/globals/quests"); require("scripts/zones/Bostaunieux_Oubliette/TextIDs"); ----------------------------------- -- onTrade Action ----------------------------------- function onTrade(player,npc,trade) if(player:getVar("troubleAtTheSluiceVar") == 2) then if(trade:hasItemQty(959,1) and trade:getItemCount() == 1) then -- Trade Dahlia player:startEvent(0x0011); end end end; ----------------------------------- -- onTrigger Action ----------------------------------- function onTrigger(player,npc) theRumor = player:getQuestStatus(SANDORIA,THE_RUMOR); troubleAtTheSluice = player:getQuestStatus(SANDORIA,TROUBLE_AT_THE_SLUICE); TheHolyCrest = player:getVar("TheHolyCrest_Event"); tatsVar = player:getVar("troubleAtTheSluiceVar"); -- The Holy Crest Quest if(TheHolyCrest == 1) then player:startEvent(0x0006); elseif(TheHolyCrest == 2) then player:startEvent(0x0007); -- Trouble at the Sluice Quest elseif(tatsVar == 1) then player:startEvent(0x000f); player:setVar("troubleAtTheSluiceVar",2); elseif(tatsVar == 2) then player:startEvent(0x0010); -- The rumor Quest elseif(theRumor == QUEST_AVAILABLE and player:getFameLevel(SANDORIA) >= 3) then player:startEvent(0x000d); elseif(theRumor == QUEST_ACCEPTED) then player:startEvent(0x000b); else player:startEvent(0x000a); -- Standard dialog end end; ----------------------------------- -- onEventUpdate ----------------------------------- function onEventUpdate(player,csid,option) --printf("CSID: %u",csid); --printf("RESULT: %u",option); end; ----------------------------------- -- onEventFinish ----------------------------------- function onEventFinish(player,csid,option) --printf("CSID: %u",csid); --printf("RESULT: %u",option); if(csid == 0x0006) then player:setVar("TheHolyCrest_Event",2); elseif(csid == 0x0011) then player:tradeComplete(); player:addKeyItem(NEUTRALIZER); player:messageSpecial(KEYITEM_OBTAINED,NEUTRALIZER); player:setVar("troubleAtTheSluiceVar",0); elseif(csid == 0x000d and option == 1) then player:addQuest(SANDORIA,THE_RUMOR); elseif(csid == 0x000c) then if (player:getFreeSlotsCount() == 0) then player:messageSpecial(ITEM_CANNOT_BE_OBTAINED,4853); -- Scroll of Drain else player:tradeComplete(); player:addItem(4853); player:messageSpecial(ITEM_OBTAINED, 4853); -- Scroll of Drain player:setVar("sharpeningTheSwordCS",0); player:addFame(SANDORIA,SAN_FAME*30); player:completeQuest(SANDORIA,THE_RUMOR); end end end;
Modifying the Header Comments
Okay, so first we want to make a note of the quest we are addding to the NPC. In the header comments add the line:
-- Starts and Finishes Quest: The Rumor
So someone else will understand why you added the code in later.
Adjusting Includes
package.loaded["scripts/zones/Bostaunieux_Oubliette/TextIDs"] = nil; ----------------------------------- require("scripts/globals/settings"); require("scripts/globals/titles"); require("scripts/globals/keyitems"); require("scripts/globals/shop"); require("scripts/globals/quests"); require("scripts/zones/Bostaunieux_Oubliette/TextIDs");
It is laid out a little differently than our example quest, but it does the same thing. Let's go ahead and clean up anything we might have leftover from previous calls to "quests", since we are now going to use this guy to start one. Now we should have:
package.loaded["scripts/globals/quests"] = nil; package.loaded["scripts/zones/Bostaunieux_Oubliette/TextIDs"] = nil; ----------------------------------- require("scripts/globals/settings"); require("scripts/globals/titles"); require("scripts/globals/keyitems"); require("scripts/globals/shop"); require("scripts/globals/quests"); require("scripts/zones/Bostaunieux_Oubliette/TextIDs");
Note that the only order requirements here are that any package.loaded lines come before the equivalent require line.
A fairly simple change to start out with!
Tweaking onTrade
Let's take a look at FFXIclopedia again. See if the quest involves a trade at any point in time. We see the step "Trade Novalmauge a vial of Beastman Blood." on the quest, so we have work to do.
First, let's look up "Beastman Blood" in Basic Item IDs. It looks like we have a value of "930", so keep track of that number. Our existing onTrade code is as follows:
if(player:getVar("troubleAtTheSluiceVar") == 2) then if(trade:hasItemQty(959,1) and trade:getItemCount() == 1) then -- Trade Dahlia player:startEvent(0x0011); end end
Which is a trade involved with a separate quest. We can add in our own code either above or below this, but make sure you don't accidentally put it between one of those two ifs and its matching end statement. Now let's start adding logic! Borrowing from our previous example, we can work out the following:
if(player:getQuestStatus(SANDORIA,THE_RUMOR) == QUEST_ACCEPTED) then
We want to make sure the trade is only processed if we have started the appropriate quest. Note that QUEST_ACCEPTED is an alias for "1" and means the same thing. We'll use QUEST_ACCEPTED to make it more obvious to anyone that goes behind us and reads it.
count = trade:getItemCount();
Like in the previous scenario, we want to make sure someone can't trade and lose items unrelated to the quest, so we will keep track of the total count of items to check later.
BeastBlood = trade:hasItemQty(930,1) <pre> Remember that number I told you to remember? Here it is! That is how we check exactly what item is being traded. Make sure we have at least one beastman blood. Note that "BeastBlood" above can be nearly anything you want as long as you use the same name later. It is best to make it obvious what it is used for to keep the script readable. It will actually store a returned true/false value from the trade:hasItemQty function. <pre> if(BeastBlood == true and count == 1) then
Make sure we satisfy both conditions that: At least one beastman blood item is being traded. Exactly one item is being traded.
If we don't do this, people could accidentally trade additional items and lost them permanently.
player:startEvent(0x000c);
We will play the cutscene that is supposed to activate upon completion of the quest. I am not going to activate the rewards in this section as I prefer to give them at the end of the cutscene to avoid potential exploits, so let's close up this section.
end end