Divergent/mods/Dynamic Zone Transitions/gamedata/scripts/dynamic_zone_news.script

340 lines
14 KiB
Plaintext

--[[
DYNAMIC ZONE - PDA News
Original Author(s)
Singustromo <singustromo at disroot.org>
VodoXleb <vodoxlebushek>
Edited by
License
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
(https://creativecommons.org/licenses/by-nc-sa/4.0)
--]]
parent = _G["dynamic_zone"]
if not (parent and parent.VERSION and parent.VERSION >= 20241224) then return end
--------------------------
-- Dependencies --
--------------------------
local opt = dynamic_zone_mcm
local utils = dynamic_zone_utils
local debug = dynamic_zone_debug
local route_manager = dynamic_zone_routes
local route_discovery = dynamic_zone_discovery
---------------------------------
-- Globals & Constants --
---------------------------------
CONST_LOGGING_PREFIX = "Dynamic News"
local log = debug.log_register("info", CONST_LOGGING_PREFIX)
local log_warn = debug.log_register("warning", CONST_LOGGING_PREFIX)
local log_err = debug.log_register("error", CONST_LOGGING_PREFIX)
CONST_RANDOMIZER_BEZIER_PARAMETERS = { 0, .0, .2, .9 } -- 41.9%
CONST_PROPAGATION_INTERVAL_MS = 20000
CONST_FALLBACK_DELAY_MIN_MS = 500
CONST_FALLBACK_DELAY_MAX_MS = CONST_PROPAGATION_INTERVAL_MS * 0.5
CONST_MESSAGE_MAX_STRINGS_TO_ITERATE = 20
CONST_STRING_ID_PREFIX = "st_dynzone_news"
CONST_STRING_ID_FALLBACK = CONST_STRING_ID_PREFIX .. "_fallback"
CONST_STRING_ID_SUFFIX_BLOCKED = "blocked"
CONST_STRING_ID_SUFFIX_OPENED = "opened"
CONST_NEWS_FALLBACK_ICON = "ui_iconsTotal_grouping"
CONST_MESSAGE_FALLBACK_NAME = "Bratan"
routes_to_reveal = {} -- routes whose changes will be propagated by other stalkers.
function clear()
log("Cleared route propagation queue")
routes_to_reveal = {}
end
special_characters = {
-- [story_id] = identifier (for strings)
["esc_m_trader"] = "sidorovich", -- Loner
["bar_dolg_general_petrenko_stalker"] = "petrenko", -- Duty
["mil_smart_terrain_7_7_freedom_leader_stalker"] = "lukash", -- Freedom
-- ["mar_smart_terrain_base_stalker_leader_marsh"] = "cold", -- Clear Sky
["yan_stalker_sakharov"] = "sakharov", -- Ecologists
["cit_killers_merc_trader_stalker"] = "dushman", -- Mercenaries
["agr_smart_terrain_1_6_near_2_military_colonel_kovalski"] = "kovalski", -- Military
["zat_b7_bandit_boss_sultan"] = "sultan", -- Bandits
["pri_monolith_monolith_trader_stalker"] = "krolik", -- Monolith
}
------------------------------
-- Just Shortcuts --
------------------------------
local ctime_to_tbl = utils_data.CTime_to_table
local tbl_to_ctime = utils_data.CTime_from_table
-- Using bezier curves for non-linear probability and to clamp result
-- Used so mean will be set percentage over time (of the time frame)
function get_random_time(lower, upper)
local randomizer = libmath_bezier and libmath_bezier.get_random_value
if utils.assert_failed(randomizer, "Bézier script not found. Using math.random()") then
return math.random(lower, upper)
end
return randomizer(lower, upper, CONST_RANDOMIZER_BEZIER_PARAMETERS)
end
-------------------------
-- Callbacks --
-------------------------
function on_game_start()
RegisterScriptCallback("dynzone_changed_block_state",
discover_changed_routes_via_news)
RegisterScriptCallback("actor_on_update", check_timed_unlocks)
end
function discover_changed_routes_via_news(previously_blocked_routes, new_blocked_routes)
clear()
queue_routes_for_propagation(previously_blocked_routes,
opt.get("percent_path_reveal_unblock"), CONST_STRING_ID_SUFFIX_OPENED)
queue_routes_for_propagation(new_blocked_routes,
opt.get("percent_path_reveal_block"), CONST_STRING_ID_SUFFIX_BLOCKED)
RegisterScriptCallback("actor_on_update", check_timed_unlocks)
end
-- Iterates through the route discovery queue and
-- checks if any of them are due to being propagated
check_timed_unlocks = utils.throttle(CONST_PROPAGATION_INTERVAL_MS, true, function()
if (utils.is_table_empty(routes_to_reveal)) then
log("No routes in the news queue. Unregistered Callback.")
UnregisterScriptCallback("actor_on_update", check_timed_unlocks)
return
end
local current_ctime = game.get_game_time()
log("Current time: %s", string.format("%d/%.2d/%.2d %.2d:%.2d:%.2d",
current_ctime:get(Y,M,D,h,m,s,ms)))
for route_id, data in pairs(routes_to_reveal) do
utils.assert(data, "Undefined Route data")
utils.assert(data.reveal_time, "Undefined Route reveal time")
local reveal_time = tbl_to_ctime(data.reveal_time)
local remaining_time_ms = (reveal_time:diffSec(current_ctime) / level.get_time_factor()) * 1000
reveal_time:sub(current_ctime)
if (remaining_time_ms >= CONST_PROPAGATION_INTERVAL_MS) then
local _, __, d, h, m, s, ___ = reveal_time:get(Y,M,D,h,m,s,ms)
log("Route #%s will be propagated in %sd %sh %sm %ss",
route_id, (d -1), h, m, s)
goto continue
end
remaining_time_ms = (remaining_time_ms < 0)
and math.random(CONST_FALLBACK_DELAY_MIN_MS, CONST_FALLBACK_DELAY_MAX_MS)
or remaining_time_ms
log("Route #%s will be propagated in %ss (realtime)", route_id,
string.format("%.1f", remaining_time_ms / 1000))
utils.assert(data.reveal_type, "Undefined route reveal type!")
utils.timed_call(remaining_time_ms, function()
propagate_route_state_change(route_id, data.reveal_type)
return true
end)
:: continue ::
end
end)
--------------------------
-- Main logic --
--------------------------
function propagate_route_state_change(route_id, reveal_type)
if not utils.valid_type{ caller = "propagate_route_state_change",
"int", route_id } then return end
local route = route_manager.get_route(route_id)
local levels, sender = route and route.connects
if math.random() > (opt.get("chance_special_character") / 100) then
sender = find_random_stalker_on_level_pair(levels)
else
sender = find_special_character_on_level_pair(levels)
end
local route_state_differs = route_manager.route_known_state_differs(route_id)
local play_discovery_sound = opt.get("news_play_discovery_sound") and route_state_differs
if not utils.assert_failed(sender, "Sender for route #%s is nil!", route_id) then
local anomaly_theme = route_manager.get_route_property(route_id, "anomaly_theme")
send_route_news_tip(reveal_type, 0, sender, 10, levels, anomaly_theme, play_discovery_sound)
end
-- TODO: Check, if still eligible to prevent log spamming at this point
-- We still want to propagate the information, nonetheless.
log("Revealing %s state of route #%s via news.", reveal_type, route_id)
route_discovery.toggle_known_route_state(route_id, play_discovery_sound)
routes_to_reveal[route_id] = nil
end
function queue_routes_for_propagation(selected_routes, percent_to_reveal, reveal_type)
local indexed_routes = {}
utils.index_keys(selected_routes, indexed_routes)
-- time before next emission when all reveal messages should be posted
local next_emission_in_hours = surge_manager and surge_manager.get_surge_manager
and (surge_manager.get_surge_manager()._delta / (60^2)) -- game time
if (not next_emission_in_hours) then
next_emission_in_hours = ui_options.get("alife/event/emission_frequency")
log_warn("Surge manager not available, using frequency set in options.")
end
local revealed = 1
local amount_to_reveal = math.floor(#indexed_routes * (percent_to_reveal/100))
for id in utils.random_numbered_sequence(1, #indexed_routes) do
if (revealed > amount_to_reveal) then goto continue end
revealed = revealed +1
local minutes = 6 * get_random_time(3, next_emission_in_hours * 10)
local hours = math.floor(minutes / 60)
minutes = minutes % 60
local days = math.floor(hours / 24)
hours = hours % 24
local add_time = game.CTime()
-- days +1 so we get proper result (engine quirk)
add_time:set(1, 1, (days +1), hours, minutes, math.random(0, 59), 0)
local selected_time = game.get_game_time()
selected_time:add(add_time)
log("Queued route #%s for propagation (%s)", id, reveal_type)
routes_to_reveal[indexed_routes[id]] = {
route = selected_routes[indexed_routes[id]],
reveal_time = ctime_to_tbl(selected_time), -- don't save userdata
reveal_type = reveal_type,
}
:: continue ::
end
end
function find_random_stalker_on_level_pair(levels)
if not utils.valid_type{ caller = "find_random_stalker_on_level_pair",
"tbl", levels } then return end
for id in utils.random_numbered_sequence(1, 2^16 -2) do
local se_obj = alife():object(id)
if not (se_obj) then goto continue end
if IsStalker(nil, se_obj:clsid())
and se_obj:alive()
and se_obj:community() ~= "zombied"
and se_obj:community() ~= "trader"
and se_obj:community() ~= "greh"
and se_obj:community() ~= "isg"
and se_obj:community() ~= "renegade"
and se_obj.group_id ~= 65535
and (get_object_story_id(se_obj.group_id) == nil)
and string.find(se_obj:name(),"sim_default_")
and utils.table_has(levels, utils.get_mapname(se_obj)) then
return id
end
:: continue ::
end
end
function find_special_character_on_level_pair(levels)
if not utils.valid_type{ caller = "find_special_character_on_level_pair",
"tbl", levels } then return end
local found_stalkers = {}
for k, identifier in pairs(special_characters) do
local id = get_story_object_id(k)
local se_obj = id and alife_object(id)
if not (se_obj) then goto continue end
local character_map = utils.get_mapname(se_obj)
local connected_maps = character_map and utils.get_directly_connected_maps(character_map)
-- Route should at least be indirectly connected to the senders map
if utils.table_contains(connected_maps, levels) then
found_stalkers[#found_stalkers + 1] = se_obj.id
end
:: continue ::
end
return found_stalkers[math.random(1, #found_stalkers)]
or find_random_stalker_on_level_pair(levels)
end
function send_route_news_tip(news_reveal_type, timeout, sender_id, showtime, news_levels, news_anom_theme, suppress_pda_sound)
timeout = timeout or 0
showtime = showtime or 5
local npc = alife():object(sender_id)
if (not npc) then return end
local actor = db.actor
local texture = (npc.character_icon) and npc:character_icon() or CONST_NEWS_FALLBACK_ICON
local special_character_nickname = special_characters[get_object_story_id(npc.id)]
local npc_story_id = special_character_nickname or npc:community()
local msg_to_translate = string.format("%s_%s_%s_", CONST_STRING_ID_PREFIX, npc_story_id, news_reveal_type)
local phrases = {}
for i = 1, CONST_MESSAGE_MAX_STRINGS_TO_ITERATE do
if (utils.has_translation(msg_to_translate .. i)) then
phrases[#phrases +1] = i
end
end
if (utils.is_table_empty(phrases)) then
log_warn("Undefined string group: '%s'. Using fallback.", msg_to_translate:sub(1, -2))
msg_to_translate = CONST_STRING_ID_FALLBACK .. "_" .. news_reveal_type
else
msg_to_translate = msg_to_translate .. phrases[math.random(1, #phrases)]
end
-- Make sure $FROM is the senders level, when route connects sender map
local sender_level = utils.get_mapname(npc)
local index = utils.index_of(news_levels, sender_level) or math.random(1, #news_levels)
local level_from, level_to = news_levels[index], news_levels[#news_levels -index +1]
utils.assert(level_from and level_to)
local character_name = (npc.character_name) and npc:character_name()
local news_caption = character_name or game.translate_string("st_tip")
-- TODO: Maybe need a debug mode flag for cases such as this
news_caption = (opt.get("verbose"))
and string.format("%s (%s)", news_caption, utils.get_mapname(npc))
or news_caption
local anomaly_theme_string = dynamic_zone_anomalies.get_theme_string(news_anom_theme) or ""
local news_text = game.translate_string(msg_to_translate)
news_text = news_text:gsub("%$SENDER_NAME", character_name or CONST_MESSAGE_FALLBACK_NAME)
news_text = news_text:gsub("%$FROM", game.translate_string(level_from))
news_text = news_text:gsub("%$TO", game.translate_string(level_to))
news_text = news_text:gsub("%$ANOM_THEME", game.translate_string(anomaly_theme_string))
actor:give_game_news(news_caption, news_text, texture, timeout *1000, showtime *1000, 0)
if (not suppress_pda_sound) then
xr_sound.set_sound_play(AC_ID, "pda_tips") -- play default sound
end
end