-- LUALOCALS < --------------------------------------------------------- local ipairs, math, minetest, pairs, tonumber, type = ipairs, math, minetest, pairs, tonumber, type local math_atan2, math_ceil, math_cos, math_pi, math_random, math_sin, math_sqrt = math.atan2, math.ceil, math.cos, math.pi, math.random, math.sin, math.sqrt -- LUALOCALS > --------------------------------------------------------- local modname = minetest.get_current_modname() local defaults = { cycletime = 20, -- time between cycles hudtime = 4, -- duration to display HUD hudreshow = 60, -- interval to re-display HUD hudmargin = 0, -- bottom margin for HUD dummyradius = 80, -- max radius for dummy cam dummyheight = 32, -- max height for dummy cam dummymin = 0.25, -- min radius (of max) for dummy cam dummyclear = 16, -- min clear sight for dummy cam vellead = -4, -- ratio of player vel for camera pos "leading/chasing" facelead = -2, -- ratio of player facing for camera pos lead/chase camdistset = 4, -- initial target distance from player to set camera camdistmin = 1, -- min distance from player to allow camera camdistmax = 16, -- max distance from player to allow camera camfov = 72, -- assume camera fov for angle check camlocktime = 1, -- min time to lock camera after moving minlight = 4, -- min light level to not affect cam time maxidle = 10, -- max idle time before penalty idlepenalty = 6, -- extra countdown speed for being idle actvig = 0.5, -- vignette when active actdark = 0, -- darken when active dummyvig = 0.75, -- vignette when dummycam dummydark = 0.3, -- darken when dummycam } local config local enabled_players local waypoints local function reconfigure() config = {} for k in pairs(defaults) do config[k] = tonumber(minetest.settings:get(modname .. "_" .. k)) or defaults[k] end enabled_players = {} for _, n in pairs((minetest.settings:get(modname .. "_players") or ""):split(',')) do if n ~= "" then enabled_players[n] = true end end waypoints = {} local wpstr = minetest.settings:get(modname .. "_waypoints") or "" if wpstr == "" then wpstr = minetest.settings:get("static_spawn") or "" end for _, n in pairs(wpstr:split(';')) do local xyz = n:split(",") if #xyz == 3 then waypoints[#waypoints + 1] = vector.new(xyz[1], xyz[2], xyz[3]) end end if #waypoints < 1 then waypoints[1] = vector.new(0, 0, 0) end return true, modname .. " config reloaded" end reconfigure() minetest.register_chatcommand(modname, { description = "Reload " .. modname .. " config from settings", privs = {server = true}, func = reconfigure }) local function setcamera(player, pos, target) local delta = vector.normalize(vector.subtract(target, pos)) player:set_look_horizontal(math_atan2(delta.z, delta.x) - math_pi / 2) player:set_look_vertical(-math_atan2(delta.y, math_sqrt( delta.x ^ 2 + delta.y ^ 2))) pos.y = pos.y - player:get_properties().eye_height return player:set_pos(pos) end local allow local fluidmedium do local allow_drawtypes = { airlike = true, torchlike = true, signlike = true, raillike = true, plantlike = true, firelike = true, liquid = true, flowingliquid = true, glasslike = true, glasslike_framed = true, glasslike_framed_optional = true, allfaces = true, allfaces_optional = true, } local allow_nodes = { ignore = true } local fluids = {} minetest.after(0, function() for k, v in pairs(minetest.registered_nodes) do if allow_drawtypes[v.drawtype] and v.paramtype == "light" then allow_nodes[k] = true end if v.sunlight_propagates then fluids[k] = "CLEAR" else fluids[k] = v.liquid_alternative_source or k end end end) allow = function(pos) return allow_nodes[minetest.get_node(pos).name] end fluidmedium = function(pos) return fluids[minetest.get_node(pos).name] end end local function sightblocked(from, to) local fsrc = fluidmedium(from) for pt in minetest.raycast(from, to, false, true) do if pt.type == "node" then if fluidmedium(pt.under) ~= fsrc then return pt.above end end end if fluidmedium(to) ~= fsrc then return to end end local getdata do local datastore = {} function getdata(p) local pname = type(p) == "string" and p or p:get_player_name() local data = datastore[pname] if not data then data = {} datastore[pname] = data end return data end minetest.register_on_leaveplayer(function(player) datastore[player:get_player_name()] = nil end) end local function hudset(player, data, key, def) local hud = data[key] if hud then if hud.text == def.text then return end player:hud_change(hud.id, "text", def.text) hud.text = def.text else data[key] = { id = player:hud_add(def), text = def.text } end end local function dimhuds(player, data, darken, vignette) hudset(player, data, "darken", { hud_elem_type = "image", position = {x = 0.5, y = 0.5}, text = darken == 0 and "" or ( "[combine:1x1^[noalpha^[opacity:" .. math_ceil(255 * darken)), direction = 0, scale = {x = -100, y = -100}, offset = {x = 0, y = 0}, quick = true }) hudset(player, data, "vignette", { hud_elem_type = "image", position = {x = 0.5, y = 0.5}, text = vignette == 0 and "" or ( "szutil_cinecam_vignette.png^[opacity:" .. math_ceil(255 * vignette)), direction = 0, scale = {x = -100, y = -100}, offset = {x = 0, y = 0}, quick = true }) end local function camdummy(player, data, dtime) data.dummycam = (data.dummycam or 0) - dtime if data.dummycam > 0 then local pos = player:get_pos() pos.y = pos.y + player:get_properties().eye_height if allow(pos) then return end end local theta = math_random() * math_pi * 2 local dummyradius = config.dummyradius local dpos = { x = math_cos(theta) * dummyradius, y = math_random() * config.dummyheight, z = math_sin(theta) * dummyradius } local dummymin = config.dummymin dpos = vector.multiply(dpos, dummymin + math_random() * (1 - dummymin)) local wp = waypoints[math_random(1, #waypoints)] or vector.new(0, 0, 0) local cpos = vector.add(wp, dpos) if not allow(cpos) then return end local len = vector.length(dpos) local tpos = vector.add(wp, vector.multiply(dpos, (len - config.dummyclear) / len)) if sightblocked(cpos, tpos) then return end dimhuds(player, data, config.dummydark, config.dummyvig) setcamera(player, cpos, tpos) data.targethud = nil data.dummycam = config.cycletime end local function camcheck(player, dtime) local data = getdata(player) if player:get_fov() ~= config.camfov then player:set_fov(config.camfov) end local text = data.targethud or "" if data.tiptext ~= text then data.tiptime = 0 end data.tiptext = text if data.tiptime > config.hudtime then text = "" end data.tiptime = data.tiptime + dtime if data.tiptime > config.hudreshow then data.tiptime = data.tiptime - config.hudreshow end hudset(player, data, "tip", { hud_elem_type = "text", position = {x = 0.5, y = 1}, text = text, number = 0xFFFFFF, alignment = {x = 0, y = -1}, offset = {x = 0, y = -config.hudmargin}, }) if data.locked then data.locked = data.locked - dtime if data.locked > 0 then return end data.locked = nil end if data.tracktime then data.tracktime = data.tracktime - dtime if data.tracktime <= 0 then data.tracktime = nil end end local target = data.tracktime and data.target target = target and minetest.get_player_by_name(target) if not target then data.target = nil if not (data.queue and #data.queue > 0) then local q = {} local pname = player:get_player_name() for _, p in ipairs(minetest.get_connected_players()) do local n = p:get_player_name() if n ~= pname then local props = p:get_properties() local vs = props and props.visual_size if vs and vs.x > 0 and vs.y > 0 then q[#q + 1] = n end end end if #q < 1 then return camdummy(player, data, dtime) end data.queue = q end data.target = data.queue[#data.queue] data.queue[#data.queue] = nil data.tracktime = config.cycletime target = minetest.get_player_by_name(data.target) if not target then return camdummy(player, data, dtime) end data.dummycam = nil end local pos = player:get_pos() pos.y = pos.y + player:get_properties().eye_height local tpos = target:get_pos() tpos.y = tpos.y + target:get_properties().eye_height local tidle = getdata(target).idletime or 0 if tidle >= config.maxidle then data.tracktime = data.tracktime - dtime * config.idlepenalty end local camdistset = config.camdistset local camdistmax = config.camdistmax local function newcam() local theta = math_random() * math_pi * 2 local trypos = vector.add(tpos, vector.multiply({ x = math_cos(theta), y = math_random() * 2 - 0.5, z = math_sin(theta) }, camdistmax)) trypos = vector.add(trypos, vector.multiply( target:get_velocity(), config.vellead)) trypos = vector.add(trypos, vector.multiply( target:get_look_dir(), config.facelead)) local dist = vector.distance(trypos, tpos) if dist > camdistset then trypos = vector.add(tpos, vector.multiply( vector.direction(tpos, trypos), camdistset)) end local bpos = sightblocked(tpos, trypos) if bpos and vector.distance(tpos, bpos) < config.camdistmin then return end trypos = bpos or trypos if sightblocked(trypos, tpos) then return end if not allow(trypos) then return end dimhuds(player, data, config.actdark, config.actvig) setcamera(player, trypos, tpos) data.targethud = "Watching: " .. data.target data.moved = 0 data.locked = config.camlocktime end if not allow(pos) then return newcam() end local tlight = nodecore and nodecore.get_node_light(tpos) or minetest.get_node_light(tpos) local minlight = config.minlight if tlight and tlight < minlight then data.tracktime = data.tracktime - dtime * minlight / tlight end local dist = vector.distance(pos, tpos) if dist < 1 or dist > camdistmax then return newcam() end local look = player:get_look_dir() local angle = vector.angle(vector.direction(pos, tpos), look) if angle > config.camfov / 2/180 * math_pi then return newcam() end if sightblocked(pos, tpos) then return newcam() end data.moved = (data.moved or 0) + dtime if data.moved > config.cycletime then return newcam() end end minetest.register_globalstep(function(dtime) local players = minetest.get_connected_players() for _, player in ipairs(players) do local pos = player:get_pos() local look = player:get_look_dir() local ctl = player:get_player_control() local data = getdata(player) if not (ctl.up or ctl.down or ctl.left or ctl.right or ctl.jump or ctl.LMB or ctl.RMB) and data.idlepos and data.idlelook and vector.equals(pos, data.idlepos) and vector.equals(look, data.idlelook) then data.idletime = (data.idletime or 0) + dtime else data.idletime = 0 end data.idlepos = pos data.idlelook = look end for _, player in ipairs(players) do if enabled_players[player:get_player_name()] then camcheck(player, dtime) end end end)