477 lines
13 KiB
Lua
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
|