Allow mechanisms to be linked, and allow storing links in schematics.

This commit is contained in:
Signal 2025-11-12 00:55:14 -05:00
parent 8f98a7fa2d
commit 8a8fa943c5
9 changed files with 438 additions and 104 deletions

View file

@ -21,13 +21,14 @@ minetest.register_entity(":artifact:lever_display", {
end
e.object:set_armor_groups{immortal = 1}
extend(e, minetest.deserialize(data) or {})
if not e.rotation then e.rotation = vector.zero() end
levers[e.object:get_pos():round():to_string()] = e
end,
on_deactivte = function(e)
levers[e.object:get_pos():round():to_string()] = nil
end,
get_staticdata = function(e)
return {rotation = e.rotation}
return minetest.serialize{rotation = e.rotation}
end,
on_interact = function(e)
if e._active then
@ -36,6 +37,7 @@ minetest.register_entity(":artifact:lever_display", {
e.object:set_animation({x=1,y=2}, 2, 0.1, false)
minetest.after(0.5, function()
e._no_interact = nil
e:trigger(false)
end)
else
e._active = true
@ -43,10 +45,21 @@ minetest.register_entity(":artifact:lever_display", {
e.object:set_animation({x=0,y=1}, 2, 0.1, false)
minetest.after(0.5, function()
e._no_interact = nil
e:trigger(true)
end)
end
end,
trigger = function(e, state)
local receivers = minetest.deserialize(minetest.get_meta(e.object:get_pos():round()):get("receivers") or "return nil")
if receivers then
artifact.dispatch_event(receivers, {type = state and "on" or "off"})
end
end,
rotate = function(e, rot)
if not rot then
say "!"
return
end
e.object:set_rotation(rot)
e.rotation = rot
e.object:set_properties {
@ -58,6 +71,7 @@ minetest.register_entity(":artifact:lever_display", {
end
})
artifact.register_node("lever", {
drawtype = "airlike",
paramtype = "light",
@ -66,9 +80,13 @@ artifact.register_node("lever", {
pointable = false,
on_construct = function(pos)
local m = minetest.get_meta(pos)
m:set_string("initialized", "true")
local rot = minetest.facedir_to_dir(minetest.get_node(pos).param2):dir_to_rotation()
minetest.add_entity(pos, "artifact:lever_display", minetest.serialize{type = "wood"}):get_luaentity():rotate(rot)
local rot = artifact.facedir_to_rotation(minetest.get_node(pos).param2)
if m:contains("initialized") then
levers[pos:to_string()]:rotate(rot)
else
m:set_string("initialized", "true")
minetest.add_entity(pos, "artifact:lever_display", minetest.serialize{type = "wood"}):get_luaentity():rotate(rot)
end
end,
on_destruct = function(pos)
levers[pos:to_string()].object:remove()
@ -79,15 +97,18 @@ artifact.register_node("lever", {
-- Dynamically initialize doors that were mapgen'd in.
if not m:contains("initialized") then
m:set_string("initialized", "true")
local rot = minetest.facedir_to_dir(minetest.get_node(pos).param2):dir_to_rotation()
local rot = artifact.facedir_to_rotation(minetest.get_node(pos).param2)
minetest.add_entity(pos, "artifact:lever_display", minetest.serialize{type = "wood"}):get_luaentity():rotate(rot)
end
end,
on_rightclick = function(pos)
local rot = artifact.facedir_to_rotation(minetest.get_node(pos).param2)
levers[pos:to_string()]:rotate(rot)
end,
on_rotate = function(pos, node, p, click, param2)
local node = minetest.get_node(pos)
node.param2 = param2
minetest.swap_node(pos, node)
local rot = minetest.facedir_to_dir(node.param2):dir_to_rotation()
local rot = artifact.facedir_to_rotation(param2)
levers[pos:to_string()]:rotate(rot)
return true
end

View file

@ -0,0 +1,14 @@
artifact.register_entity("ccolor_swapper_display", {
initial_properties = {
visual = "mesh",
mesh = "artifact_color_swapper.gltf",
textures = {"artifact_color_swapper_gold.png"}
}
})
artifact.register_node("color_swapper", {
on_construct = function(pos)
end,
})

View file

@ -25,14 +25,17 @@ minetest.register_entity(":artifact:door", {
return (e._open and vector.new(-0.5, 0.5, 0) or vector.new(0, 0.5, 0.5)):rotate(e.object:get_rotation())
end,
on_activate = function(e, data)
if not minetest.get_node(e.object:get_pos()).name:find "door" then
local node = minetest.get_node(e.object:get_pos())
if not node.name:find "door" then
e.object:remove()
return
end
e.object:set_armor_groups{immortal = 1}
extend(e, minetest.deserialize(data) or {})
if e.type == "iron" and e._locked == nil then
e._locked = true
if e.type == "iron" then
if e._locked == nil then
e._locked = true
end
e.object:set_properties {
textures = {"artifact_door_iron.png"},
}
@ -45,50 +48,69 @@ minetest.register_entity(":artifact:door", {
e._no_interact = true
end
e._name = ""..math.random()
local nm = minetest.get_meta(e.object:get_pos())
if (node.name:find "_open") and not e._open then
e:open(true)
elseif not (node.name:find "_open") and e._open then
e:close(true)
end
doors[e.object:get_pos():round():to_string()] = e
end,
on_deactivate = function(e)
doors[e.object:get_pos():round():to_string()] = nil
end,
on_interact = function(e)
if e._locked then return end
if e._open then
e._open = nil
e._no_interact = true
e._name = ""..math.random()
e.object:set_animation({x=0.5,y=1}, 1.5, 0.1, false)
minetest.after(0.1, function()
local pos = e.object:get_pos():round()
local node = minetest.get_node(pos)
node.name = "door_"..e.type
minetest.swap_node(pos, node)
e.object:set_properties {
selectionbox = artifact.rotate_selectionbox({
-0.5, -0.5, -0.5,
0.5, 1.5, -6/16
}, e.rotation)
}
end)
minetest.after(0.25, function()
e:close()
else
e:open()
end
end,
open = function(e, snap)
if e._open then return end
e._open = true
e._no_interact = true
e._name = ""..math.random()
e.object:set_animation({x=snap and 0.5 or 0,y=0.5}, 1.5, 0.1, false)
minetest.after(snap and 0 or 0.1, function()
local pos = e.object:get_pos():round()
local node = minetest.get_node(pos)
node.name = "door_"..e.type.."_open"
minetest.swap_node(pos, node)
e.object:set_properties {
selectionbox = artifact.rotate_selectionbox({
0.5, -0.5, -0.5,
6/16, 1.5, 0.5
}, e.rotation)
}
end)
if not e._locked then
minetest.after(snap and 0 or 0.25, function()
e._no_interact = nil
end)
else
e._open = true
e._no_interact = true
e._name = ""..math.random()
e.object:set_animation({x=0,y=0.5}, 1.5, 0.1, false)
minetest.after(0.1, function()
local pos = e.object:get_pos():round()
local node = minetest.get_node(pos)
node.name = "door_"..e.type.."_open"
minetest.swap_node(pos, node)
e.object:set_properties {
selectionbox = artifact.rotate_selectionbox({
0.5, -0.5, -0.5,
6/16, 1.5, 0.5
}, e.rotation)
}
end)
minetest.after(0.25, function()
end
end,
close = function(e, snap)
if not e._open then return end
e._open = nil
e._no_interact = true
e._name = ""..math.random()
e.object:set_animation({x=snap and 1 or 0.5,y=1}, 1.5, 0.1, false)
minetest.after(snap and 0 or 0.1, function()
local pos = e.object:get_pos():round()
local node = minetest.get_node(pos)
node.name = "door_"..e.type
minetest.swap_node(pos, node)
e.object:set_properties {
selectionbox = artifact.rotate_selectionbox({
-0.5, -0.5, -0.5,
0.5, 1.5, -6/16
}, e.rotation)
}
end)
if not e._locked then
minetest.after(snap and 0 or 0.25, function()
e._no_interact = nil
end)
end
@ -113,6 +135,43 @@ minetest.register_entity(":artifact:door", {
})
local function register_basic_door(type)
local function onload(pos)
local m = minetest.get_meta(pos)
-- Dynamically initialize doors that were mapgen'd in.
if not m:contains("initialized") then
m:set_string("initialized", "true")
local rot = minetest.facedir_to_dir(minetest.get_node(pos).param2):dir_to_rotation()
rot.y = rot.y -math.pi
minetest.add_entity(pos, "artifact:door", minetest.serialize{type = type}):get_luaentity():rotate(rot)
end
end
local function ondestruct(pos, reason)
if reason == "whack" then
-- TODO: Particles
end
doors[pos:to_string()].object:remove()
doors[pos:to_string()] = nil
end
local function onsignal(pos, event)
local door = doors[vector.to_string(pos)]
if event.type == "on" then
if door then
door:open()
else
local node = minetest.get_node(pos)
node.name = "door_"..e.type.."_open"
minetest.swap_node(pos, node)
end
elseif event.type == "off" then
if door then
door:close()
else
local node = minetest.get_node(pos)
node.name = "door_"..e.type.."_open"
minetest.swap_node(pos, node)
end
end
end
artifact.register_node("door_"..type, {
drawtype = "nodebox",
node_box = {
@ -128,30 +187,10 @@ local function register_basic_door(type)
paramtype = "light",
pointable = false,
groups = {call_on_load = 1, whackable = type == "wood" and 1 or nil},
on_construct = function(pos)
local m = minetest.get_meta(pos)
m:set_string("initialized", "true")
local rot = minetest.facedir_to_dir(minetest.get_node(pos).param2):dir_to_rotation()
rot.y = rot.y -math.pi
minetest.add_entity(pos, "artifact:door", minetest.serialize{type = type}):get_luaentity():rotate(rot)
end,
on_destruct = function(pos, reason)
if reason == "whack" then
-- TODO: Particles
end
doors[pos:to_string()].object:remove()
doors[pos:to_string()] = nil
end,
on_load = function(pos)
local m = minetest.get_meta(pos)
-- Dynamically initialize doors that were mapgen'd in.
if not m:contains("initialized") then
m:set_string("initialized", "true")
local rot = minetest.facedir_to_dir(minetest.get_node(pos).param2):dir_to_rotation()
rot.y = rot.y -math.pi
minetest.add_entity(pos, "artifact:door", minetest.serialize{type = type}):get_luaentity():rotate(rot)
end
end
on_construct = onload,
on_destruct = ondestruct,
on_load = onload,
on_signal = onsignal
})
artifact.register_node("door_"..type.."_open", {
drawtype = "nodebox",
@ -168,27 +207,10 @@ local function register_basic_door(type)
paramtype = "light",
pointable = false,
groups = {call_on_load = 1, whackable = type == "wood" and 1 or nil},
on_construct = function(pos)
local m = minetest.get_meta(pos)
m:set_string("initialized", "true")
local rot = minetest.facedir_to_dir(minetest.get_node(pos).param2):dir_to_rotation()
rot.y = rot.y -math.pi
minetest.add_entity(pos, "artifact:door", minetest.serialize{type = type}):get_luaentity():rotate(rot)
end,
on_destruct = function(pos)
doors[pos:to_string()].object:remove()
doors[pos:to_string()] = nil
end,
on_load = function(pos)
local m = minetest.get_meta(pos)
-- Dynamically initialize doors that were mapgen'd in.
if not m:contains("initialized") then
m:set_string("initialized", "true")
local rot = minetest.facedir_to_dir(minetest.get_node(pos).param2):dir_to_rotation()
rot.y = rot.y -math.pi
minetest.add_entity(pos, "artifact:door", minetest.serialize{type = type}):get_luaentity():rotate(rot)
end
end
on_construct = onload,
on_destruct = ondestruct,
on_load = onload,
on_signal = onsignal
})
end

View file

@ -13,6 +13,239 @@ artifact.register_craftitem("cancel", {
inventory_image = "artifact_cancel.png"
})
--- @param receivers: A list of node positions to notify.
--- @param event: The event to send.
function artifact.dispatch_event(receivers, event)
for _, x in ipairs(receivers) do
-- Ensure that nodes are available.
minetest.load_area(x.pos)
local node = minetest.get_node(x.pos)
local def = minetest.registered_nodes[node.name]
if def.on_signal then
def.on_signal(x.pos, event, x.channel or "gold")
end
end
end
include "basics.lua"
include "doors.lua"
include "chest.lua"
function artifact.load_schematic(dst, path, rot)
minetest.place_schematic(dst, path..".mts", rot or "0")
local f = io.open(path..".json")
local meta = minetest.parse_json(f:read("a"))
f:close()
-- Load auxiliary metadata.
for p, m in pairs(meta or {}) do
local pos = dst +vector.from_string(p)
-- Transform all position fields back into global space.
if m.fields and m.fields.receivers then
local receivers = minetest.deserialize(m.fields.receivers)
for i, x in ipairs(receivers) do
x.pos = vector.add(x.pos, dst)
receivers[i] = x
end
m.fields.receivers = minetest.serialize(receivers)
end
minetest.get_meta(pos):from_table(m)
local def = minetest.registered_nodes[minetest.get_node(pos).name]
if def.on_construct then
def.on_construct(pos)
end
end
end
minetest.register_entity(":test", {
initial_properties = {
static_save = false,
visual = "mesh",
mesh = "artifact_character.gltf",
},
on_activate = function(e)
-- e.object:set_bone_override("root", {
-- position = {vec = vector.new(15,15,15)}
-- })
end,
on_rightclick = function(e, p)
local v = vector.new(0, p:get_properties().eye_height *10, 0)
p:set_eye_offset(v,v,v)
p:set_attach(e.object, "Head")
end,
on_punch = function(e, p)
p:set_detach()
end
})
if artifact.debug then
local link_colors = {
"gold",
"red",
"blue"
}
if minetest.get_modpath("rhotator") then
minetest.override_item("rhotator:screwdriver", {
pointabilities = {
nodes = {
-- This gets added to everything in debug mode.
["group:dig_immediate"] = true,
air = false,
},
objects = {
-- The display entities should all be immortal.
["group:immortal"] = false
}
},
})
minetest.override_item("testtools:param2tool", {
pointabilities = {
nodes = {
-- This gets added to everything in debug mode.
["group:dig_immediate"] = true,
air = false,
},
objects = {
-- The display entities should all be immortal.
["group:immortal"] = false
}
},
})
end
artifact.register_craftitem("linker_tool", {
inventory_image = "artifact_linker_tool.png",
stack_max = 1,
pointabilities = {
nodes = {
-- This gets added to everything in debug mode.
["group:dig_immediate"] = true,
air = false,
},
objects = {
-- The display entities should all be immortal.
["group:immortal"] = false
}
},
on_secondary_use = function(s, p, pt)
local m = s:get_meta()
-- Just cycle through the colors list.
local color = link_colors[(table.indexof(link_colors, m:get("color") or "gold") +1) %#link_colors +1]
m:set_string("color", color)
m:set_string("inventory_image", "[fill:16x16:0,0:"..color.."#00^artifact_linker_tool.png")
return s
end,
on_place = function(s, p, pt)
local m = artifact.players[p:get_player_name()]
if pt.type == "node" and m._linker_target then
local color = s:get_meta():get("color") or "gold"
local meta = minetest.get_meta(m._linker_target)
local receivers = minetest.deserialize(meta:get("receivers") or "return {}")
if m.ctl.sneak then
local idx = 0
for i, x in ipairs(receivers) do
if vector.equals(x.pos, pt.under) then idx = i end
end
if idx > 0 then
table.remove(receivers, idx)
m.object:hud_remove(m._linker_receivers[idx])
table.remove(m._linker_receivers, idx)
end
else
local idx = 0
for i, x in ipairs(receivers) do
if vector.equals(x.pos, pt.under) then idx = i end
end
-- Ensure we haven't added this pos already.
if idx == 0 then
table.insert(receivers, {pos = pt.under, channel = color})
table.insert(m._linker_receivers, m.object:hud_add {
type = "image_waypoint",
scale = {x=3, y=3},
world_pos = pt.under,
text = "artifact_linker_tool.png^[colorize:"..color..":64"
})
end
end
meta:set_string("receivers", minetest.serialize(receivers))
end
end,
on_use = function(s, p, pt)
local m = artifact.players[p:get_player_name()]
if pt.type == "node" then
m._linker_target = pt.under
if m._linker_target_hud then
m.object:hud_remove(m._linker_target_hud)
end
m._linker_target_hud = m.object:hud_add {
type = "image_waypoint",
scale = {x=3, y=3},
world_pos = pt.under,
text = "artifact_linker_tool.png"
}
if m._linker_receivers then
for _, x in ipairs(m._linker_receivers) do
m.object:hud_remove(x)
end
end
m._linker_receivers = {}
local receivers = minetest.deserialize(minetest.get_meta(m._linker_target):get("receivers") or "return {}")
for _, x in ipairs(receivers) do
table.insert(m._linker_receivers, m.object:hud_add {
type = "image_waypoint",
scale = {x=3, y=3},
world_pos = x.pos,
text = "artifact_linker_tool.png^[colorize:"..x.channel..":64"
})
end
end
end
})
-- To make life easier, simply require worldedit in order to export an area.
if minetest.global_exists("worldedit") then
minetest.mkdir(minetest.get_worldpath().."/schems")
minetest.register_chatcommand("export", {
privs = {server = true},
params = "<name>",
description = "Export the selected region as a schematic, with all node metadata stored in an adjacent JSON file.",
func = function(name, args)
local pos1, pos2 = worldedit.pos1[name], worldedit.pos2[name]
print(dump(minetest.create_schematic(pos1, pos2, nil, minetest.get_worldpath().."/schems/"..args..".mts")))
local minp = vector.sort(pos1, pos2)
local meta = {}
for _, pos in pairs(minetest.find_nodes_with_meta(pos1, pos2)) do
local mt = minetest.get_meta(pos):to_table()
mt.fields.initialized = nil
-- Transform all position fields into local coordinate space.
if mt.fields.receivers then
local receivers = minetest.deserialize(mt.fields.receivers)
for i, x in ipairs(receivers) do
x.pos = vector.subtract(x.pos, minp)
receivers[i] = x
end
mt.fields.receivers = minetest.serialize(receivers)
end
meta[(pos -minp):to_string()] = mt
end
local f = io.open(minetest.get_worldpath().."/schems/"..args..".json", "w")
f:write(minetest.write_json(meta))
f:flush()
f:close()
end
})
end
minetest.register_chatcommand("load", {
privs = {server = true},
func = function(name, args)
artifact.load_schematic(artifact.players[name].pos:round(), minetest.get_worldpath().."/schems/"..args)
end
})
end