Draft:Tutorial: Adding The Mission Board

From PMDOWiki

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


The menu folder

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.