warpfield/init.lua

397 lines
13 KiB
Lua

warpfield = {}
local S = minetest.get_translator()
local warpfield_trigger_uses = tonumber(minetest.settings:get("warpfield_trigger_uses")) or 0
local warpfield_trigger_cooldown = tonumber(minetest.settings:get("warpfield_cooldown")) or 10
local default_x = {
octaves = 1,
scale = "500",
lacunarity = "2",
flags = "",
spread = {
y = "800",
x = "800",
z = "800"
},
seed = 33356,
offset = "1",
persistence = "0.5"
}
local default_y = {
octaves = 1,
scale = "100",
lacunarity = "2",
flags = "",
spread = {
y = "200",
x = "200",
z = "200"
},
seed = 33357,
offset = "1",
persistence = "0.5"
}
local default_z = {
octaves = 1,
scale = "500",
lacunarity = "2",
flags = "",
spread = {
y = "800",
x = "800",
z = "800"
},
seed = 33358,
offset = "1",
persistence = "0.5"
}
local warpfield_x = minetest.settings:get_np_group("warpfield_x_params") or default_x
local warpfield_y = minetest.settings:get_np_group("warpfield_y_params") or default_y
local warpfield_z = minetest.settings:get_np_group("warpfield_z_params") or default_z
-- For some reason, these numbers are returned as strings by get_np_group.
local tonumberize_params = function(params)
params.scale = tonumber(params.scale)
params.lacunarity = tonumber(params.lacunarity)
params.spread.x = tonumber(params.spread.x)
params.spread.y = tonumber(params.spread.y)
params.spread.z = tonumber(params.spread.z)
params.offset = tonumber(params.offset)
params.persistence = tonumber(params.persistence)
end
tonumberize_params(warpfield_x)
tonumberize_params(warpfield_y)
tonumberize_params(warpfield_z)
local trigger_stack_size = 99
local trigger_wear_amount = 0
local trigger_tool_capabilities = nil
if warpfield_trigger_uses ~= 0 then
trigger_stack_size = 1
trigger_wear_amount = math.ceil(65535 / warpfield_trigger_uses)
trigger_tool_capabilities = {
full_punch_interval=1.5,
max_drop_level=1,
groupcaps={},
damage_groups = {},
}
end
local particle_node_pos_spread = vector.new(0.5,0.5,0.5)
local particle_user_pos_spread = vector.new(0.5,1.5,0.5)
local particle_speed_spread = vector.new(0.1,0.1,0.1)
local min_spark_delay = 30
local max_spark_delay = 120
local trigger_help_addendum = ""
if warpfield_trigger_uses > 0 then
trigger_help_addendum = S(" This tool can be used @1 times before breaking.", warpfield_trigger_uses)
end
local warp_x
local warp_y
local warp_z
-- An external API to allow use of warp field by other mods
local get_warp_at = function(pos)
if not warp_x then
warp_x = minetest.get_perlin(warpfield_x)
warp_y = minetest.get_perlin(warpfield_y)
warp_z = minetest.get_perlin(warpfield_z)
end
return {x = warp_x:get_3d(pos), y = warp_y:get_3d(pos), z = warp_z:get_3d(pos)}
end
warpfield.get_warp_at = get_warp_at
local player_cooldown = {}
local trigger_def = {
description = S("Warpfield Trigger"),
_doc_items_longdesc = S("A triggering device that allows teleportation via warpfield."),
_doc_items_usagehelp = S("When triggered, this tool and its user will be displaced in accordance with the local warp field's displacement. Simply holding it makes it act as a compass of sorts, showing the current strength of the warp field.") .. trigger_help_addendum,
inventory_image = "warpfield_spark.png^warpfield_tool_base.png",
stack_max = trigger_stack_size,
tool_capabilites = trigger_tool_capabilities,
sound = {
breaks = "warpfield_trigger_break",
},
on_use = function(itemstack, user, pointed_thing)
local player_name = user:get_player_name()
if (player_cooldown[player_name] or 0) > 0 then
return itemstack
end
local old_pos = user:get_pos()
local warp = get_warp_at(old_pos)
local new_pos = vector.add(old_pos, warp)
old_pos.y = old_pos.y + 0.5
local speed = vector.multiply(vector.direction(old_pos, new_pos), 5/0.5)
minetest.add_particlespawner({
amount = 100,
time = 0.1,
minpos = vector.subtract(old_pos, particle_node_pos_spread),
maxpos = vector.add(old_pos, particle_user_pos_spread),
minvel = vector.subtract(speed, particle_speed_spread),
maxvel = vector.add(speed, particle_speed_spread),
minacc = {x=0, y=0, z=0},
maxacc = {x=0, y=0, z=0},
minexptime = 0.1,
maxexptime = 0.5,
minsize = 1,
maxsize = 1,
collisiondetection = false,
vertical = false,
texture = "warpfield_spark.png",
})
minetest.sound_play({name="warpfield_teleport_from"}, {pos = old_pos}, true)
user:set_pos({x=new_pos.x, y=new_pos.y-0.5, z=new_pos.z})
new_pos = vector.subtract(new_pos, speed)
minetest.add_particlespawner({
amount = 100,
time = 0.1,
minpos = vector.subtract(new_pos, particle_node_pos_spread),
maxpos = vector.add(new_pos, particle_user_pos_spread),
minvel = vector.subtract(speed, particle_speed_spread),
maxvel = vector.add(speed, particle_speed_spread),
minacc = {x=0, y=0, z=0},
maxacc = {x=0, y=0, z=0},
minexptime = 0.5,
maxexptime = 0.5,
minsize = 1,
maxsize = 1,
collisiondetection = false,
vertical = false,
texture = "warpfield_spark.png",
})
minetest.sound_play({name="warpfield_teleport_to"}, {pos = new_pos}, true)
if trigger_wear_amount > 0 and not minetest.is_creative_enabled(player_name) then
itemstack:add_wear(trigger_wear_amount)
end
player_cooldown[player_name] = warpfield_trigger_cooldown
return itemstack
end
}
local hud_position = {
x= tonumber(minetest.settings:get("warpfield_hud_x")) or 0.5,
y= tonumber(minetest.settings:get("warpfield_hud_y")) or 0.9,
}
local hud_color = tonumber("0x" .. (minetest.settings:get("warpfield_hud_color") or "FFFF00")) or 0xFFFF00
local hud_color_stressed = tonumber("0x" .. (minetest.settings:get("warpfield_hud_color_stressed") or "FF0000")) or 0xFF0000
local player_huds = {}
local function hide_hud(player, player_name)
local id = player_huds[player_name]
if id then
player:hud_remove(id)
player_huds[player_name] = nil
end
end
local function update_hud(player, player_name, player_cooldown_val)
local player_pos = player:get_pos()
local local_warp = vector.floor(get_warp_at(player_pos))
local color
local description = S("Local warp field: @1", minetest.pos_to_string(local_warp))
if player_cooldown_val > 0 then
color = hud_color_stressed
description = description .. "\n" .. S("Cooldown: @1s", math.ceil(player_cooldown_val))
else
color = hud_color
end
local id = player_huds[player_name]
if not id then
id = player:hud_add({
hud_elem_type = "text",
position = hud_position,
text = description,
number = color,
scale = 20,
})
player_huds[player_name] = id
else
player:hud_change(id, "text", description)
player:hud_change(id, "number", color)
end
end
local function warpfield_globalstep(dtime)
for i, player in ipairs(minetest.get_connected_players()) do
local player_name = player:get_player_name()
local player_cooldown_val = math.max((player_cooldown[player_name] or 0) - dtime, 0)
player_cooldown[player_name] = player_cooldown_val
local wielded = player:get_wielded_item()
if wielded:get_name() == "warpfield:trigger" then
update_hud(player, player_name, player_cooldown_val)
else
hide_hud(player, player_name)
end
end
end
-- update hud
minetest.register_globalstep(warpfield_globalstep)
if trigger_tool_capabilities then
minetest.register_tool("warpfield:trigger", trigger_def)
else
minetest.register_craftitem("warpfield:trigger", trigger_def)
end
local number_of_attempts_to_use = 100
local precision = 0.1
local find_minimum = function(pos, max_tries, direction)
direction = direction or 1
local dir_func
if direction > 0 then
dir_func = vector.add
else
dir_func = vector.subtract
end
local last_jump = vector.new(pos)
for i = 1, max_tries do
local local_warp = get_warp_at(last_jump)
local new_jump = dir_func(last_jump, local_warp)
if vector.distance(new_jump, last_jump) < precision then
return last_jump, i, true
end
last_jump = new_jump
end
return last_jump, max_tries, false
end
minetest.register_chatcommand("find_warp_minimum", {
params = "[<pos>]",
description = S("locate the nearest warpfield minimum by following the field downhill from the provided location, or from the player's location if not provided. This is where a player starting at that position will eventually wind up if they repeatedly travel by warp, not counting any falls along the way."),
privs = {server=true}, -- Require the "privs" privilege to run
func = function(name, param)
local pos = nil
local param = minetest.string_to_pos(param)
if param then
pos = param
else
local player = minetest.get_player_by_name(name)
pos = player:get_pos()
end
local minimum, tries, success = find_minimum(pos, number_of_attempts_to_use)
if success then
minetest.chat_send_player(name, S("Minimum located at @1 after @2 jumps", minetest.pos_to_string(vector.round(minimum)), tries))
else
minetest.chat_send_player(name, S("Stopped testing for minima at @1 after @2 jumps.", minetest.pos_to_string(vector.round(minimum)), tries))
end
end,
})
local follow_field_array = function(name, param, direction)
local p1, p2, step_size, round_to_nearest
local args = param:split(" ")
if #args == 3 or #args == 4 then
p1 = minetest.string_to_pos(args[1])
p2 = minetest.string_to_pos(args[2])
step_size = tonumber(args[3])
round_to_nearest = 1
if #args == 4 then
round_to_nearest = tonumber(args[4]) or 1
end
end
if p1 == nil or p2 == nil or step_size == nil then
minetest.chat_send_player(name, S('Incorrect argument format. Expected: "(x1,y1,z1) (x2,y2,z2) number [number]"'))
return
end
local minima_hashes = {}
local failures = 0
local successes = 0
for x = math.min(p1.x, p2.x), math.max(p1.x, p2.x), math.abs(step_size) do
for y = math.min(p1.y, p2.y), math.max(p1.y, p2.y), math.abs(step_size) do
for z = math.min(p1.z, p2.z), math.max(p1.z, p2.z), math.abs(step_size) do
local minimum, tries, success = find_minimum({x=x,y=y,z=z}, number_of_attempts_to_use, direction)
if success then
successes = successes + 1
minima_hashes[minetest.hash_node_position(vector.round(vector.divide(minimum, round_to_nearest)))] = true
else
failures = failures + 1
end
local total = successes + failures
if total % 1000 == 0 then
minetest.chat_send_player(name, S("Tested @1 starting points...", total))
end
end
end
end
local sorted_minima = {}
for hash, _ in pairs(minima_hashes) do
table.insert(sorted_minima, vector.multiply(minetest.get_position_from_hash(hash), round_to_nearest))
end
table.sort(sorted_minima, function(p1, p2)
if p1.x < p2.x then
return true
elseif p1.x > p2.x then
return false
elseif p1.y < p2.y then
return true
elseif p1.y > p2.y then
return false
elseif p1.z < p2.z then
return true
elseif p1.z > p2.z then
return false
end
return false
end)
return successes, failures, round_to_nearest, sorted_minima
end
minetest.register_chatcommand("find_warp_minima", {
params = "<minpos> <maxpos> <step_size> [<rounded_to_nearest>]",
description = S("Find all warp minima accessible from within the given volume, starting from test points separated by step_size. These are locations that players who repeatedly teleport will eventually wind up."),
privs = {server=true},
func = function(name, param)
local successes, failures, round_to_nearest, sorted_minima = follow_field_array(name, param, 1)
minetest.chat_send_player(name, S("With @1 successful and @2 failed runs found the following minima (rounded to @3m):", successes, failures, round_to_nearest))
for _, pos in ipairs(sorted_minima) do
minetest.chat_send_player(name, minetest.pos_to_string(pos))
end
end,
})
minetest.register_chatcommand("find_warp_maxima", {
params = "<minpos> <maxpos> <step_size> [<rounded_to_nearest>]",
description = S("Find all warp maxima accessible from within the given volume, starting from test points separated by step_size. These are places that are difficult or impossible to reach by warpfield teleport."),
privs = {server=true},
func = function(name, param)
local successes, failures, round_to_nearest, sorted_minima = follow_field_array(name, param, -1)
minetest.chat_send_player(name, S("With @1 successful and @2 failed runs found the following maxima (rounded to @3m):", successes, failures, round_to_nearest))
for _, pos in ipairs(sorted_minima) do
minetest.chat_send_player(name, minetest.pos_to_string(pos))
end
end,
})
if minetest.get_modpath("default") then
minetest.register_craft({
output = "warpfield:trigger",
recipe = {
{"default:steel_ingot", "default:mese_crystal_fragment", "default:steel_ingot"},
{"default:mese_crystal_fragment", "default:mese_crystal_fragment", "default:mese_crystal_fragment"},
{"default:steel_ingot", "default:mese_crystal_fragment", "default:steel_ingot"}
}
})
end