Table of Contents

Missions

Transport Fever 3 ships with one campaign. For modders, it is possible to provide additional campaigns or single missions. These are then available over CAMPAIGN in the main menu.

Each campaign consists of several missions.

Campaign File

The campaign file is must carry the suffix .campaign.lua. The file can be placed either in a mission mod file or within a separate mod. It should have the following content specified:

function data()
return {
	name = _("CAMPAIGN_NAME"),
	icon = "campaign.tga",
	description = _("CAMPAIGN_DESC"),
	order = 1,
	unlockingProgression = true,
}
end

The properties are:

Mission Folder

Inside each mission folder should consist of the following files, which we will explain one after the other:

info.mission.lua

function data()
	return {
		name = _("MISSION_01_NAME"),
		description = _("MISSION_01_DESC"),
		image = "/gui/mission/m01_preview.tga",
		imagePreview = "/gui/mission/m01_preview2.tga",
		imageSuccess = "/gui/mission/m01_suc.tga",
		imageFailure = "/gui/mission/m01_fail.tga",
		languageScript = "urbangames_campaign_mission_01::/mission/keyframes/languages.script@get",
		loadscreen = "/gui/mission/m01_loadscreen.tga",
		loadscreenDescription = _("MISSION_01_LOADSCREEN"),
		loadscreenVoiceOver = "/audio/voice_over/MISSION_01_LOADSCREEN.wav",
		savegame = "/savegames/savegame.sav",
		starsDescription = _("MISSION_01_STARS_INFO"),
		stars = {
			{
				id = "MISSION_01_STAR_1",
				name = _("MISSION_01_STAR_1"),
				iconLocked = "::/gui/menu/icons/star_1.tga",
				iconCompleted = "::/gui/menu/icons/star_1.tga",
			},
			{
				id = "MISSION_01_STAR_2",
				name = _("MISSION_01_STAR_2"),
				iconLocked = "::/gui/menu/icons/star_2.tga",
				iconCompleted = "::/gui/menu/icons/star_2.tga",
			},
			{
				id = "MISSION_01_STAR_3",
				name = _("MISSION_01_STAR_3"),
				iconLocked = "::/gui/menu/icons/star_3.tga",
				iconCompleted = "::/gui/menu/icons/star_3.tga",
			},
		},
		medals = {
			{
				id = "MISSION_01_MEDAL_1",
				name = _("MISSION_01_MEDAL"),
				nameLocked = _("MISSION_01_MEDAL_LOCKED"),
				iconLocked = "::/gui/menu/icons/star_1.tga",
				iconCompleted = "::/gui/menu/icons/star_1.tga",
			},
		},
		characters = {
			{
				name = _("MISSION_01_CHARACTER_1_NAME"),
				info = _("MISSION_01_CHARACTER_1_INFO"),
				portrait = "urbangames_campaign_mission_01::/mission/dialogue/major_neutral.tga",
			},
			{
				name = _("MISSION_01_CHARACTER_2_NAME"),
				info = _("MISSION_01_CHARACTER_2_INFO"),
				portrait = "urbangames_campaign_mission_01::/mission/dialogue/katie_baker_neutral.tga",
			},
			{
				name = _("MISSION_01_CHARACTER_3_NAME"),
				info = _("MISSION_01_CHARACTER_3_INFO"),
				portrait = "urbangames_campaign_mission_01::/mission/dialogue/andrew_neutral.tga",
			},
		},
		location = _("MISSION_01_LOCATION"),
		year = 1906,
		campaign = "urbangames_campaign::/info.campaign",
		order = 1,
		musicTracks = {
			{ "urbangames_campaign_mission_01::/audio/music/m1_intro.ogg", false },
			{ "urbangames_campaign_mission_01::/audio/music/m1_theme_chapter_1.ogg", true },
		},
	}
end

Notable properties are:

savegame.sav

You can either create a new game or use an existing savegame. Important is that the mission mod is active, when creating/loading the game so that the game scripts for the mission are loaded. Then you can save the game and copy the savegame to your mission folder.

mission.tl

This is the entry point for your mission. The mission interface is highly flexible, but as a starting point, we recommend this pattern here:

local mission_boot_util = ug_require "::/mission/mission_boot_util.tl" as MissionBootUtil
local taskDataFactory = ug_require "mission_story.tl" as function(taskKey : string) : MissionTaskUtil.StaticTaskInfo
 
return mission_boot_util.makeMission({
	scriptKey = "urbangames_campaign_mission_01::/mission/mission.script",
	taskDataFactory = taskDataFactory,
	bootTasks = {
		{ taskKey = "your_first_task" },
	},
})

The mission_story.tl contains the entire logic of the mission and it contains a function that takes a key as an input and returns a mission task. The bootTasks select all tasks that are triggered at the start of the mission. These will be retrieved from the mission_story.tl file.

Mission Scripting

Basically a mission is the combination of an orchestrating game script and a savegame that is played on.

Mission Tasks

Missions are just a sequence of tasks. These tasks can either be tasks that have to be completed by the player, or they can be executed in the background and can be completed by themselves when their internal logic is completed.

Task Scripts

Task scripts are the base of all tasks. These tasks have parameters that need to be specified in the mission_story.tl file and they are triggered sequentially. Task scripts can define various methods in them. We show here only the most essential methods, and we refer to the source code for a complete definition.

	interface TaskScript<TaskSpecificState, TaskParams>
		taskScriptName : string
 
		-- prepare the initial state when attempting to spawn (return nil to prevent spawning)
		onSpawn : function(TaskParams, guiTimeSeconds : number) : TaskSpecificState | nil
 
		isComplete : function(state: TaskContext<TaskSpecificState, TaskParams>) : boolean
 
		onUpdate : function(ctx : TaskContext<TaskSpecificState, TaskParams>, readOnlyApi : MissionTaskReadOnlyApi)
		handleEvent : function(src : string, id : string, name : string, param : any, ctx : TaskContext<TaskSpecificState, TaskParams>, missionTaskApi : MissionTaskApi) : any
		guiHandleEvent : function(src : string, id : string, name : string, param : any, ctx : TaskContext<TaskSpecificState, TaskParams>, taskApi : DeferredTaskApi) : any
		guiUpdate : function(ctx : TaskContext<TaskSpecificState, TaskParams>, taskApi : DeferredTaskApi)
	end

Here, we will focus on the most important ones:

The mission_story.tl file registers each task with a unique key within the file.

The base game already provides a variety of pre-defiend tasks in ::/mission/tasks/. For example, there is the buy_vehicle task:

global record MissionTaskBuyVehicle is MissionInterface.TaskScript<MissionTaskBuyVehicle.State, MissionTaskBuyVehicle.Params>
	record Params
		carrier : Carrier
		cargoTypeRes : string
		minCount : integer
		allowBuyVehicles : {string: integer}
	end
	record State
		result : {Engine.Entity}
		purchasedVehicles : {string: integer}
	end
end

This task takes as parameters:

The state is used by the task internally.

For tasks that are to be completed by the player's action, usually some UI is wanted. In this case, the task info must be specified. Here, we display the essential fields. See the source code for the complete definition.

	record TaskInfo
		record Paragraph
			text : string
		end
		record Option
			text : string
			key : any
		end
 
		name : string
		paragraphs : { Paragraph }
		options : { Option }
 
		instructionData : MissionDialogueData.InstructionData
	end

The fields are:

In the mission_story.tl file, the task might be defined as follows:

	if taskKey == "buy_truck" then
		return {
			taskScript = buy_vehicle as MissionInterface.TaskScript<any, any>,
			taskParams : MissionTaskBuyVehicle.Params = {
				carrier = api.type["enum"].Carrier.ROAD,
				cargoTypeRes = "::/cargos/logs/logs.cargo",
			},
			taskInfo = {
				name = _("Buy a Truck"),
				paragraphs = {
					{ text = _("Buy a truck that can transport logs.") },
				},
 
			},
			followup = {
				{ taskKey = "<NEXT TASK>" },
			},
		}
	end

We present one more example, that shows a branching point. The flow of the mission can be imagined as a directed graph. In this graph, branching points are allowed. The conditions can be given in the follow up definition. We present here the example where the player can choose between two different options A and B:

	if taskKey == "branching_point" then
		return {
			taskScript = page as MissionInterface.TaskScript<any, any>,
			showInHistory = true,
			taskInfo = {
				name = _("Branching point"),
				paragraphs = {
					{ text = _("Make your decision") },
				},
				options = {
					{ text = _("Choose A"), key = "finish-a" },
					{ text = _("Choose B"), key = "finish-b" },
				},
			},
			followup = {
 
				{
					taskKey = "a",
					condition = function(ctx : MissionInterface.TaskContext<any, any>) : boolean
						if ctx.genericState.optionDecision == "finish-a" then
							return true
						end
					end,
				 },
				{
					taskKey = "b",
					condition = function(ctx : MissionInterface.TaskContext<any, any>) : boolean
						if ctx.genericState.optionDecision == "finish-b" then
							return true
						end
					end,
				 },
			}
		}
	end

Here we used the page task. This is an empty task that completes once the player selects a button in the mission window.

To conclude this section, we explain how to end the mission. There are two possible endings: success and failure, defined by the finish task.

	if taskKey == "failure" then
		return {
			taskScript = finish as MissionInterface.TaskScript<any, any>,
			taskParams = {
				success = false,
			},
		}
	end
	if taskKey == "success" then
		return {
			taskScript = finish as MissionInterface.TaskScript<any, any>,
		}
	end

Takeaway

  1. Functions in handlers operate on engine thread (api.gui unavailable, engine functions available)
  2. Functions in guiHandlers operate on ui thread (api.gui available, engine functions unavailable)