2023-01-14 10:52:03 -05:00

477 lines
13 KiB
Lua

-- LUALOCALS < ---------------------------------------------------------
local ipairs, math, minetest, pairs, rawget, rawset, table, tonumber,
type
= ipairs, math, minetest, pairs, rawget, rawset, table, tonumber,
type
local math_atan2, math_ceil, math_cos, math_pi, math_random, math_sin,
math_sqrt, table_concat
= math.atan2, math.ceil, math.cos, math.pi, math.random, math.sin,
math.sqrt, table.concat
-- LUALOCALS > ---------------------------------------------------------
local modname = minetest.get_current_modname()
local modstore = minetest.get_mod_storage()
local api = rawget(_G, modname) or {}
rawset(_G, modname, api)
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 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
return true, modname .. " config reloaded"
end
reconfigure()
minetest.register_chatcommand(modname, {
description = "Reload " .. modname .. " config from settings",
privs = {server = true},
func = reconfigure
})
local waypoints
do
local wpdistance = 8
local explicit_wps
local function waypoint_publish()
waypoints = explicit_wps
if #waypoints < 1 then
local ss = minetest.setting_get_pos("static_spawn")
if ss then waypoints = {ss} end
end
if #waypoints < 1 then
waypoints = {vector.new(0, 0, 0)}
end
end
do
local s = modstore:get_string("waypoints")
explicit_wps = s and minetest.deserialize(s) or {}
waypoint_publish()
end
local function wprm(pos)
for i = #explicit_wps, 1, -1 do
local wp = explicit_wps[i]
if vector.distance(wp, pos) <= wpdistance then
explicit_wps[i] = explicit_wps[#explicit_wps]
explicit_wps[#explicit_wps] = nil
end
end
end
local function wprpt()
local tbl = {modname, "waypoints:"}
for i = 1, #explicit_wps do
tbl[#tbl + 1] = minetest.pos_to_string(explicit_wps[i], 0)
end
tbl[#tbl + 1] = "total=" .. #explicit_wps
return true, table_concat(tbl, " ")
end
local function waypoint_rmadd(add)
return function(name)
local player = minetest.get_player_by_name(name)
local pos = player and player:get_pos()
if not pos then return false, "must be online" end
wprm(pos)
if add then explicit_wps[#explicit_wps + 1] = pos end
waypoint_publish()
modstore:set_string("waypoints", minetest.serialize(explicit_wps))
return wprpt()
end
end
minetest.register_chatcommand(modname .. "_wpls", {
description = "List current " .. modname .. " waypoints",
privs = {server = true},
func = wprpt
})
minetest.register_chatcommand(modname .. "_wprm", {
description = "Remove current location from " .. modname .. " waypoints",
privs = {server = true},
func = waypoint_rmadd()
})
minetest.register_chatcommand(modname .. "_wpset", {
description = "Add current location to " .. modname .. " waypoints",
privs = {server = true},
func = waypoint_rmadd(true)
})
minetest.register_chatcommand(modname .. "_wpclear", {
description = "Reset all " .. modname .. " waypoints",
privs = {server = true},
func = function()
explicit_wps = {}
waypoint_publish()
modstore:set_string("waypoints", minetest.serialize(explicit_wps))
return wprpt()
end
})
end
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
function api.get_watchable_player_names()
local names = {}
for _, p in ipairs(minetest.get_connected_players()) do
local n = p:get_player_name()
local props = p:get_properties()
local vs = props and props.visual_size
if vs and vs.x > 0 and vs.y > 0 then
names[n] = true
end
end
return names
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 n in pairs(api.get_watchable_player_names()) do
if n ~= pname then q[#q + 1] = n end
end
for i = #q, 2, -1 do
local j = math_random(1, i)
q[i], q[j] = q[j], q[i]
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)
local status = minetest.get_server_status
function minetest.get_server_status(name, ...)
if enabled_players[name] and
not minetest.settings:get_bool(modname .. "_statusline") then
return ""
end
return status(name, ...)
end