dc22af9e61
Waypoints can be added/removed via chat commands. This makes it easier to adapt to various "points of interest" that may evolve over time with the world.
476 lines
13 KiB
Lua
476 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 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) <= 4 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
|