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
WHATso 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.