Draft:Tutorial: Adding The Mission Board
This article explains how to add randomly-generated missions to your mod, using Enable Mission Board as a base.
If you want to follow along with this article, download the Mission Board Example Mod here.
Required Files
The files needed for custom missions are found in the Enable Mission Board mod, which is included with PMDO. To implement the functionality for custom missions in your mod, you will need to copy some of these files, as well as the methods from others.
Make sure to change the require calls at the top to reference your own mod, instead of Enable Mission Board. For example, event.lua would go from:
require 'enable_mission_board.event_single' require 'enable_mission_board.event_battle' require 'enable_mission_board.event_misc' require 'enable_mission_board.event_mapgen'
to:
require 'mission_board_example_mod.event_single' require 'mission_board_example_mod.event_battle' require 'mission_board_example_mod.event_misc' require 'mission_board_example_mod.event_mapgen'
mission_gen.lua
Custom missions in PMDO have their data stored in the mission_gen.lua file.
The top line of the file reads "require 'enable_mission_board.common'". This allows the file to use some methods found in the mod's common.lua file. To enable this functionality for your own mod, you will need to:
1. Add all of the methods found in Enable Mission Board's common.lua file to your mod's common.lua file.
2. Change the header of your mission_gen.lua file to reference the common.lua file from your own mod.
require 'mission_board_example_mod.common'
The various sections of mission_gen.lua control different aspects of custom missions. Most of these do not need to be edited, but the ones which are relevant to edit are listed below.
DUNGEON_LIST
This section of the file controls the rank of each dungeon in your mod. The rank affects what class of missions will spawn there, with higher ranks featuring stronger Pokémon as outlaws and giving greater rewards.
In our example mod, we have three dungeons. Delete every MISSION_GEN.DUNGEON_LIST entry except for the first empty one, and replace them with these:
MISSION_GEN.DUNGEON_LIST["example_dungeon_a"] = { [0] = "F" } --Example Dungeon A MISSION_GEN.DUNGEON_LIST["example_dungeon_b"] = { [0] = "E" } --Example Dungeon B MISSION_GEN.DUNGEON_LIST["example_dungeon_c"] = { [0] = "D" } --Example Dungeon C
These entries state that Example Dungeon A has F-rank missions by default, Example Dungeon B has E-rank missions, and Example Dungeon C has D-rank missions.
If you want a sub-dungeon to be eligible for missions, add an additional entry on the same line:
MISSION_GEN.DUNGEON_LIST["example_dungeon_b"] = { [0] = "E", [1] = "D" } --Example Dungeon B; Example Subdungeon B
This entry states that the main section of Example Dungeon B has E-rank missions, but the subdungeon has D-rank missions instead. Example Dungeon C does not have a second entry like this one; as such, its subdungeon is not eligible for missions.
DIFFICULTY
These entries control how much EXP is rewarded from completing a mission of a given rank.
TITLES, FLAVOR_TOP, FLAVOR_BOTTOM
These entries are used to get the text for the titles and body text of various missions. If you add more of these, you'll have to update these lists to include them.
POKEMON
The three lists here contain all the Pokémon which are eligible to become mission clients or targets. The lists are shared between rescue and outlaw missions.
DIFF_POKEMON
The weights here affect how likely each rank of mission is to use a Pokémon with a given power level.
DIFF_REWARDS
The weights here affect the odds of getting certain classes of reward from each rank of mission.
REWARDS
The tables here control which items are in each reward class, and how likely they are to be selected as a reward. A higher number means that item is more likely to be the reward for a mission with a reward in that class.
SPECIAL_LOVER_PAIRS
The pairs of Pokémon here are ones which are eligible for the lover rescue missions. As the comment above says, the first Pokémon is the mission client, while the second is the mission target.
SPECIAL_CHILD_PAIRS
The pairs of Pokémon here are ones which are eligible for the parent-child rescue missions.
SPECIAL_FRIEND_PAIRS
The pairs of Pokémon here are ones which are eligible for the friend rescue missions.
SPECIAL_RIVAL_PAIRS
The pairs of Pokémon here are ones which are eligible for the rival rescue missions.
LOST_ITEMS
Any item on this list is eligible to be used by missions as a lost item.
STOLEN_ITEMS
Any item on this list is eligible to be used by missions as an item stolen by an outlaw.
DELIVERABLE_ITEMS
Any item on this list is eligible to be asked for by a mission which wants an item delivered.
scriptvars.lua
This file contains save data variables for your mod. The one for Enable Mission Board holds the data entries for custom missions.
Copy and paste everything from this file into the scriptvars.lua file for your own mod. (If your mod doesn't have one, simply copy Enable Mission Board's scriptvars.lua file directly.)
common.lua
There are various functions here which are required for mission_gen.lua to work. Copy and paste every function from here into your own mod's common.lua file.
Notice the call to menu.MemberReturnMenu at the top. Later, we'll add the functionality of this menu to your mod.
This file denotes which Pokémon will end up being the sheriff and deputies who come to pick up the outlaw in your mod. By default, these characters are a Magnezone and two Magnemite. Change their species at the top, and you change the Pokémon which appear in the cutscene.
common_vars.lua
There are various functions here which are required for mission_gen.lua to work. Copy and paste every function from here into your own mod's common_vars.lua file.
The function COMMON.ExitDungeonMissionCheckEx controls which map you will be taken to after clearing a mission. In Enable Mission Board, this map is the town area of the Base Camp. Change it to the map with your mission boards. If you're following along with the example mod, change it to:
COMMON.EndDungeonDay(result, 'mystery_zone', -1, 0, 0)
event.lua
This file allows PMDO to call the lua files with your custom code. Copy and paste the require lines from here into your own mod's event.lua file.
event_battle.lua
This file contains the code for interacting with escorts and mission targets. Copy and paste every function from here into your own mod's event_battle.lua file.
event_mapgen.lua
This file contains a script which gets called at the start of a dungeon to make the game generate the mission's required Pokémon and items. Copy and paste this script into your own mod's event_mapgen.lua file.
Note that, after adding this file, you have to call this script in the Zone Steps in PMDO's Universal scripts tab.
event_misc.lua
This file contains the scripts relating to picking up mission items. Copy and paste these scripts into your own mod's event_misc.lua file.
event_single.lua
This file contains scripts used to spawn the mission target Pokémon, such as rescue targets and outlaws. Copy and paste every function from here into your own mod's event_single.lua file.
main.lua
As the comment at the top says, main.lua is used to keep things such as services loaded in memory. Copy and paste the require lines into your own mod's main.lua file
Enable Mission Board's menu folder contains a file named MemberReturnMenu.lua. Copy and paste the folder into your own mod's Script folder. MemberReturnMenu.lua itself does not need any edits.
The services folder
Note that Enable Mission Board has two services folders: one within the Script folder, and one within the Script folder's enable_mission_board subfolder. The second one is the one which will be used for this step.
Enable Mission Board's services folder contains three sub-folders: menu_tools, mission_menu_tools, and mission_service. Copy and paste the services folder into your own mod's Script folder.
For each folder's init.lua file, all you need to do is change the require lines at the top to reference your own mod.
When you're done, your mod's Data/Script/[mod name] folder should look like this.
Other required files
You're almost done! Move to Enable Mission Board's Data/Script folder. There should be two files and a folder there:
- enable_mission_board
- services
- main.lua
Copy and paste the services folder, as well as main.lua, into your own mod's Data/Script folder. Leave both of them as is.
Now, move to Enable Mission Board's Data/Item folder. There are six entries for items in the mod, which are used for lost and stolen items for missions. You have two choices here: you can either add the items manually, or you can copy and paste their data.
If you add the items manually, open the Enable Mission Board mod in PMDO and look at the items to see which characteristics need to be added to your custom items.
If you want to clone the existing items, copy and paste all the item files from Enable Mission Board's Item folder into your own mod's Item folder. If your mod doesn't yet have any custom items, you can simply copy the index.idx file. Otherwise, open it in a text editor, and paste this code into it. This will let PMDO recognize the mission items.
"mission_lost_band": { "$type": "RogueEssence.Data.ItemEntrySummary, RogueEssence", "Icon": 14, "UsageType": 0, "States": [], "Name": { "DefaultText": "Lost Band", "LocalTexts": {} }, "Released": true, "Comment": "", "SortOrder": 0 }, "mission_lost_scarf": { "$type": "RogueEssence.Data.ItemEntrySummary, RogueEssence", "Icon": 14, "UsageType": 0, "States": [], "Name": { "DefaultText": "Lost Scarf", "LocalTexts": {} }, "Released": true, "Comment": "", "SortOrder": 0 }, "mission_lost_specs": { "$type": "RogueEssence.Data.ItemEntrySummary, RogueEssence", "Icon": 15, "UsageType": 0, "States": [], "Name": { "DefaultText": "Lost Specs", "LocalTexts": {} }, "Released": true, "Comment": "", "SortOrder": 0 }, "mission_stolen_band": { "$type": "RogueEssence.Data.ItemEntrySummary, RogueEssence", "Icon": 14, "UsageType": 0, "States": [], "Name": { "DefaultText": "Stolen Band", "LocalTexts": {} }, "Released": true, "Comment": "", "SortOrder": 0 }, "mission_stolen_scarf": { "$type": "RogueEssence.Data.ItemEntrySummary, RogueEssence", "Icon": 14, "UsageType": 0, "States": [], "Name": { "DefaultText": "Stolen Scarf", "LocalTexts": {} }, "Released": true, "Comment": "", "SortOrder": 0 }, "mission_stolen_specs": { "$type": "RogueEssence.Data.ItemEntrySummary, RogueEssence", "Icon": 15, "UsageType": 0, "States": [], "Name": { "DefaultText": "Stolen Specs", "LocalTexts": {} }, "Released": true, "Comment": "", "SortOrder": 0 }
If PMDO is giving you an error after adding this code, go to https://jsonlint.com/ and paste the contents of index.idx into it to find any errors.
Next, go to Enable Mission Board's Data/Strings folder, and copy strings.resx and stringsEx.resx. Then, paste them into your own mod's Strings folder. (If you've already edited these files for your own mod, just paste the strings from each into your own files.)
Finally, go to Enable Mission Board's Content/Music folder, and copy the job clear music. Paste it into your own mod's Content/Music folder.
Implementing Custom Missions
Now that you have these files added, PMDO has the necessary code to generate custom missions. However, by itself, this code won't do anything unless you edit your mod to make use of it.
Let's go to the map where we have the mission boards to implement this functionality. This example will be putting the boards and the area where mission rewards are handed out in the same area.
First, take the strings.resx file from Enable Mission Board's base_camp_2 map (located in Enable_Mission_Board\Data\Script\enable_mission_board\ground\base_camp_2), and paste it into the folder of your map with the mission boards. (if you've already added text to your map's strings.resx file, simply add the strings from Enable Mission Board's file to your own map's file.)
Next, open up init.lua and add these lines at the top:
require 'mission_board_example_mod.common' require 'mission_board_example_mod.mission_gen'
These lines let PMDO use the functions from your custom common.lua and mission_gen.lua on this map.
Next, in the Enter script of the map, change it to look like this:
function default_map.Enter(map) DEBUG.EnableDbgCoro() --Enable debugging this coroutine SV.checkpoint = { Zone = 'mystery_zone', Segment = -1, Map = 0, Entry = 0 } if SV.TemporaryFlags.MissionCompleted then default_map.Hand_In_Missions() end GAME:FadeIn(20) end
The code with SV.checkpoint was there previously, and simply informs PMDO where to place you if you fail a dungeon.
The new lines of code tell PMDO that, if you've completed a mission, it should run the code where you hand in your missions.
Next, below the code for interacting with Storage and your Teammates, but before the final return line, add these functions:
function default_map.MissionBoard_Action(obj, activator) local dungeons_needed = 2 --Number of dungeons needed to unlock the Mission Board local hero = CH('PLAYER') GROUND:CharSetAnim(hero, 'None', true) if SV.MissionPrereq.NumDungeonsCompleted >= dungeons_needed then local menu = BoardSelectionMenu:new(COMMON.MISSION_BOARD_MISSION) UI:SetCustomMenu(menu.menu) UI:WaitForChoice() else UI:ResetSpeaker() UI:WaitShowDialogue(STRINGS:Format(STRINGS.MapStrings['Mission_Board_Locked'])) UI:WaitShowDialogue(STRINGS:Format(STRINGS.MapStrings['Mission_Board_Locked_2'], dungeons_needed - SV.MissionPrereq.NumDungeonsCompleted)) end GROUND:CharEndAnim(hero) end
function default_map.OutlawBoard_Action(obj, activator) local dungeons_needed = 2 --Number of dungeons needed to unlock the Outlaw Board local hero = CH('PLAYER') GROUND:CharSetAnim(hero, 'None', true) if SV.MissionPrereq.NumDungeonsCompleted >= dungeons_needed then local menu = BoardSelectionMenu:new(COMMON.MISSION_BOARD_OUTLAW) UI:SetCustomMenu(menu.menu) UI:WaitForChoice() else UI:ResetSpeaker() UI:WaitShowDialogue(STRINGS:Format(STRINGS.MapStrings['Mission_Board_Locked'])) UI:WaitShowDialogue(STRINGS:Format(STRINGS.MapStrings['Mission_Board_Locked_2'], dungeons_needed - SV.MissionPrereq.NumDungeonsCompleted)) end GROUND:CharEndAnim(hero) end
These functions are interaction code for two objects on this map: MissionBoard, and OutlawBoard.
After these, add this function:
function default_map.Hand_In_Missions() for i = 8, 1, -1 do if SV.TakenBoard[i].Client ~= "" and SV.TakenBoard[i].Completion == MISSION_GEN.COMPLETE then if SV.TakenBoard[i].Type == COMMON.MISSION_TYPE_OUTLAW or SV.TakenBoard[i].Type == COMMON.MISSION_TYPE_OUTLAW_ITEM or SV.TakenBoard[i].Type == COMMON.MISSION_TYPE_OUTLAW_FLEE or SV.TakenBoard[i].Type == COMMON.MISSION_TYPE_OUTLAW_MONSTER_HOUSE then default_map.Outlaw_Job_Clear(SV.TakenBoard[i]) else default_map.Mission_Job_Clear(SV.TakenBoard[i]) end --short pause between fadeins GAME:WaitFrames(20) --clear the job SV.TakenBoard[i] = { Client = "", Target = "", Flavor = "", Title = "", Zone = "", Segment = -1, Floor = -1, Reward = "", Type = -1, Completion = -1, Taken = false, Difficulty = "", Item = "", Special = "", ClientGender = -1, TargetGender = -1, BonusReward = "", BackReference = -1 } end end --reset this flag SV.TemporaryFlags.MissionCompleted = false GAME:MoveCamera(0, 0, 1, true) SOUND:PlayBGM(SV.base_town.Song, true) MISSION_GEN.RegenerateJobs(result) --sort taken jobs now that we're removed completed ones MISSION_GEN.SortTaken() end
This code may seem complex, but it's actually quite simple. What it's doing is:
- Check all of the jobs you've taken to see if they're complete.
- If it finds a complete one, either play the outlaw arrest cutscene if it's an outlaw mission, or the default completion cutscene if it's not.
- After playing that cutscene, delete the mission from your list now that it's been completed.
- Keep checking for completed jobs. Lather, rinse, repeat.
- After checking all your jobs like this, set the value which lets the game know you've completed a mission to false, now that you have none left.
- Set the camera to be no longer locked, and set the music back to normal.
- Generate more jobs to fill the boards.
- Sort your list of missions to remove any gaps caused by completed missions.
After this, we need the functions that this code will use to play the cutscenes. There's a lot of code to copy here, but relatively little of it needs to be edited.
--takes a job and plays an outlaw reward scene depending on the job. function default_map.Outlaw_Job_Clear(job) local hero = CH('PLAYER') GAME:CutsceneMode(true) UI:ResetSpeaker() GROUND:TeleportTo(hero, 244, 288, Direction.Up) GAME:MoveCamera(256, 320, 1, false) SOUND:StopBGM() local money = false if job.Reward == 'money' then money = true end --client is magna, he and the magnemite take the outlaw away if job.Client == 'magna' then local magnemite_left, magnemite_right, magna = COMMON.MakeCharactersFromList({ {'Magnemite_Left', 212, 248, Direction.Down}, {'Magnemite_Right', 276, 248, Direction.Down}, {'Magnezone', 244, 280, Direction.Down} }) local outlaw_gender = job.TargetGender outlaw_gender = COMMON.NumToGender(outlaw_gender) local outlaw_monster = RogueEssence.Dungeon.MonsterID(job.Target, 0, "normal", outlaw_gender) local outlaw = RogueEssence.Ground.GroundChar(outlaw_monster, RogueElements.Loc(244, 248), Direction.Down, outlaw_monster.Species, 'Outlaw') outlaw:ReloadEvents() GAME:GetCurrentGround():AddTempChar(outlaw) GAME:FadeIn(40) SOUND:PlayBGM("Job Clear!.ogg", true) UI:SetSpeaker(magna) UI:WaitShowDialogue(STRINGS:Format(STRINGS.MapStrings['Outlaw_Capture_Cutscene_001'], _DATA:GetMonster(outlaw.CurrentForm.Species):GetColoredName())) GAME:WaitFrames(20) UI:WaitShowDialogue(STRINGS:Format(STRINGS.MapStrings['Outlaw_Capture_Cutscene_002'])) GAME:WaitFrames(20) --reward the item if money then COMMON.RewardItem(MISSION_GEN.DIFF_TO_MONEY[job.Difficulty], true) else COMMON.RewardItem(job.Reward) end if job.BonusReward ~= '' then UI:SetSpeaker(magna) GAME:WaitFrames(20) UI:WaitShowDialogue(STRINGS:Format(STRINGS.MapStrings['Outlaw_Capture_Cutscene_003'])) GAME:WaitFrames(20) COMMON.RewardItem(job.BonusReward) end GAME:WaitFrames(20) default_map.RewardEXP(job) GAME:WaitFrames(20) UI:SetSpeaker(magna) UI:WaitShowDialogue(STRINGS:Format(STRINGS.MapStrings['Outlaw_Capture_Cutscene_004'])) GROUND:CharSetEmote(magnemite_left, "happy", 0) GROUND:CharSetEmote(magnemite_right, "happy", 0) local coro1 = TASK:BranchCoroutine(function() GROUND:CharSetAction(magna, RogueEssence.Ground.PoseGroundAction(magna.Position, magna.Direction, RogueEssence.Content.GraphicsManager.GetAnimIndex("Pose"))) end) local coro2 = TASK:BranchCoroutine(function() GROUND:CharSetAction(magnemite_left, RogueEssence.Ground.PoseGroundAction(magnemite_left.Position, magnemite_left.Direction, RogueEssence.Content.GraphicsManager.GetAnimIndex("Pose"))) end) local coro3 = TASK:BranchCoroutine(function() GROUND:CharSetAction(magnemite_right, RogueEssence.Ground.PoseGroundAction(magnemite_right.Position, magnemite_right.Direction, RogueEssence.Content.GraphicsManager.GetAnimIndex("Pose"))) end) local coro4 = TASK:BranchCoroutine(function() GAME:WaitFrames(12) SOUND:PlayBattleSE('EVT_CH03_Magnezone') end) TASK:JoinCoroutines({coro1, coro2, coro3, coro4}) GAME:WaitFrames(60) GROUND:CharEndAnim(magna) GROUND:CharEndAnim(magnemite_left) GROUND:CharEndAnim(magnemite_right) GROUND:CharSetEmote(magnemite_left, "", 0) GROUND:CharSetEmote(magnemite_right, "", 0) GAME:WaitFrames(20) --fade out and clean up any temporary characters SOUND:FadeOutBGM(40) GAME:FadeOut(false, 40) GAME:GetCurrentGround():RemoveTempChar(magna) GAME:GetCurrentGround():RemoveTempChar(magnemite_left) GAME:GetCurrentGround():RemoveTempChar(magnemite_right) GAME:GetCurrentGround():RemoveTempChar(outlaw) else--client is some random mon local client_gender = job.ClientGender client_gender = COMMON.NumToGender(client_gender) client_gender = client_gender local client_monster = RogueEssence.Dungeon.MonsterID(job.Client, 0, "normal", client_gender) local client = RogueEssence.Ground.GroundChar(client_monster, RogueElements.Loc(244, 248), Direction.Down, job.Client:gsub("^%l", string.upper), client_monster.Species) client:ReloadEvents() GAME:GetCurrentGround():AddTempChar(client) GAME:FadeIn(40) SOUND:PlayBGM("Job Clear!.ogg", true) UI:SetSpeaker(client) local item = RogueEssence.Dungeon.InvItem(job.Item) UI:WaitShowDialogue(STRINGS:Format(STRINGS.MapStrings['Outlaw_Retrieve_Cutscene'], item:GetDisplayName())) GAME:WaitFrames(20) UI:WaitShowDialogue(STRINGS:Format(STRINGS.MapStrings['Mission_Generic_Reward'])) GAME:WaitFrames(20) --reward the item if money then COMMON.RewardItem(MISSION_GEN.DIFF_TO_MONEY[job.Difficulty], true) else COMMON.RewardItem(job.Reward) end if job.BonusReward ~= '' then UI:SetSpeaker(client) GAME:WaitFrames(20) UI:WaitShowDialogue(STRINGS:Format(STRINGS.MapStrings['Mission_Generic_Reward_2'])) GAME:WaitFrames(20) COMMON.RewardItem(job.BonusReward) end GAME:WaitFrames(20) default_map.RewardEXP(job) GAME:WaitFrames(20) --fade out and clean up any temporary characters SOUND:FadeOutBGM(40) GAME:FadeOut(false, 40) GAME:GetCurrentGround():RemoveTempChar(client) end GAME:CutsceneMode(false) end
--takes a job and plays an regular mission reward scene depending on the job. function default_map.Mission_Job_Clear(job) local hero = CH('PLAYER') GAME:CutsceneMode(true) UI:ResetSpeaker() GROUND:TeleportTo(hero, 100, 600, Direction.Up) GAME:MoveCamera(90, 565, 1, false) SOUND:StopBGM() local money = false if job.Reward == 'money' then money = true end --client is target. Check on escort is needed in case the escort is to the same species. if job.Client == job.Target and job.Type ~= COMMON.MISSION_TYPE_ESCORT then local client_gender = job.ClientGender client_gender = COMMON.NumToGender(client_gender) local client_monster = RogueEssence.Dungeon.MonsterID(job.Client, 0, "normal", client_gender) local client = RogueEssence.Ground.GroundChar(client_monster, RogueElements.Loc(244, 248), Direction.Down, job.Client:gsub("^%l", string.upper), client_monster.Species) client:ReloadEvents() GAME:GetCurrentGround():AddTempChar(client) GAME:FadeIn(40) SOUND:PlayBGM("Job Clear!.ogg", true) UI:SetSpeaker(client) --different thank you message depending on the job type if job.Type == COMMON.MISSION_TYPE_RESCUE then UI:WaitShowDialogue(STRINGS:Format(STRINGS.MapStrings['Mission_Response_Rescue'])) elseif job.Type == COMMON.MISSION_TYPE_EXPLORATION then local zone = _DATA.DataIndices[RogueEssence.Data.DataManager.DataType.Zone]:Get(job.Zone) UI:WaitShowDialogue(STRINGS:Format(STRINGS.MapStrings['Mission_Response_Exploration'], zone:GetColoredName())) elseif job.Type == COMMON.MISSION_TYPE_LOST_ITEM then local item = RogueEssence.Dungeon.InvItem(job.Item) UI:WaitShowDialogue(STRINGS:Format(STRINGS.MapStrings['Mission_Response_Lost_Item'], item:GetDisplayName())) else--delivery local item = RogueEssence.Dungeon.InvItem(job.Item) UI:WaitShowDialogue(STRINGS:Format(STRINGS.MapStrings['Mission_Response_Delivery_Item'], item:GetDisplayName())) end GAME:WaitFrames(20) UI:WaitShowDialogue(STRINGS:Format(STRINGS.MapStrings['Mission_Generic_Reward'])) GAME:WaitFrames(20) --reward the item if money then COMMON.RewardItem(MISSION_GEN.DIFF_TO_MONEY[job.Difficulty], true) else COMMON.RewardItem(job.Reward) end if job.BonusReward ~= '' then UI:SetSpeaker(client) GAME:WaitFrames(20) UI:WaitShowDialogue(STRINGS:Format(STRINGS.MapStrings['Mission_Generic_Reward_2'])) GAME:WaitFrames(20) COMMON.RewardItem(job.BonusReward) end GAME:WaitFrames(20) default_map.RewardEXP(job) GAME:WaitFrames(20) --fade out and clean up any temporary characters SOUND:FadeOutBGM(40) GAME:FadeOut(false, 40) GAME:GetCurrentGround():RemoveTempChar(client) else--client not the target local client_gender = job.ClientGender client_gender = COMMON.NumToGender(client_gender) local client_monster = RogueEssence.Dungeon.MonsterID(job.Client, 0, "normal", client_gender) local client = RogueEssence.Ground.GroundChar(client_monster, RogueElements.Loc(224, 248), Direction.Down, job.Client:gsub("^%l", string.upper), client_monster.Species) client:ReloadEvents() GAME:GetCurrentGround():AddTempChar(client) local target_gender = job.TargetGender target_gender = COMMON.NumToGender(target_gender) local target_monster = RogueEssence.Dungeon.MonsterID(job.Target, 0, "normal", target_gender) target_monster.Gender = _DATA:GetMonster(job.Target).Forms[0]:RollGender(_ZONE.CurrentGround.Rand) local target = RogueEssence.Ground.GroundChar(target_monster, RogueElements.Loc(264, 248), Direction.Down, job.Target:gsub("^%l", string.upper), target_monster.Species) target:ReloadEvents() GAME:GetCurrentGround():AddTempChar(target) GAME:FadeIn(40) SOUND:PlayBGM("Job Clear!.ogg", true) UI:SetSpeaker(client) if job.Type == COMMON.MISSION_TYPE_ESCORT then UI:WaitShowDialogue(STRINGS:Format(STRINGS.MapStrings['Mission_Response_Escort'])) else if job.Special == MISSION_GEN.SPECIAL_CLIENT_LOVER then UI:WaitShowDialogue(STRINGS:Format(STRINGS.MapStrings['Mission_Response_Lover'])) elseif job.Special == MISSION_GEN.SPECIAL_CLIENT_RIVAL then UI:WaitShowDialogue(STRINGS:Format(STRINGS.MapStrings['Mission_Response_Rival'])) elseif job.Special == MISSION_GEN.SPECIAL_CLIENT_CHILD then UI:WaitShowDialogue(STRINGS:Format(STRINGS.MapStrings['Mission_Response_Child'])) else UI:WaitShowDialogue(STRINGS:Format(STRINGS.MapStrings['Mission_Response_Rescue_Friend'])) end end GAME:WaitFrames(20) UI:WaitShowDialogue(STRINGS:Format(STRINGS.MapStrings['Mission_Generic_Reward'])) GAME:WaitFrames(20) --reward the item if money then COMMON.RewardItem(MISSION_GEN.DIFF_TO_MONEY[job.Difficulty], true) else COMMON.RewardItem(job.Reward) end if job.BonusReward ~= '' then UI:SetSpeaker(client) GAME:WaitFrames(20) UI:WaitShowDialogue(STRINGS:Format(STRINGS.MapStrings['Mission_Generic_Reward_2'])) GAME:WaitFrames(20) COMMON.RewardItem(job.BonusReward) end GAME:WaitFrames(20) default_map.RewardEXP(job) GAME:WaitFrames(20) --fade out and clean up any temporary characters SOUND:FadeOutBGM(40) GAME:FadeOut(false, 40) GAME:GetCurrentGround():RemoveTempChar(client) GAME:GetCurrentGround():RemoveTempChar(target) end GAME:CutsceneMode(false) end
function default_map.RewardEXP(job) --Reward EXP for your party local exp_reward = MISSION_GEN.GetJobExpReward(job.Difficulty) local exp_reward_string = "[color=#00FFFF]"..exp_reward.."[color]" UI:ResetSpeaker() UI:WaitShowDialogue(STRINGS:Format(STRINGS.MapStrings['Mission_Handout_EXP'], exp_reward_string)) PrintInfo("Rewarding EXP for job with difficulty "..job.Difficulty.." and reward "..exp_reward_string) local player_count = _DATA.Save.ActiveTeam.Players.Count for player_idx = 0, player_count-1, 1 do TASK:WaitTask(GROUND:_HandoutEXP(_DATA.Save.ActiveTeam.Players[player_idx], exp_reward)) end end
This code is used to handle the aftermath of a mission, with Outlaw_Job_Clear handling the results of outlaw missions, and Mission_Job_Clear handling other types of missions. RewardEXP gives all Pokémon in the party EXP for having completed a mission.
To modify the positions of the characters in the job clear cutscene, you can modify this code. For example, if you want to make the player character spawn at position (160, 32) and face left, you would change the GROUND:TeleportTo line to read:
GROUND:TeleportTo(hero, 160, 32, Direction.Left)
If you want to make the mission client spawn at position (32, 32) and face right, similarly, you would change its position.
local client = RogueEssence.Ground.GroundChar(client_monster, RogueElements.Loc(32, 32), Direction.Right, job.Client:gsub("^%l", string.upper), client_monster.Species)
Optional: Creating a Separate Outlaw Board
By default, mission_gen.lua only supports one mission board. Editing it to create a separate mission board and outlaw notice board is easy, but requires changes in a few areas.
Generating a separate outlaw board
The default code in mission_gen.lua only generates one board.
function MISSION_GEN.RegenerateJobs(result) --Regenerate jobs MISSION_GEN.ResetBoards() MISSION_GEN.RemoveMissionBackReference() MISSION_GEN.GenerateBoard(result, COMMON.MISSION_BOARD_MISSION) --MISSION_GEN.GenerateBoard(COMMON.MISSION_BOARD_OUTLAW) MISSION_GEN.SortMission() MISSION_GEN.SortOutlaw() end
To make it generate a second, replace the commented line like so:
function MISSION_GEN.RegenerateJobs(result) --Regenerate jobs MISSION_GEN.ResetBoards() MISSION_GEN.RemoveMissionBackReference() MISSION_GEN.GenerateBoard(result, COMMON.MISSION_BOARD_MISSION) MISSION_GEN.GenerateBoard(result, COMMON.MISSION_BOARD_OUTLAW) MISSION_GEN.SortMission() MISSION_GEN.SortOutlaw() end
Generating outlaw missions
By default, regardless of the board type, mission_gen.lua will generate missions with a 1-in-3 chance of being an outlaw mission.
--generate the objective. local objective local missionOutlawRoll = math.random(3) if missionOutlawRoll == 1 then --1 in 3 chance of outlaw
To change this behavior, remove the check for missionOutlawRoll and replace it with this:
local objective if mission_type == COMMON.MISSION_BOARD_OUTLAW then -- Now, we always get outlaw missions on the outlaw board
Making the outlaw board accessible
The default code used in mission_gen.lua for determining whether the board has jobs works fine for a mission board, but not for an outlaw board. It will cause an outlaw board to always be inaccessible, even if jobs have been generated for it.
function BoardSelectionMenu:DrawMenu() --color this red if there's no jobs and mark there's no jobs to view. self.board_populated = true local board_name = "" if self.board_type == COMMON.MISSION_BOARD_OUTLAW then if SV.OutlawBoard[1].Client == '' then board_name = "[color=#FF0000]"..Text.FormatKey("MISSION_BOARD_NAME_OUTLAW").."[color]" self.board_populated = false else board_name = Text.FormatKey("MISSION_BOARD_NAME_OUTLAW") end else if MISSION_GEN.MissionBoardIsEmpty() then board_name = "[color=#FF0000]"..Text.FormatKey("MISSION_BOARD_NAME_MISSION").."[color]" self.board_populated = false else board_name = Text.FormatKey("MISSION_BOARD_NAME_MISSION") end end
To fix this error, add a new function in mission_gen.lua, MISSION_GEN.OutlawBoardIsEmpty(), with this code:
function MISSION_GEN.OutlawBoardIsEmpty() for k, v in pairs(SV.OutlawBoard) do if v.Client ~= '' then return false end end return true end
Then, make the DrawMenu function in BoardSelectionMenu use your new function.
function BoardSelectionMenu:DrawMenu() --color this red if there's no jobs and mark there's no jobs to view. self.board_populated = true local board_name = "" if self.board_type == COMMON.MISSION_BOARD_OUTLAW then if MISSION_GEN.OutlawBoardIsEmpty() then board_name = "[color=#FF0000]"..Text.FormatKey("MISSION_BOARD_NAME_OUTLAW").."[color]" self.board_populated = false else board_name = Text.FormatKey("MISSION_BOARD_NAME_OUTLAW") end else if MISSION_GEN.MissionBoardIsEmpty() then board_name = "[color=#FF0000]"..Text.FormatKey("MISSION_BOARD_NAME_MISSION").."[color]" self.board_populated = false else board_name = Text.FormatKey("MISSION_BOARD_NAME_MISSION") end end
Troubleshooting
Problems with code
Make sure to double-check the require lines for each imported file to ensure that they reference your own mod, and not Enable Mission Board.
Problems with text strings
The strings required for custom missions to work are located in two locations:
1. The Enable Mission Board/Strings folder, in both strings.resx and stringsEx.resx. These contain the global strings used to display mission data on your job list and elsewhere.
2. The strings.resx folder for the ground map which the mission boards are located in. This is base_camp_2 for Enable Mission Board and default_map for Mission Board Example Mod; for your mod, it will be in the ground scripts folder for the map with the mission boards. These contain the strings used just on this map, when completing a mission and receiving rewards.
Make sure to put these strings in the proper locations for your mod. Do not change the names of any of these strings, unless any references to the changed name are also updated.
Problems with spawning mission Pokémon
Double-check that GenerateMissionFromSV is being called in your mod's Universal Events, and that the function is present in your mod's event_mapgen.lua.
Problems with mission items
If adding the mission items via copying and pasting the item data is not working, then adding the items manually is recommended. Make sure to delete the data you added to your mod's index.idx file, as well as the item files you pasted.
The important qualities for your mission items to have:
- They must have the same names as the ones defined in mission_gen.lua.
- They should be undroppable and unstackable.
- They should not have any uses defined.
Conclusion
After adding these files and making these changes to your project, custom missions should be enabled for your mod. Complete two dungeons and interact with the mission board or outlaw board objects, and you should be able to take and complete missions.