Divergent/mods/Dialog Dynamic UI/gamedata/scripts/dialog_fov.script

383 lines
9.3 KiB
Plaintext
Raw Normal View History

2024-03-17 20:18:03 -04:00
-- UTILS
-- Similar floats
local function similar(f1, f2, e)
return math.abs(f1 - f2) <= (e or 0.001)
end
-- Truncate to a decimal
local function trunc(float, decimal)
local d = 10 ^ (decimal or 1)
return math.floor(float * d) / d
end
-- Linear inter/extrapolation
local function lerp(a, b, f)
if a and b and f then
return a + f * (b - a)
else
return a or b or 0
end
end
-- Time delta
local function time_delta()
return math.min(50, device().time_delta)
end
-- LUT Lookup
local function array_keys(t, sorted, sort_func)
local res = {}
local res_count = 1
for k, v in pairs(t) do
res[res_count] = k
res_count = res_count + 1
end
if sorted then
if sort_func then
table.sort(res, sort_func)
else
table.sort(res)
end
end
return res
end
local function bisect_left(a, x, lo, hi)
local lo = lo or 1
local hi = hi or #a
if lo < 0 then
printf('bisect, lo must be non-negative')
return
end
while lo < hi do
local mid = math.floor((lo + hi) * 0.5)
if a[mid] < x then
lo = mid+1
else
hi = mid
end
end
return lo
end
local function lookup(t, key, tkeys)
if is_empty(t) then return 0 end
if t[key] then return t[key] end
if not tkeys then
tkeys = array_keys(t, true)
end
local tkeys_len = #tkeys
if key <= tkeys[1] then
if key == tkeys[1] then
return t[tkeys[1]]
elseif not tkeys[2] then
return t[tkeys[1]]
else
local hi = tkeys[2]
local lo = tkeys[1]
local k = (key - lo) / (hi - lo)
return lerp(t[lo], t[hi], k)
end
end
if key >= tkeys[tkeys_len] then
if key == tkeys[tkeys_len] then
return t[tkeys[tkeys_len]]
elseif not tkeys[tkeys_len - 1] then
return t[tkeys[tkeys_len]]
else
local hi = tkeys[tkeys_len]
local lo = tkeys[tkeys_len - 1]
local k = (key - lo) / (hi - lo)
return lerp(t[lo], t[hi], k)
end
end
local where = bisect_left(tkeys, key)
local lo = tkeys[where-1] or tkeys[where]
local hi = tkeys[where]
if lo == hi then return t[lo] end
local delta = (key - lo) / (hi - lo)
local res = delta * t[hi] + (1 - delta) * t[lo]
--printf(res)
return res
end
-- EMA smoothing for changing values, frame independent
local default_smoothing = 11.5
local smoothed_values = {}
local function ema(key, value, def, steps, delta)
local steps = steps or default_smoothing
local delta = delta or steps
local smoothing_alpha = 2.0 / (steps + 1)
smoothed_values[key] = smoothed_values[key] and smoothed_values[key] + math.min(smoothing_alpha * (delta / steps), 1) * (value - smoothed_values[key]) or def or value
--printf("EMA fired, key %s, target %s, current %s, going %s", key, value, smoothed_values[key], (value > smoothed_values[key] and "up" or "down"))
return smoothed_values[key]
end
special_characters = {
["m_trader"] = "esc_2_12_stalker_trader",
["m_lesnik"] = "red_forester_tech",
["zat_a2_stalker_nimble"] = "esc_2_12_stalker_nimble",
}
-- LUT for rotating camera to the angle depending on aspect ratio of display (device().width / device().height)
local ratio_to_rotate = {
[1.333] = -11,
[1.600] = -13.5,
[1.777] = -15.5,
[2.370] = -21.5,
[3.555] = -31,
}
local ratio_to_rotate_keys = array_keys(ratio_to_rotate, true)
-- Vars
local saved_fov
local zoom_fov_from
local zoom_fov_to
local zoom_fov_active = false
local zoom_fov_active_always = false
local saved_hud_fov
local zoom_hud_fov_from
local zoom_hud_fov_to
local saved_npc
local saved_dist
local saved_rotate
-- Get NPC actor talks to
function valid_npc(npc)
return npc and (special_characters[npc:section()] or IsStalker(npc) and npc:alive() and npc:is_talking() and npc:id() ~= AC_ID)
end
function GetTalkingNpc()
local npc = get_speaker()
if valid_npc(npc) then
return npc
2024-03-17 20:18:03 -04:00
end
-- for i=1, #db.OnlineStalkers do
-- local st = db.storage[db.OnlineStalkers[i]]
-- local npc = st and st.object or level.object_by_id(db.OnlineStalkers[i])
-- if valid_npc(npc) then
-- return npc
-- end
-- end
2024-03-17 20:18:03 -04:00
end
-- Move camera so the npc is in the left
function move_camera()
if not saved_npc then
return stop_move_camera()
end
-- Get look position and direction
local look_pos = utils_obj.safe_bone_pos(saved_npc, "bip01_head")
local look_dir = look_pos:sub(device().cam_pos):normalize()
-- Turn look direction depending on aspect ratio of display
local rotate_deg
if saved_rotate then
rotate_deg = saved_rotate
else
local ratio = trunc(device().width / device().height, 3)
rotate_deg = lookup(ratio_to_rotate, ratio, ratio_to_rotate_keys)
saved_rotate = rotate_deg
end
if saved_dist and settings.zoom_fov_enabled and settings.distance_fov then
rotate_deg = rotate_deg / saved_dist
end
rotate_deg = rotate_deg * settings.turn_modifier
local new_look_dir = vector_rotate_y(look_dir, rotate_deg)
-- Smooth the turning for better effect
local steps = settings.smoothing
local delta = time_delta()
local smoothed_look_dir = vector():set(
ema("look_dir_x", new_look_dir.x, look_dir.x, steps, delta),
ema("look_dir_y", new_look_dir.y, look_dir.y, steps, delta),
ema("look_dir_z", new_look_dir.z, look_dir.z, steps, delta)
)
-- Apply the turning
local new_look_pos = vector():mad(device().cam_pos, smoothed_look_dir, 10)
db.actor:actor_look_at_point(new_look_pos)
end
function start_move_camera()
RegisterScriptCallback("actor_on_update", move_camera)
end
function stop_move_camera()
UnregisterScriptCallback("actor_on_update", move_camera)
if db.actor then
if db.actor.actor_stop_look_at_point then
db.actor:actor_stop_look_at_point()
end
end
smoothed_values.look_dir_x = nil
smoothed_values.look_dir_y = nil
smoothed_values.look_dir_z = nil
saved_rotate = nil
saved_npc = nil
saved_dist = nil
end
function GUI_on_show(name)
if name == "Dialog" then
saved_npc = GetTalkingNpc()
saved_dist = saved_npc and math.max(1, device().cam_pos:distance_to(utils_obj.safe_bone_pos(saved_npc, "bip01_head")) - 0.2)
2024-03-17 20:18:03 -04:00
if settings.zoom_fov_enabled then
saved_fov = zoom_fov_active and saved_fov or get_console_cmd(2, "fov")
local new_fov = math.min(settings.min_zoom_fov, saved_fov / settings.zoom_fov_modifier)
if saved_dist and settings.distance_fov then
new_fov = math.atan(math.tan(new_fov * (0.5 * math.pi / 180)) / saved_dist) / (0.5 * math.pi / 180)
end
saved_hud_fov = zoom_fov_active and saved_hud_fov or get_console_cmd(2, "hud_fov")
local new_hud_fov = saved_hud_fov / (saved_fov / new_fov)
start_fov(saved_fov, new_fov, saved_hud_fov, new_hud_fov, true)
end
if settings.track_npc then
start_move_camera()
end
end
end
function GUI_on_hide(name)
if name == "Dialog" then
reset(true)
end
end
-- Smooth changing of FOV
-- start_fov(fov_from, fov_to, fov_hud_from, fov_hud_to, active_always) initiate the changing procedure
function change_fov()
local steps = settings.fov_smoothing
local delta = time_delta()
local smoothed_fov = ema("fov", zoom_fov_to, zoom_fov_from, steps, delta)
exec_console_cmd("fov " .. smoothed_fov)
local smoothed_hud_fov = ema("hud_fov", zoom_hud_fov_to, zoom_hud_fov_from, steps, delta)
exec_console_cmd("hud_fov " .. smoothed_hud_fov)
if not zoom_fov_active_always and similar(smoothed_fov, zoom_fov_to, 0.02) then
exec_console_cmd("fov " .. zoom_fov_to)
exec_console_cmd("hud_fov " .. zoom_hud_fov_to)
stop_fov()
end
end
function start_fov(fov_from, fov_to, fov_hud_from, fov_hud_to, active_always)
zoom_fov_from = fov_from or get_console_cmd(2, "fov")
zoom_fov_to = fov_to
zoom_hud_fov_from = fov_hud_from or get_console_cmd(2, "hud_fov")
zoom_hud_fov_to = fov_hud_to
zoom_fov_active = true
zoom_fov_active_always = active_always
RegisterScriptCallback("actor_on_update", change_fov)
end
function stop_fov()
smoothed_values.fov = nil
smoothed_values.hud_fov = nil
zoom_fov_active = false
zoom_fov_active_always = false
UnregisterScriptCallback("actor_on_update", change_fov)
end
function reset_fov()
stop_fov()
if not saved_fov then return end
exec_console_cmd("fov " .. saved_fov)
exec_console_cmd("hud_fov " .. saved_hud_fov)
saved_fov = nil
saved_hud_fov = nil
2024-03-17 20:18:03 -04:00
end
function reset(soft_fov)
if soft_fov then
if saved_fov then
start_fov(zoom_fov_to, saved_fov, zoom_hud_fov_to, saved_hud_fov)
end
else
reset_fov()
end
stop_move_camera()
end
function hard_reset()
reset()
end
-- MCM
function load_defaults()
local t = {}
local op = dialog_fov_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
settings = load_defaults()
function load_settings()
settings = load_defaults()
if ui_mcm then
for k, v in pairs(settings) do
settings[k] = ui_mcm.get(dialog_fov_mcm.op_id .. "/" .. k)
end
end
return settings
end
function on_option_change()
load_settings()
switch_mod(settings.enabled)
end
function switch_mod(enable)
local funcs = {
{"GUI_on_show", GUI_on_show},
{"GUI_on_hide", GUI_on_hide},
{"actor_on_net_destroy", hard_reset},
{"actor_on_before_death", hard_reset},
}
if enable then
for i, v in ipairs(funcs) do
RegisterScriptCallback(v[1], v[2])
end
else
for i, v in ipairs(funcs) do
UnregisterScriptCallback(v[1], v[2])
end
reset()
return
end
if not settings.zoom_fov_enabled then
reset_fov()
end
if not settings.track_npc then
stop_move_camera()
end
end
function on_game_start()
RegisterScriptCallback("actor_on_first_update", on_option_change)
RegisterScriptCallback("on_option_change", on_option_change)
switch_mod(true)
end