TASK GUIDE
Written by @nltp_ashes
Table of content :
- I. INTRODUCTION
- II. TASK CONFIG
- II.A. GENERALITIES
- II.B.1. TASK LIFECYCLE (IN CONFIG)
- II.B.2. TASK LIFECYCLE (IN SCRIPT)
- II. TASK CONFIG
- III. STARTING THE TASK
- III.A. USING THE GAME'S DYNAMIC RANDOM TASKS
- III.B. USING YOUR OWN FUNCTIONS
- IV. TASK FUNCTORS
- IV.A. TASK TITLE FUNCTOR
- IV.B. TASK DESCRIPTION FUNCTOR
- IV.C. TASK TARGET FUNCTOR
- IV.D. TASK STATUS FUNCTOR
I. INTRODUCTION
Before we start, keep in mind tasks often take part at the junction of a lot of the game's systems : dialogs, NPCs, squads, smart terrains and what not.
This guide aims to address how to work on tasks, and specifically tasks, while sometimes making connections to other things. You'll most likely need to get familiar (either before, or along the way) with those other systems in order to create your task, depending on how complex you want it.
Make sure to check the Anomaly Modding Book to see if guides exists for these other aspects not detailed here.
Without going into details, to create a task, you will at least need to play with the following files.
-
gamedata/configs/misc/task/task_manager.ltx
:
where your task config will be; -
gamedata/configs/text/eng/st_my_task.xml
:
where the translation strings for your task will be; -
gamedata/scripts/my_task.script
:
where your task functors will be.
For more complex tasks, you may also need to create custom squads of NPCs, for which you will need to create the following file.
-
gamedata/configs/misc/squad_descr/squad_descr_my_task.ltx
:
where your custom squads configs will be. -
gamedata/configs/gameplay/dialogs_my_task.xml
:
where your custom dialogs will be.
Note that, to ensure maximum compatibility, it is highly discouraged to edit and redistribute modified vanilla files. Instead, you can use DLTX and DXML to modify those files at runtime, ensuring high compatibility with other addons.
II. TASK CONFIG
Your task is in reality composed of two things. A config side, and a script side. First, we'll take a look at the config side.
II.A. GENERALITIES
Here is a complete task config, or section. It contains all the information to allow the game to use your task. Some of these fields are optional, but I highly recommend you use all of them to ensure a good experience for the player.
File: gamedata/configs/misc/task/task_manager.ltx
[my_task]
title_functor = my_task_title_f
descr_functor = my_task_descr_f
target_functor = my_task_target_f
status_functor = my_task_status_f
...
icon = ui_inGame2_Skat_1
storyline = false
stage_complete = 1
...
-
title_functor
Name of a function (defined intask_functor
namespace). This function will be used to display the name of your task in the PDA. See more in IV.A.. -
descr_functor
Name of a function (defined intask_functor
namespace). This function will be used to display a description of the player's current objective in the PDA. See more in IV.B.. -
target_functor
Name of a function (defined intask_functor
namespace). This function will be used to place a marker on the player's objective in the PDA. See more in IV.C.. -
status_functor
Name of a function (defined intask_functor
namespace). This function will manage your task logic as a whole. It will be responsible for spawning objects, checking objective completion, progressing the task, etc. See more in IV.D.. -
icon
ID of a texture description. This is the icon for the task that will be displayed in the PDA and in the dialogs. -
storyline
Boolean.true
if your task is a main mission (golden marker on the PDA) orfalse
if your task is a secondary mission (gray marker on the PDA). -
stage_complete
Number. Stage at which the task is considered completed. A task starts at stage0
and can have any number of stages you want.
II.B.1. TASK LIFECYCLE (IN CONFIG)
Worth a special attention are on_init
, on_complete
and on_fail
. Those are three condlists executed at key moments of your task.
At these events, you might want to give the player a reward, or take an item away from the player, etc. There honestly a lot you can do here, so I'll just breeze over the commonly used things.
File: gamedata/configs/misc/task/task_manager.ltx
[my_task]
...
on_init = %+my_task_is_active%
on_complete = %-my_task_is_active +my_task_is_finished =reward_random_money(5000:10000) =reward_stash(true)%
on_fail = %-my_task_is_active%
-
on_init
Condlist. List of functions to execute when the player has just taken the task. -
on_complete
Condlist. List of functions to execute when the player has just completed the task. -
on_fail
Condlist. List of functions to execute when the player has just failed the task.
You can replace the content of % ... %
by any arrangement of the following :
-
+some_info_portion
This will give an info portion calledsome_info_portion
to the player.
You need to replacesome_info_portion
with the name of an info portion of your liking. -
-some_info_portion
This will remove an info portion calledsome_info_portion
from the player.
You need to replacesome_info_portion
with the name of an info portion of your liking. -
=reward_random_money(min:max)
This will reward the player with a sum of money betweenmin
andmax
.
You need to replacemin
andmax
with number values. -
=reward_stash(true)
This will reward the player with a stash location. -
=reward_item(sec)
This will reward the player with a specific item that hassec
for a section.
You need to replacesec
with the name of an existing section. -
=reward_random_item(sec_1:sec_2)
This will give the player one item among the list of sections passed (you can list as many sections as you want, separated by a:
.
You need to replacesec_1
,sec_2
, etc. with the name of an existing section. -
=remove_item(sec)
This will remove one item that hassec
for a section from the player's inventory. No effect if the player does not have the item.
You need to replacesec
with the name of an existing section. -
=complete_task_inc_goodwill(value:faction)
This will give the playervalue
points of goodwill withfaction
faction.
You need to replacevalue
with a number value andfaction
with the name of a faction. -
=fail_task_dec_goodwill(value:faction)
This will remove from playervalue
points of goodwill withfaction
faction.
You need to replacevalue
with a number value andfaction
with the name of a faction.
This list is not exhaustive, and there is plenty more you can do. As you'll soon read in II.B.2., you can even define your own functions to use in these condlists.
II.B.2. TASK LIFECYCLE (IN SCRIPT)
The condlists we just had a look at will try to call the functions they contain in the xr_effects
namespace.
Because of how S.T.A.L.K.E.R. scripts are set up, this means you can define your own functions (in your script) to handle those events :
File: gamedata/configs/misc/task/task_manager.ltx
[my_task]
...
on_init = %=my_task_init()%
on_complete = %=my_task_complete()%
on_fail = %=my_task_fail()%
File: gamedata/scripts/my_task.script
function xr_effects.my_task_init(actor, npc, params)
give_info("my_task_is_active")
-- some other code you might want can be added here
end
function xr_effects.my_task_complete(actor, npc, params)
disable_info("my_task_is_active")
give_info("my_task_is_finished")
xr_effects.reward_random_money(actor,npc,{"5000","10000"})
xr_effects.reward_stash(actor,npc,{"true"})
-- some other code you might want can be added here
end
function xr_effects.my_task_fail(actor, npc, params)
disable_info("my_task_is_active")
-- some other code you might want can be added here:
end
This allows you for a finer control over what happens at the events, though this has the downside of putting config-related stuff in script, which isn't ideal.
III. STARTING THE TASK
It is important to note that there are multiple ways to make the player start your task.
III.A. USING THE GAME'S DYNAMIC RANDOM TASKS
You can utilize the game's dynamic random tasks system. For this, you need to :
- name your task according to the following contract
<npc_name>_task_<task_name>
- add
<actor_dialog>dm_ordered_task_dialog</actor_dialog>
to the task giver's dialogs (if the NPC doesn't already have it) - add
<actor_dialog>dm_ordered_task_completed_dialog</actor_dialog>
to the task giver's dialogs (if the NPC doesn't already have it)
Once that's done, you will be able to add a few things to your task config to allow you more control.
File: gamedata/configs/misc/task/task_manager.ltx
[<npc_name>_task_my_task]
...
job_descr = st_my_task_job_descr
task_complete_descr = st_my_task_complete_descr
repeat_timeout = 0
precondition = {-my_task_is_finished} true, false
...
-
job_descr
ID of a translation string. This translation string will be the task description displayed before taking the task. -
task_complete_descr
ID of a translation string. This translation string will be the text displayed after the player says "The job is done.". -
repeat_timeout
Number. Time before the player can complete that same task again. -
precondition
Condlist. If the condlist is evaluated astrue
, the player can take the task, if it is evaluated asfalse
, then the task won't show up.
By default, those tasks are repeatable, but if you check for an info portion in the precondition, and give that info when the player completes the task, you can make sure the task is played only once (useful for storyline tasks).
III.B. USING YOUR OWN FUNCTIONS
Alternatively to using the dynamic random tasks, you can define your own dialog, that will eventually call a function you defined, which will be responsible for starting the task. This will complexify your code, it'll also increase the amount of code to maintain, but it'll allow you more control over the dialogs leading to taking up a tasks, and how the tasks themselves are started.
There are many ways to start the task using that technique, but the two important things you should know are :
- To execute a script function in a dialog, you use
<action>namespace.function</action>
inside a phrase of a dialog.
File:gamedata/configs/gameplay/my_task_dialogs.xml
<dialog id="my_dialog_id"> <phrase_list> <phrase id="0"> <!-- actor --> <text>st_my_string_id</text> <action>my_task.start_task</action> </phrase> </phrase_list> </dialog>
- You define that function in your script :
File:gamedata/scripts/my_task.script
function start_task(actor, npc) -- You can check conditions or do anything before giving the task if you want -- Get the server object of the NPC giving the task -- If you can that function from a dialog, 'npc' will be the person the player is talking to, which can be used as a task giver local task_giver = get_story_se_object("my_task_giver") -- Start the task called 'my_task' with 'task_giver' as the NPC giving the task task_manager.get_task_manager():give_task("my_task", task_giver:id()) end
IV. TASK FUNCTORS
On the script side of your task, as previously explained, you will need a few functions to get your task rolling. Those are :
task_functor.my_task_title_f(...)
task_functor.my_task_descr_f(...)
task_functor.my_task_target_f(...)
task_status_functor.my_task_status_f(...)
I recommend you use a table to manage what has been done in your task. You can define this table at the beginning of your my_task.script
.
File: gamedata/scripts/my_task.script
MY_TASK_CACHE = {}
Note that you'll need to persist this table, otherwise your task will break with loading a save-game :
File: gamedata/scripts/my_task.script
--- Function used to store information in the save file.
--- @param m_data table
--- @return nil
function save_state(m_data)
m_data.my_addon_name_my_task_cache = MY_TASK_CACHE
end
--- Function used to load information stored in the save file.
--- @param m_data table
--- @return nil
function load_state(m_data)
MY_TASK_CACHE = m_data.my_addon_name_my_task_cache or {}
end
IV.A. TASK TITLE FUNCTOR
This function manages the task's title. In the end, this function must return a string, a string that will be displayed in the PDA (among other places) as the task's name.
While generally, the name of the task itself won't change, having it as a functor allows you to have a dynamic title, in case you'd like to change it in the middle of the task for some reason.
File: gamedata/scripts/my_task.script
--- Function used to retrieve the title of the mission (displayed in the PDA).
--- @param task_id number
--- @param field string
--- @param p any
--- @param tsk CGameTask
--- @return string
function task_functor.my_task_title_f(task_id,field,p,tsk)
return game.translate_string("st_my_task_title")
end
Alternatively, since the task's title isn't really meant to change, you can hard-code it in the task's config.
File: gamedata/configs/misc/task/task_manager.ltx
[my_task]
...
title = st_my_task_title
...
IV.B. TASK DESCRIPTION FUNCTOR
In the PDA, the task's description will be whatever string is returned by this function.
A few recommendations :
- write your description in a passive way, and assume doesn't know what it did before and what it needs to do
- always give a bit of context as to what the player has done :
You have found and retrieved the documents.
- describe what is the new objective the player has to achieve :
Return the item to Dushman, in Deadcity.
You can have the description evolve anyway you want, either by checking the task's stage, or by checking info portions, etc.
File: gamedata/scripts/my_task.script
--- Function used to retrieve the description of the mission (displayed in the PDA).
--- @param task_id number
--- @param field string
--- @param p any
--- @param tsk CGameTask
--- @return string
function task_functor.my_task_descr_f(task_id,field,p,tsk)
if has_alife_info("the_player_has_done_something_really_bad") then
return game.translate_string("st_send_player_to_brazil")
end
if tsk.stage == 0 then
return game.translate_string("st_my_task_stage_0_description")
end
if tsk.stage == 1 then
return game.translate_string("st_my_task_stage_1_description")
end
end
Similarly to the task's title, the description can also be hard-coded in the task's config. Although this isn't recommended, since your task will rarely have only one stage, and each stage should have its own description.
File: gamedata/configs/misc/task/task_manager.ltx
[my_task]
...
descr = st_my_task_description
...
IV.C. TASK TARGET FUNCTOR
This function will be used to place a marker on the player's objective in the PDA. You can code this function as you see fit.
All you have to keep in mind is, if the function returns nil, no marker will be shown on the PDA, if it returns a number, the game will try to find an object with an ID matching the number, and it'll place a marker on it.
Unfortunately, this means that you cannot just place markers anywhere, and that markers can exclusively be placed on top of game objects.
File: gamedata/scripts/my_task.script
--- Function used to retrieve the target of the mission (marker displayed (or not) in the PDA).
--- @param task_id number
--- @param field string
--- @param p any
--- @param tsk CGameTask
--- @return number
function task_functor.my_task_target_f(task_id,field,p,tsk)
if tsk.stage == 0 then
-- Stage 0 target : the quest item
local quest_item_se = get_story_se_object("my_task_quest_item")
return quest_item_se and quest_item_se.id
end
if tsk.stage == 1 then
-- Stage 1 target : the NPC that gave the task
return tsk.task_giver_id
end
end
IV.D. TASK STATUS FUNCTOR
The task status functor is a function that is automatically called every few seconds, by the game's task manager. This is where the heart of your task will be. Spawning objects, check if they still exist, if they have been killed, progressing the task, etc. Everything lies here.
There isn't really a convention for how to write tasks, but as a personal recommendation, I suggest you write your code using a sort of "return early" pattern. That means you write your code from top and downwards, and return as soon as a condition to progress isn't met.
File: gamedata/scripts/my_task.script
--- Function used to manage the mission logic as a whole.
--- @param tsk CGameTask
--- @param task_id number
--- @return string
function task_status_functor.my_task_status_f(tsk,task_id)
if tsk.stage == 0 then
-- logic of the first stage
end
if tsk.stage == 1 then
-- logic of the second stage
end
end
There are a few important things you need to manipulate in this function :
-
The task stage : you can use
tsk.stage
as both a getter and a setter for the stage :local stage = tsk.stage
to get the current stage;tsk.stage = 2
to set the current stage to stage 2;
-- We get the current stage of the task local stage = tsk.stage -- If we're in the correct stage, and the player has a certain info portion if stage == 0 and has_alife_info("the_player_has_done_something") then -- We progress the task to stage 1 tsk.stage = 1 end
-
The return value :
- use
return
(akareturn nil
) to stop the execution of the function "for now"; - use
return "complete"
to force-complete the task; - use
return "fail"
to force-fail the task;
If you use "return" or "return nil" (which is the same), the task manager will not execute the rest of the code for now - but it will have no effect on the status of the task. Remember the task manager calls your status functor every few seconds, so it will quickly call it again.
-- While the player hasn't done anything, we return (aka we wait) if not has_alife_info("the_player_has_done_something_great") and not has_alife_info("the_player_has_done_something_really_bad") then return -- If the player has done something great, the task is complete elseif has_alife_info("the_player_has_done_something_great") then return "complete" -- If the player has done something bad, the task is failed elseif has_alife_info("the_player_has_done_something_really_bad") then return "fail" end
- use