Difference between revisions of "How to Script a Mission"
Line 1: | Line 1: | ||
In this page you will learn how to script a mission from scratch. | In this page you will learn how to script a mission from scratch. | ||
+ | '''IN PROGRESS''' | ||
Line 11: | Line 12: | ||
To keep things simple, let's start off with '''Windurst Mission 7-1''' (''The Sixth Ministry'') as it covers most of what we need to go through. | To keep things simple, let's start off with '''Windurst Mission 7-1''' (''The Sixth Ministry'') as it covers most of what we need to go through. | ||
− | After accepting the Mission, we must go to '''Tosuka-Porika''' in '''Windurst Waters''' for a cutscene, so go ahead open up his script | + | After accepting the Mission, we must go to '''Tosuka-Porika''' in '''Windurst Waters''' for a cutscene, so go ahead open up his script :<code>scripts/zones/Windurst_Waters/Tosuka-Porika.lua</code>. |
− | + | I recommend using [http://notepad-plus-plus.org Notepad++] to open scripts as it has syntax highlighting and helps keep track of 'if' statements and their 'end's. | |
Scroll down to the '''onTrigger''' section and we see this: | Scroll down to the '''onTrigger''' section and we see this: | ||
Line 19: | Line 20: | ||
function onTrigger(player,npc) | function onTrigger(player,npc) | ||
− | -- NOTE: | + | -- NOTE: cut out lines that are not needed for this guide to keep things simple |
if (player:getCurrentMission(COP) == THE_ROAD_FORKS and player:getVar("MEMORIES_OF_A_MAIDEN_Status")==10)then | if (player:getCurrentMission(COP) == THE_ROAD_FORKS and player:getVar("MEMORIES_OF_A_MAIDEN_Status")==10)then | ||
player:startEvent(0x036B);--COP event | player:startEvent(0x036B);--COP event | ||
− | -- NOTE: | + | -- NOTE: cut out lines that are not needed for this guide to keep things simple |
elseif(chasingStatus == QUEST_ACCEPTED and player:getVar("CHASING_TALES_TRACK_BOOK") > 0) then | elseif(chasingStatus == QUEST_ACCEPTED and player:getVar("CHASING_TALES_TRACK_BOOK") > 0) then | ||
Line 63: | Line 64: | ||
Okay we've got the correct logic/checks in there; now what? | Okay we've got the correct logic/checks in there; now what? | ||
Now it's time to find the correct cutscene with the correct parameters (e.g. cutscenes that have items/keyitems/numbers/values returned as part of text). | Now it's time to find the correct cutscene with the correct parameters (e.g. cutscenes that have items/keyitems/numbers/values returned as part of text). | ||
+ | |||
+ | |||
== Finding the correct cutscene (with parameters) == | == Finding the correct cutscene (with parameters) == | ||
Line 225: | Line 228: | ||
Alright so now that we've ''set the variable'' '''MissionStatus''' to '''1''' using <code>player:setVar("MissionStatus",1);</code> we need to trigger the cutscene before the '''onEventFinish''' code is called. | Alright so now that we've ''set the variable'' '''MissionStatus''' to '''1''' using <code>player:setVar("MissionStatus",1);</code> we need to trigger the cutscene before the '''onEventFinish''' code is called. | ||
+ | |||
+ | This is what your code should look like so far: | ||
+ | |||
+ | '''onTrigger''' | ||
+ | <pre> | ||
+ | -- NOTE: cut out lines that are not needed for this guide to keep things simple | ||
+ | |||
+ | elseif(chasingStatus == QUEST_ACCEPTED) then | ||
+ | player:startEvent(0x0196); -- Add folllow up cutscene | ||
+ | |||
+ | -- Windurst Mission 7-1 -- | ||
+ | elseif(player:getCurrentMission(WINDURST) == THE_SIXTH_MINISTRY and player:getVar("MissionStatus") == 0) then | ||
+ | player:startEvent(0x02cb,0,OPTISTERY_RING); | ||
+ | |||
+ | else | ||
+ | player:startEvent(0x0172); -- Standard Conversation | ||
+ | end | ||
+ | </pre> | ||
+ | |||
+ | '''onEventFinish''' | ||
+ | <pre> | ||
+ | -- NOTE: cut out lines that are not needed for this guide to keep things simple | ||
+ | elseif(csid ==0x036B)then | ||
+ | player:setVar("MEMORIES_OF_A_MAIDEN_Status",11); | ||
+ | |||
+ | -- Windurst Mission 7-1 -- | ||
+ | elseif(csid == 0x02cb) then | ||
+ | player:addKeyItem(OPTISTERY_RING); | ||
+ | player:messageSpecial(KEYITEM_OBTAINED,OPTISTERY_RING); | ||
+ | player:setVar("MissionStatus",1); | ||
+ | </pre> | ||
+ | |||
+ | Alright so we've got that working but we're missing the cutscene that reminds the player what they need to do next in the mission/questline. | ||
+ | This is usually the next cutscene up from the 'starting cutscene' and adding this check is simple; | ||
+ | <pre> | ||
+ | -- NOTE: cut out lines that are not needed for this guide to keep things simple | ||
+ | |||
+ | elseif(chasingStatus == QUEST_ACCEPTED) then | ||
+ | player:startEvent(0x0196); -- Add folllow up cutscene | ||
+ | |||
+ | -- Windurst Mission 7-1 -- | ||
+ | elseif(player:getCurrentMission(WINDURST) == THE_SIXTH_MINISTRY and player:getVar("MissionStatus") == 0) then | ||
+ | player:startEvent(0x02cb,0,OPTISTERY_RING); | ||
+ | |||
+ | elseif(player:getCurrentMission(WINDURST) == THE_SIXTH_MINISTRY and player:getVar("MissionStatus") == 1) then -- reminder cs | ||
+ | player:startEvent(0x02cc,0,OPTISTERY_RING); | ||
+ | |||
+ | else | ||
+ | player:startEvent(0x0172); -- Standard Conversation | ||
+ | end | ||
+ | </pre> | ||
+ | |||
+ | Notice how we haven't added anything to '''onEventFinish'''. | ||
+ | This is because we don't need the script to do anything other than trigger the cutscene to remind the player what to do. | ||
+ | |||
+ | == Tracking progress through variables == | ||
+ | |||
+ | You made it this far, good. | ||
+ | |||
+ | The Mission Walkthrough (on ffxiclopedia) is telling us (in short): | ||
+ | <pre> | ||
+ | - Travel to Toraimarai Canal | ||
+ | - go to (G-8)(Second Map), where there is a large room with 4 Hinge Oils accompanied by a few bats | ||
+ | - If even one of the oils re-spawn before you click the Marble Door, it will be "locked" until all oils are defeated again. | ||
+ | </pre> | ||
+ | |||
+ | Now lets navigate to <code>scripts/zones/Toraimarai_Canal/mobs</code> since the script we're modifying is a mob's script. | ||
+ | |||
+ | Save the following code as <code>hinge_oil.lua</code> | ||
+ | <pre> | ||
+ | ----------------------------------- | ||
+ | -- Area: Toraimarai Canal | ||
+ | -- Mob: Hinge Oil | ||
+ | ----------------------------------- | ||
+ | |||
+ | require("scripts/globals/titles"); | ||
+ | require("scripts/globals/status"); | ||
+ | require("scripts/globals/missions"); | ||
+ | |||
+ | ----------------------------------- | ||
+ | -- OnMobInitialise Action | ||
+ | ----------------------------------- | ||
+ | |||
+ | function onMobInitialize(mob) | ||
+ | end; | ||
+ | |||
+ | ----------------------------------- | ||
+ | -- onMobFight Action | ||
+ | ----------------------------------- | ||
+ | |||
+ | function onMobFight(mob,target) | ||
+ | end; | ||
+ | |||
+ | ----------------------------------- | ||
+ | -- onMobDeath | ||
+ | ----------------------------------- | ||
+ | |||
+ | function onMobDeath(mob, killer) | ||
+ | |||
+ | end; | ||
+ | </pre> | ||
+ | |||
+ | This script is called whenever there's a change in the mob's current action (as far as i know) | ||
+ | |||
+ | Since we need to track how many '''Hinge Oils''' the player has killed; we need the variable to increase on each mob's death til it reaches the value of 4 (4 is the number of Hinge Oils in the room and they all need to be killed). | ||
+ | |||
+ | To do this we add the following code to '''onMobDeath''': | ||
+ | <pre> | ||
+ | function onMobDeath(mob, killer) | ||
+ | |||
+ | local CurrentMission = killer:getCurrentMission(WINDURST); | ||
+ | local WindyKills = killer:getVar("Windurst_7-1Kills"); | ||
+ | |||
+ | if(CurrentMission == THE_SIXTH_MINISTRY and player:getVar("MissionStatus") == 1) then | ||
+ | if(WindyKills < 4) then | ||
+ | killer:setVar("Windurst_7-1Kills",WindyKills+1); | ||
+ | end | ||
+ | end | ||
+ | end; | ||
+ | </pre> | ||
+ | Breakdown of the code we just added: | ||
+ | |||
+ | <pre> | ||
+ | local WindyKills = killer:getVar("Windurst_7-1Kills"); | ||
+ | </pre> | ||
+ | We declare this as a <code>local</code> variable. | ||
+ | |||
+ | This means that the variable is contained only in the script within this <code>function</code>. | ||
+ | |||
+ | This is useful for when you have a long-ass check like <code>killer:getVar("Windurst_7-1Kills")</code> and want to use an alias for that instead; it also helps make the code more readable. | ||
+ | |||
+ | |||
+ | <pre> | ||
+ | if(CurrentMission == THE_SIXTH_MINISTRY and player:getVar("MissionStatus") == 1) then | ||
+ | </pre> | ||
+ | The reason we check killers current mission first and the stage they're at is because we don't want every player to get the variable added to their character since they don't need it. | ||
+ | |||
+ | Notice how it's <code>killer:</code> and not <code>player:</code>. | ||
+ | The <code>function</code> tells you whether to use ''player'' or ''killer'' like so: | ||
+ | <pre> | ||
+ | -- THIS IS AN EXAMPLE, your script should NOT look like this, it's for demonstration purposes only. | ||
+ | function onMobDeath(mob, killer); | ||
+ | --^ killer, not player | ||
+ | |||
+ | function onTrigger(npc,player) | ||
+ | --^ player, not killer | ||
+ | </pre> |
Revision as of 11:21, 19 October 2013
In this page you will learn how to script a mission from scratch.
IN PROGRESS
Contents
Getting Started
This guide assumes you already have a basic understanding of writing quests, if not then I highly recommend reading BlueKirby0's guide: "How to Make a Quest". If you're too lazy to read over that then don't worry, i'll be going over most things in his guide (but in a lot less detail).
onTrigger
To keep things simple, let's start off with Windurst Mission 7-1 (The Sixth Ministry) as it covers most of what we need to go through.
After accepting the Mission, we must go to Tosuka-Porika in Windurst Waters for a cutscene, so go ahead open up his script :scripts/zones/Windurst_Waters/Tosuka-Porika.lua
.
I recommend using Notepad++ to open scripts as it has syntax highlighting and helps keep track of 'if' statements and their 'end's.
Scroll down to the onTrigger section and we see this:
function onTrigger(player,npc) -- NOTE: cut out lines that are not needed for this guide to keep things simple if (player:getCurrentMission(COP) == THE_ROAD_FORKS and player:getVar("MEMORIES_OF_A_MAIDEN_Status")==10)then player:startEvent(0x036B);--COP event -- NOTE: cut out lines that are not needed for this guide to keep things simple elseif(chasingStatus == QUEST_ACCEPTED and player:getVar("CHASING_TALES_TRACK_BOOK") > 0) then player:startEvent(0x019c); elseif(player:hasKeyItem(149) ==true) then player:startEvent(0x019c); elseif(chasingStatus == QUEST_ACCEPTED) then player:startEvent(0x0196); -- Add follow up cutscene else player:startEvent(0x0172); -- Standard Conversation end end;
Now that we've got his script open, we need to add a check for Windurst Mission 7-1 before starting the cutscene to progress in the mission so let's add that check:
elseif(chasingStatus == QUEST_ACCEPTED) then player:startEvent(0x0196); -- Add follow up cutscene ----> Windurst Mission 7-1 <---- elseif(player:getCurrentMission(WINDURST) == THE_SIXTH_MINISTRY and player:getVar("MissionStatus") == 0) then -- player:startEvent() else player:startEvent(0x0172); -- Standard Conversation
The code simplified:
elseif <--- if the above code isn't true then if... player:getCurrentMission(WINDURST) == THE_SIXTH_MINISTRY <--- The player's Current Mission (in Windurst) is THE_SIXTH_MINISTRY and player:getVar("MissionStatus") == 0) <--- and "MissionStatus" var is equal to 0 (they haven't progressed in the mission) then <--- then carry out the block of code til you see 'end' or another 'elseif' statement.
Notice how we not only check the player's current mission, we also check the variable "MissionStatus".
This is done so we know that the player has just received the mission and has not made any progress in it which is why the variable "MissionStatus" has a value of 0.
The reason we've commented out player:startEvent()
is because we don't know the correct EventID yet.
Okay we've got the correct logic/checks in there; now what?
Now it's time to find the correct cutscene with the correct parameters (e.g. cutscenes that have items/keyitems/numbers/values returned as part of text).
Finding the correct cutscene (with parameters)
Now we must find the correct cutscene for this mission. To do this you can find the mission on ffxiclopedia and scroll down to the spoiler and click [show].
SPOILER WARNING: Details about a quest, mission or other Final Fantasy XI in-game storyline follow. [show]
You then find the Event IDs for that NPC in that particular zone (in our case it's Windurst Waters Event IDs and the NPC is Tosuka-Porika and find the cutscene matching that dialog using the "@cs" command on a GM char e.g.
Note: Cutscenes linked to each other are usually have similar IDs e.g. @cs 830 @cs 831 @cs 832 are most likely related to the same mission/questline
@cs 830
Once you find the correct cutscene you must convert it from decimal to hexadecimal before using it for "player:startEvent( )".
Okay so now we've found the correct cutscene (0x02cb) but it's showing this:
SCRIPT:
player:startEvent(0x02cb);
Script looks fine, lets have a look at how it displays client-sided/in-game.
Client-side cutscene triggered:
Tosuka-Porika: Ah, yes, I mustaru give you this. You should be able to enter the Animastery using this ring. It is the ' '. Try your bestaru not to lose it.
That doesn't look right!
This is because this cutscene requires a special parameter. In our case the parameter is the value of the keyitem Optistery Ring.
To add this we can either call the keyitems script at the top of the file like so:
require("scripts/globals/settings"); require("scripts/globals/titles"); require("scripts/globals/missions"); require("scripts/globals/quests"); require("scripts/zones/Windurst_Walls/TextIDs"); require("scripts/globals/keyitems"); --<----- require keyitems
After we require the keyitems script we can then add this:
player:startEvent(0x02cb,0,OPTISTERY_RING);
Or we can go into the keyitems.lua file, find the id of OPTISTERY_RING and add the following to player:startEvent
player:startEvent(0x02cb,0,250); -- cutscene(0x02cb),idk what this parm does(0),keyitemparam(250) keyitem = OPTISTERY_RING
Alright, time to see if this works!
SCRIPT:
player:startEvent(0x02cb,0,250); -- cutscene, param1, keyitemid
Looks good; we got the correct cutscene and params. Lets see if it displays correctly in-game (client sided).
Client-side cutscene triggered:
Tosuka-Porika: Ah, yes, I mustaru give you this. You should be able to enter the Animastery using this ring. It is the Optistery Ring. Try your bestaru not to lose it.
Success! Alright now we've got the cutscene to trigger so we need to tell the script what to do when the cutscene is finished.
onEventFinish
This is where you tell the script what to do when that cutscene is finished and since we haven't told the script to do anything once the cutscene is triggered, we need to scroll down to onEventFinish and tell it what to do.
----------------------------------- -- onEventFinish ----------------------------------- function onEventFinish(player,csid,option) --printf("CSID: %u",csid); --printf("RESULT: %u",option); if(csid == 0x0037) then -- Show Off Hat player:setVar("QuestHatInHand_var",player:getVar("QuestHatInHand_var")+32); player:setVar("QuestHatInHand_count",player:getVar("QuestHatInHand_count")+1); elseif(csid == 0x00a0) then player:setVar("MissionStatus",1); elseif(csid == 0x00a8) then finishMissionTimeline(player,1,csid,option); elseif(csid == 0x0183 and option == 0) then -- Early Bird Gets The Bookworm player:addQuest(WINDURST,EARLY_BIRD_CATCHES_THE_BOOKWORM); elseif(csid == 0x0193 and option == 0) then player:addQuest(WINDURST,CHASING_TALES); elseif(csid ==0x036B)then player:setVar("MEMORIES_OF_A_MAIDEN_Status",11); end end;
Alright so what does this mean?
--printf("CSID: %u",csid); --printf("RESULT: %u",option);
When not commented out, (two hyphens "--" comments out whatever is after it on the same line) these "printf"s tell the script to show a message on DSGame-Server.exe window showing the CutsceneID of the cutscene that just finished and shows which option the user selected e.g. if the cutscene has a multi-choice option like:
Do you wish to teleport to Windurst Walls? [Yes] [No]
When triggering a cutscene with multi-choice options you need to comment out the line --printf("RESULT: %u",option);
and when triggering the cutscene (from script, not @cs) it'll show a message in DSGameServer.exe like this:
[Lua Script]RESULT: 0
Ok now that the boring explanation is out of the way we need to tell it what to do once the cutscene is finished.
Now since we dont have a cutscene with multi-choice options, what we need to do is add a line like so:
elseif(csid == 0x02cb) then
We then need to tell it what to do upon finishing that cutscene. Alright wiki says to give the keyitem Optistery Ring so lets add that to our code.
elseif(csid == 0x02cb) then player:addKeyItem(OPTISTERY_RING); player:messageSpecial(KEYITEM_OBTAINED,OPTISTERY_RING);
Looking good.
Time to test out the script so far; on your [GM] char use @addmission 2 17 (2 = Windurst, 17 = THE_SIXTH_MINISTRY) and then talk to Tosuka-Porika.
Client-sided (in-game)
Tosuka-Porika: Ah, yes, I mustaru give you this. You should be able to enter the Animastery using this ring. It is the Optistery Ring. Try your bestaru not to lose it.
Doesn't look like there are any issues but wait. Lets talk to him again and see what happens...
Tosuka-Porika: Ah, yes, I mustaru give you this. You should be able to enter the Animastery using this ring. It is the Optistery Ring. Try your bestaru not to lose it.
That shouldn't be happening... Ah wait, we forgot to set the variable for "MissionStatus"
.
Lets add that to our onEventFinish code:
elseif(csid == 0x02cb) then player:addKeyItem(OPTISTERY_RING); player:messageSpecial(KEYITEM_OBTAINED,OPTISTERY_RING); player:setVar("MissionStatus",1);
Alright so now that we've set the variable MissionStatus to 1 using player:setVar("MissionStatus",1);
we need to trigger the cutscene before the onEventFinish code is called.
This is what your code should look like so far:
onTrigger
-- NOTE: cut out lines that are not needed for this guide to keep things simple elseif(chasingStatus == QUEST_ACCEPTED) then player:startEvent(0x0196); -- Add folllow up cutscene -- Windurst Mission 7-1 -- elseif(player:getCurrentMission(WINDURST) == THE_SIXTH_MINISTRY and player:getVar("MissionStatus") == 0) then player:startEvent(0x02cb,0,OPTISTERY_RING); else player:startEvent(0x0172); -- Standard Conversation end
onEventFinish
-- NOTE: cut out lines that are not needed for this guide to keep things simple elseif(csid ==0x036B)then player:setVar("MEMORIES_OF_A_MAIDEN_Status",11); -- Windurst Mission 7-1 -- elseif(csid == 0x02cb) then player:addKeyItem(OPTISTERY_RING); player:messageSpecial(KEYITEM_OBTAINED,OPTISTERY_RING); player:setVar("MissionStatus",1);
Alright so we've got that working but we're missing the cutscene that reminds the player what they need to do next in the mission/questline. This is usually the next cutscene up from the 'starting cutscene' and adding this check is simple;
-- NOTE: cut out lines that are not needed for this guide to keep things simple elseif(chasingStatus == QUEST_ACCEPTED) then player:startEvent(0x0196); -- Add folllow up cutscene -- Windurst Mission 7-1 -- elseif(player:getCurrentMission(WINDURST) == THE_SIXTH_MINISTRY and player:getVar("MissionStatus") == 0) then player:startEvent(0x02cb,0,OPTISTERY_RING); elseif(player:getCurrentMission(WINDURST) == THE_SIXTH_MINISTRY and player:getVar("MissionStatus") == 1) then -- reminder cs player:startEvent(0x02cc,0,OPTISTERY_RING); else player:startEvent(0x0172); -- Standard Conversation end
Notice how we haven't added anything to onEventFinish. This is because we don't need the script to do anything other than trigger the cutscene to remind the player what to do.
Tracking progress through variables
You made it this far, good.
The Mission Walkthrough (on ffxiclopedia) is telling us (in short):
- Travel to Toraimarai Canal - go to (G-8)(Second Map), where there is a large room with 4 Hinge Oils accompanied by a few bats - If even one of the oils re-spawn before you click the Marble Door, it will be "locked" until all oils are defeated again.
Now lets navigate to scripts/zones/Toraimarai_Canal/mobs
since the script we're modifying is a mob's script.
Save the following code as hinge_oil.lua
----------------------------------- -- Area: Toraimarai Canal -- Mob: Hinge Oil ----------------------------------- require("scripts/globals/titles"); require("scripts/globals/status"); require("scripts/globals/missions"); ----------------------------------- -- OnMobInitialise Action ----------------------------------- function onMobInitialize(mob) end; ----------------------------------- -- onMobFight Action ----------------------------------- function onMobFight(mob,target) end; ----------------------------------- -- onMobDeath ----------------------------------- function onMobDeath(mob, killer) end;
This script is called whenever there's a change in the mob's current action (as far as i know)
Since we need to track how many Hinge Oils the player has killed; we need the variable to increase on each mob's death til it reaches the value of 4 (4 is the number of Hinge Oils in the room and they all need to be killed).
To do this we add the following code to onMobDeath:
function onMobDeath(mob, killer) local CurrentMission = killer:getCurrentMission(WINDURST); local WindyKills = killer:getVar("Windurst_7-1Kills"); if(CurrentMission == THE_SIXTH_MINISTRY and player:getVar("MissionStatus") == 1) then if(WindyKills < 4) then killer:setVar("Windurst_7-1Kills",WindyKills+1); end end end;
Breakdown of the code we just added:
local WindyKills = killer:getVar("Windurst_7-1Kills");
We declare this as a local
variable.
This means that the variable is contained only in the script within this function
.
This is useful for when you have a long-ass check like killer:getVar("Windurst_7-1Kills")
and want to use an alias for that instead; it also helps make the code more readable.
if(CurrentMission == THE_SIXTH_MINISTRY and player:getVar("MissionStatus") == 1) then
The reason we check killers current mission first and the stage they're at is because we don't want every player to get the variable added to their character since they don't need it.
Notice how it's killer:
and not player:
.
The function
tells you whether to use player or killer like so:
-- THIS IS AN EXAMPLE, your script should NOT look like this, it's for demonstration purposes only. function onMobDeath(mob, killer); --^ killer, not player function onTrigger(npc,player) --^ player, not killer