Divergent/mods/PDA Taskboard/gamedata/scripts/ui_pda_taskboard_tab.script

474 lines
16 KiB
Plaintext

local xml = CScriptXmlInit()
xml:ParseFile("pda_taskboard.xml")
local SINGLETON = nil
function get_ui()
SINGLETON = SINGLETON or pda_taskboard_tab()
return SINGLETON
end
-- Constructor.
class "pda_taskboard_tab" (CUIScriptWnd)
function pda_taskboard_tab:__init() super()
self.rows = {}
self:InitControls()
end
-- Initialise the interface.
function pda_taskboard_tab:InitControls()
self:SetWndRect(Frect():set(0, 0, 1024, 768))
-- Main frame.
xml:ParseFile("pda_taskboard.xml")
xml:InitFrame("frame1", self)
xml:InitFrame("frame2", self)
-- Refresh tasks button
self.refresh_tasks_btn = xml:Init3tButton("btn_refresh_tasks", self)
self:Register(self.refresh_tasks_btn, "refresh_tasks_btn")
self:AddCallback("refresh_tasks_btn", ui_events.BUTTON_CLICKED, refresh_tasks_factory(self), self)
-- Taskboard
self.list = xml:InitScrollView("list", self)
self:Register(self.list, "list")
end
------------------- TASKBOARD
class "ui_taskboard_row" (CUIWindow)
function ui_taskboard_row:__init(parent, row) super(parent, row)
self.frame = xml:InitFrame("frame2", self)
-- Subcomponents
self.task_category = xml:InitTextWnd("task_category", self)
self.icon_field = xml:InitStatic("icon_field", self)
self.stalker_info = xml:InitTextWnd("stalker_info", self)
self.task_icon_field = xml:InitStatic("task_icon_field", self)
self.task_details_field = xml:InitTextWnd("task_details_field", self)
self.task_full_description_field = xml:InitTextWnd("task_full_description_field", self)
self.task_accept_btn = xml:Init3tButton("btn_accept_task", self)
self.task_next_btn = xml:Init3tButton("btn_next_task", self)
parent.rows[row] = self
parent:Register(self.task_accept_btn, "task_accept_btn_" .. row)
parent:AddCallback("task_accept_btn_" .. row, ui_events.BUTTON_CLICKED, accept_task_callback_factory(parent, row), self)
parent:Register(self.task_next_btn, "task_next_btn_" .. row)
parent:AddCallback("task_next_btn_" .. row, ui_events.BUTTON_CLICKED, next_task_in_category_callback_factory(parent, row), self)
end
local prepared_tasks_data = {}
local current_board_state = {}
function reset_taskboard(pda_tab)
clear_taskboard_ui(pda_tab)
local npc_list = get_nearby_npcs()
trigger_generate_available_tasks(npc_list)
prepared_tasks_data = get_prepared_task_data(npc_list)
local sorted_keys = get_sorted_keys(prepared_tasks_data)
-- Basically let XRay do its stuff with setting up logic
for _, task_effect in ipairs(sorted_keys) do
local task_data = prepared_tasks_data[task_effect][1]
prepare_task( task_data )
end
-- Delay required for task details from actor message to be properly collected
CreateTimeEvent("taskboard_delay_render", "taskboard_delay_render", 0.05, function ()
-- Generate taskboard entries
local i = 1
for _, task_effect in ipairs(sorted_keys) do
-- Save the order of the rows and currently viewed item from the category to be able to update a single row later
current_board_state[i] = {
task_effect = task_effect,
current_index = 1
}
update_task_entry(pda_tab, i, prepared_tasks_data[task_effect][1])
i = i + 1
end
a_taskboard_utils.adjust_rows(pda_tab)
return true
end)
end
currently_processed_npc_id = nil -- required for fetch quest override
function prepare_task(task_data)
currently_processed_npc_id = task_data.npc_id
local on_job_descr = task_data.task_id and task_manager.task_ini:r_string_ex(task_data.task_id,"on_job_descr")
if (on_job_descr) then
local cond = xr_logic.parse_condlist(db.actor,"task_manager","condlist",on_job_descr)
if (cond) then
xr_logic.pick_section_from_condlist(db.actor,db.actor,cond)
end
end
local fetch = task_data.task_id and task_manager.task_ini:r_string_ex(task_data.task_id,"fetch_descr")
if (fetch) then
axr_task_manager.trigger_fetch_func(task_data.task_id)
end
task_data.task_description = get_long_task_description(task_data)
currently_processed_npc_id = nil
end
function get_long_task_description(task_data)
-- Mechanic quests are using standard "job_descr" in the dialog manager instead of the fetch one, even though they're
-- technically a fetch task. This requires a small exception
local is_mechanic_task = string.find(task_data.task_id, "mechanic_task")
local base_desc = game.translate_string(
(not is_mechanic_task and axr_task_manager.get_fetch_task_description( task_data.task_id )) or
axr_task_manager.get_task_job_description( task_data.task_id )
)
return string.format( base_desc, dialogs._FETCH_TEXT or "" )
end
function accept_task_callback_factory(pda_tab, row_index)
return function()
accept_task(pda_tab, row_index)
end
end
function accept_task(pda_tab, row_index)
local entry_info = current_board_state[row_index]
local task_details = prepared_tasks_data[entry_info.task_effect][entry_info.current_index]
currently_processed_npc_id = task_details.npc_id
task_manager.get_task_manager():give_task(task_details.task_id, task_details.npc_id)
currently_processed_npc_id = nil
clear_cached_data()
reset_taskboard(pda_tab)
end
function next_task_in_category_callback_factory(pda_tab, row_index)
return function()
next_task_in_category(pda_tab, row_index)
end
end
function next_task_in_category(pda_tab, row_index)
local entry_info = current_board_state[row_index]
local next_task = prepared_tasks_data[entry_info.task_effect][entry_info.current_index + 1]
if next_task then
local stalker = level.object_by_id(next_task.npc_id)
if stalker then
prepare_task( next_task )
CreateTimeEvent("next_task_delay", "next_task_delay", 0.05, function ()
entry_info.current_index = entry_info.current_index + 1
update_task_entry(pda_tab, row_index, next_task)
a_taskboard_utils.adjust_rows(pda_tab)
return true
end)
else
-- In case the taskgiver has been deleted by the engine, reset the whole taskboard. Creating workaround
-- for deleted stalkers generates several edge cases and adds unnecessary complexity to the code - resetting is easier.
reset_taskboard(pda_tab)
end
end
end
function refresh_tasks_factory(pda_tab)
local refresh_disabled = false
local function temporary_disable_refresh()
local delay = 2
refresh_disabled = true
CreateTimeEvent("reenable_refresh", "reenable_refresh", delay, function ()
refresh_disabled = false
return true
end)
end
return function ()
if (not refresh_disabled) then
temporary_disable_refresh()
clear_cached_data()
reset_taskboard(pda_tab)
end
end
end
function clear_cached_data()
z_taskboard_overrides.clear_tasks_info()
prepared_tasks_data = {}
current_board_state = {}
end
function clear_taskboard_ui(pda_tab)
-- Erase all pre-existing information.
pda_tab.list:Clear()
pda_tab.rows = {}
end
local excluded_npc_sections = {"bar_duty_security_squad_leader"}
function get_nearby_npcs()
local radius = config.scanning_range
local npc_list = {}
level.iterate_nearest(db.actor:position(), radius, function (obj)
local is_alive_friendly_stalker = IsStalker(obj) and obj:alive() and obj:relation(db.actor) ~= game_object.enemy
local is_excluded_npc = has_value(excluded_npc_sections, obj:section())
local is_companion = obj:has_info("npcx_is_companion")
if
is_alive_friendly_stalker
and not is_excluded_npc
and (is_companion and config.companions_give_tasks or not is_companion) then
table.insert(npc_list, obj)
end
end)
-- Special case for Sidorovich and Forester.
local function add_by_sid(sid)
local id = story_objects.object_id_by_story_id[sid]
if (id) then
local npc = db.storage[id] and db.storage[id].object
if (npc and db.actor:position():distance_to(npc:position()) <= radius) then
table.insert(npc_list, npc)
end
end
end
add_by_sid("esc_m_trader")
add_by_sid("red_tech_forester")
return npc_list
end
local actually_is_sim = {
"bar_visitors_garik_stalker_guard", -- Garik
"bar_visitors_zhorik_stalker_guard2", -- Zhorik
"bar_arena_guard", -- Liolik
"bar_dolg_general_zoneguard_stalker", -- "Get out of here stalker" guy
"mil_smart_terrain_7_7_freedom_bodyguard_stalker" -- Lukash's bodyguards
}
function trigger_generate_available_tasks(npc_list)
for _,npc in pairs(npc_list) do
-- Sim npcs are random roaming stalkers. Non sims are traders, mechanics, etc.
local is_sim = string.find(npc:section(), "sim_")
currently_processed_npc_id = npc:id()
if has_value(actually_is_sim, npc:section()) then
axr_task_manager.generate_available_tasks(npc, not is_sim)
else
axr_task_manager.generate_available_tasks(npc, is_sim)
end
currently_processed_npc_id = nil
end
end
-- Tasks that should not be picked by the taskboard (e. g. ones that are not accessible without it)
local excluded_tasks = {
"jup_b19_freedom_yar_task_1", -- Yar's CoC task - bugged
"merc_pri_a18_mech_mlr_task_1", -- Outskirts merc mechanic task for tools - bugged
"agr_u_bandit_boss_task_1" -- Reefer's task (Agro underground) - it doesn't use the standard setup functions and breaks the board logic
}
function get_prepared_task_data(npc_list)
-- Split available tasks into categories based on their init effect
local result = {}
for _,stalker in pairs(npc_list) do
local stalker_task_list = axr_task_manager.available_tasks[stalker:id()] or {}
for _, task_id in pairs(stalker_task_list) do
local task_effect, has_details = get_task_effect(task_id)
if not has_value(excluded_tasks, task_id) then
if not result[task_effect] then
result[task_effect] = {}
end
table.insert(result[task_effect], {
npc_id = stalker:id(),
task_id = task_id,
has_details = has_details
})
end
end
end
return result
end
function get_task_effect(task_id) -- (task_id: string) => task_effect: string, has_details: boolean
-- Support for mods that add more tasks and dispatch effects of their own and can't be categorized using the base Anomaly task types
local task_category_override = task_manager.task_ini:r_string_ex(task_id,"task_category_override")
if task_category_override then return task_category_override end
local raw_task_effect = task_manager.task_ini:r_string_ex(task_id,"on_job_descr") or task_manager.task_ini:r_string_ex(task_id,"fetch_func")
local task_effect = string.gmatch(raw_task_effect or "", "=(.*)%(")() or "rest"
task_effect = normalize_task_effect(task_effect)
-- If there's no category string defined for a given effect/category override, default to `rest` to avoid ugly strings
-- Also makes it easy for modders to just dump all their tasks into `Other tasks` category by simply not defining category override in ini
local has_invalid_category = string.find(game.translate_string('st_pda_caption_' .. task_effect), '_')
if has_invalid_category then
return "rest", not not raw_task_effect
else
return task_effect, not not raw_task_effect
end
end
function update_task_entry(pda_tab, i, task_details)
if (not pda_tab.rows[i]) then
pda_tab.list:AddWindow(ui_taskboard_row(pda_tab, i))
end
pda_tab.rows[i].task_category:SetText(game.translate_string('st_pda_caption_' .. current_board_state[i].task_effect))
local stalker = level.object_by_id(task_details.npc_id)
local stalker_comm = stalker:character_community()
local stalker_icon = stalker:character_icon()
local stalker_name = stalker:character_name()
stalker_icon = stalker_icon and stalker_icon ~= "" and stalker_icon or "ui\\ui_noise"
pda_tab.rows[i].icon_field:InitTexture(stalker_icon)
local stalker_info = stalker_name .. "\\n" ..
game.translate_string("ui_st_community") .. ": " .. game.translate_string(stalker_comm)
pda_tab.rows[i].stalker_info:SetText(stalker_info)
local more_task_details = get_more_task_details(i) or {}
pda_tab.rows[i].task_icon_field:InitTexture(more_task_details.task_icon or "ui\\ui_noise")
local details = (more_task_details.task_title or "") ..
"\\n" ..
(string.gsub(more_task_details.task_details or "", "\\n ", "\\n"))
pda_tab.rows[i].task_details_field:SetText(details)
local full_desc = task_details.task_description or ""
pda_tab.rows[i].task_full_description_field:SetText(full_desc)
pda_tab.rows[i].task_next_btn:Show(has_more_task_in_category(i))
end
function has_more_task_in_category(i)
local entry_info = current_board_state[i]
local next_task = prepared_tasks_data[entry_info.task_effect][entry_info.current_index + 1]
return not not next_task
end
function get_igi_task_details(i)
if not igi_generic_task then return end
if not (igi_description and igi_description.get_task_text_values) then return end
local task_id = prepared_tasks_data.rest[i] and prepared_tasks_data.rest[i].task_id
local CACHE = task_id and igi_generic_task.TASK_SETUP[task_id]
if CACHE then
local title, text, icon = igi_description.get_task_text_values(CACHE)
return {
task_title = title,
task_details = text,
task_icon = icon
}
end
end
-- This function is a pure fuckery and crime against the code, but there's literally no other way to couple the
-- task data with the details received through overridden actor method.
function get_more_task_details(i)
local entry_info = current_board_state[i]
local task_data = prepared_tasks_data[entry_info.task_effect][entry_info.current_index]
if entry_info.current_index == 1 then
-- If no new task has been requested for a given category, index of details will be the same as the index of row (or at least I didn't observe any
-- deviations from that rule for now)
if(task_data.has_details) then
return z_taskboard_overrides.tasks_info[i - get_processed_tasks_with_no_info_count(i)]
elseif igi_generic_task then
return get_igi_task_details(entry_info.current_index)
else return end
else
-- If new task has been requested, the details will be appended at the end of the array, so we can easily read them
if(task_data.has_details) then
return z_taskboard_overrides.tasks_info[#z_taskboard_overrides.tasks_info]
elseif igi_generic_task then
return get_igi_task_details(entry_info.current_index)
else return end
end
end
function get_processed_tasks_with_no_info_count(i)
local count = 0
for index, entry_info in ipairs(current_board_state) do
local task_data = prepared_tasks_data[entry_info.task_effect][entry_info.current_index]
if index < i and not task_data.has_details then
count = count + 1
end
end
return count
end
function has_value(tab, val)
for index, value in ipairs(tab) do
if value == val then
return true
end
end
return false
end
function get_sorted_keys(tab)
local result = {}
for key, _ in pairs(tab) do
table.insert(result, key)
end
table.sort(result)
return result
end
-- Some effects have different names even though they perform pretty much the same actions as the generic/simulation task effects
-- Those should be pushed into the same category to prevent queued actor messages from being discarded
normalizer = {
setup_supplies_fetch_task_lostzone_patch = "setup_fetch_task",
setup_generic_fetch_task = "setup_fetch_task",
drx_sl_create_quest_stash = "setup_bounty_task", -- The actor message in base vanilla files is sent to the wrong queue here
setup_dominance_task = "setup_assault_task", -- Same as above - this actually scraps the whole Dominance category and puts these tasks as assault tasks
multifetch_on_job_descr = "setup_fetch_task" -- Unnecessary thing, but there's only one task in this category
}
function normalize_task_effect(task_effect)
return normalizer[task_effect] or task_effect
end
local function load_defaults()
local t = {}
local op = pda_taskboard_mcm.op
for i, v in ipairs(op.gr) do
if v.def ~= nil then
t[v.id] = v.def
end
end
return t
end
-- Default config
config = load_defaults()
local function load_settings()
config = load_defaults()
if ui_mcm then
for k, v in pairs(config) do
config[k] = ui_mcm.get("pda_taskboard/" .. k)
end
end
end
function on_key_release(key)
if key == config.taskboard_key then
local pda_menu = ActorMenu.get_pda_menu()
local pda3d = get_console_cmd(1,"g_3d_pda")
if not (pda_menu:IsShown()) and db.actor:item_in_slot(8) then
if (pda3d) then
db.actor:activate_slot(8)
else
pda_menu:ShowDialog(true)
end
pda_menu:SetActiveSubdialog("eptTaskboard")
elseif (pda_menu:IsShown()) then
if (pda3d) then
db.actor:activate_slot(0)
else
pda_menu:HideDialog()
end
end
end
end
function on_game_start()
RegisterScriptCallback("on_game_load", load_settings)
RegisterScriptCallback("on_option_change", load_settings)
RegisterScriptCallback("on_key_release",on_key_release)
end