Gameplay content

Quests

Multi-scene storylines in the quest journal, with objectives placed at buildings around the city.

A quest is a storyline the player starts from the quest journal: a chain of scenes held together by objectives placed at buildings in the city. The official mods hold dozens to learn from (ul_Friends/Quests/, ul_AdventureSports/Quests/…), and the boilerplate mod has a commented three-step one.

The pieces

Piece File Role
The quest .ulquest Name, description, and the scene that starts it
The start scene .ulscene Sets up people and places, places the first objective
Objectives .ulaction with WHERE = { "quest" } "Go here and do this": the next step, offered only at the building the quest chose
Step scenes .ulscene What happens at each step; they place the next objective

A tidy layout keeps a quest in its own folder:

textQuests/ynl_heist/
├── ynl_heist.ulquest
├── Actions/
│   ├── ynl_heist_meet_crew.ulaction
│   └── ynl_heist_case_the_bank.ulaction
└── Scenes/
    ├── ynl_heist_start.ulscene
    ├── ynl_heist_meet_crew.ulscene
    └── ynl_heist_case_the_bank.ulscene

The .ulquest file

luareturn {
	QUEST_UNIQUEID = "ynl_heist",
	QUEST_NAME = "The Last Job",
	QUEST_DESC = "An old friend needs one more pair of hands. It pays well. It always does.",
	ONCE = true,
	START = "ynl_heist_start",
}
Field Meaning
QUEST_UNIQUEID The id, used by Game:isQuestCompleted
QUEST_NAME, QUEST_DESC Shown in the journal
ONCE true (the default): cannot be replayed after it is completed
START The scene that runs when the player starts the quest

Any number of quests run at once: each has its own row in the journal and on the map's Quests panel, with its own objectives.

Objectives

An objective is an ordinary action whose WHERE is { "quest" }: it is offered nowhere until a scene places it at a building.

lua-- Actions/ynl_heist_meet_crew.ulaction
return {
	ACTION_UNIQUEID = "ynl_heist_meet_crew",
	ACTION_NAME = "Meet the crew",
	WHERE = { "quest" },
	WHEN = { 20, 2 },
	MINUTES = { 1, 1 },
	TIMEOUT_MINUTES = 0,
	EFFECTS = {},
	CONDITIONS = {},
	SCENE_ALWAYS = "ynl_heist_meet_crew",
}

Scenes place and remove objectives through building aliases:

lua-- Scenes/ynl_heist_start.ulscene
return {
	WHAT = {}, WHERE = {}, WHEN = { 0, 24 },   -- only ever started by the quest

	RUN = function(Game, Player, CurrentCompanion)
		Game:SceneStart()
		local friend = Game:generatePerson(true, nil)
		friend:setName("Marco", nil)
		friend:makePermanent()
		Game:setActorAlias("ynl_heist_marco", friend)

		Game:secondScreen(friend)
		Game:Say(friend, "Neutral", "Tonight. The bar on the corner. Come alone.")
		Game:secondScreen()

		Game:generateBuildingAlias("YnlHeistBar", "bar")
		YnlHeistBar:addObjective("ynl_heist_meet_crew")
		Game:SceneEnd()
	end,
}
lua-- Scenes/ynl_heist_meet_crew.ulscene (runs when the player does the objective)
RUN = function(Game, Player, CurrentCompanion)
	Game:SceneStart()
	currentBuilding:removeObjective("ynl_heist_meet_crew")
	-- … the scene …
	Game:generateBuildingAlias("YnlHeistBank", "bank")
	YnlHeistBank:addObjective("ynl_heist_case_the_bank")
	Game:SceneEnd()
end,

Ending a quest

Game:questEnd() completes the quest and clears its objectives. Called from one of the quest's own scenes it knows which quest that is (the folder the scene is in); anywhere else, name it: Game:questEnd("ynl_heist"). Game:isQuestCompleted("ynl_heist") lets other content react to it later.

When the player loses or gives up, end it with Game:questFail([id] [, reason]) instead: the same id rule and the same objectives cleared, but the quest is not completed (a ONCE quest can be started again from the journal), Game:isQuestFailed(id) is true until it is completed one day, and the questFailed event carries the reason.

luaif Game:isQuestCompleted("ynl_heist") then
	Game:Say(Actor, "Surprised", "Wait… weren't you on the news?")
end

Game:startQuest(id) starts another quest from a scene (for sequels); its START scene runs once the current scene is over. Game:openQuestJournal() opens the journal.

A storyline without a quest file

A mod that runs a long arc from its own scenes can still put it in the journal, with a line saying where the story stands:

luaGame:startQuest("ynl_family", {
	name = "The Family Business",
	description = "Uncle Sal wants you in.",
	step = "Meet Sal at the docks after dark",
})
-- later
Game:setQuestStep("ynl_family", "Collect from the bakery by Friday")
-- the end
Game:questEnd("ynl_family")

The definition is kept in the save. once = false lets it be started again after it ends. Such a quest is not listed under "available" in the journal: your scenes start it.

Call Does
Game:getActiveQuests() The ids of every running quest
Game:isQuestActive(id), Game:isQuestCompleted(id) Checks
Game:setQuestStep(id, text), Game:getQuestStep(id) The journal's line under the quest (any quest, file or not)

The events questStarted(id) and questCompleted(id) fire for every quest (Events).

Tips

  • Give every quest scene an empty WHAT so it never fires on its own.
  • Prefix the aliases too (YnlHeistBar): building aliases are globals shared with every other mod.
  • Remove an objective when its step is done, or the action stays offered at that building.
  • Keep quest state in globals (Game:setGlobal("ynl_heist_stage", 2)) when a step depends on earlier choices.