Add towns

This commit is contained in:
Signal 2025-09-13 15:39:18 -04:00
parent 26ba673220
commit 7c08d3f61a
7 changed files with 456 additions and 29 deletions

View file

@ -20,10 +20,13 @@ function ns.register_spawner(def)
ns.spawners[def.plot] = def ns.spawners[def.plot] = def
end end
function ns.create_spawner(for_plot) function ns.create_spawner(for_plot, owner)
local s = ItemStack("rgt_towns:spawner") local s = ItemStack("rgt_towns:spawner")
local m = s:get_meta() local m = s:get_meta()
m:set_string("plot", for_plot) m:set_string("plot", for_plot)
if owner then
m:set_string("owner", owner)
end
m:set_string("description", ns.spawners[for_plot].label) m:set_string("description", ns.spawners[for_plot].label)
return s return s
end end
@ -37,6 +40,8 @@ end
thumbnail = "", -- Optional; defines the thumbnail shown in the guide. thumbnail = "", -- Optional; defines the thumbnail shown in the guide.
size = <vector>, -- The amount of space taken up by this plot, in grid squares. size = <vector>, -- The amount of space taken up by this plot, in grid squares.
schematic = <schem> or {}, schematic = <schem> or {},
ground_level = 5, -- The ground level relative to the bottom of the schematic, used by spawners to determine where to place the schematic relative to the player spawning it.
constructors = {n = {5}, w = {5}, s = {5}, e = {5}}, -- The offset from the bottom of the plot at which to place each constructor (to allow for constructors at different Y levels). Defaults to one per horizontally adjoining grid space at y=`ground_level`.
recipe = { -- 3x3 crafting recipe for this structure, as a flat list of ItemStack strings. recipe = { -- 3x3 crafting recipe for this structure, as a flat list of ItemStack strings.
"stone 60", "wood 10", "stone 60", "stone 60", "wood 10", "stone 60",
"cobble 25", "diamond 3", "cobble 25", "cobble 25", "diamond 3", "cobble 25",
@ -69,5 +74,29 @@ function ns.register_plot(def)
warn "A plot was registered without a name!" warn "A plot was registered without a name!"
return return
end end
if not def.size then
def.size = vector.new(1,1,1)
end
if not def.ground_level then def.ground_level = 5 end
for i, x in ipairs(def.recipe) do
if x ~= "" and not x:find ":" then
def.recipe[i] = "red_glazed_terracotta:"..x
end
end
if def.constructors then
local x = def.constructors
local out = {}
else
local out = {}
for x = 1, def.size.x do
out[#out +1] = vector.new((x -1) *15, def.ground_level,-7)
out[#out +1] = vector.new((x -1) *15, def.ground_level, 7)
end
for y = 1, def.size.z do
out[#out +1] = vector.new(-7, def.ground_level, (y -1) *15)
out[#out +1] = vector.new( 7, def.ground_level, (y -1) *15)
end
def.constructors = out
end
ns.plots[def.name] = def ns.plots[def.name] = def
end end

View file

@ -1,5 +1,12 @@
local ns = rgt_towns local ns = rgt_towns
ns.directions = {
[0] = vector.new(0,0,1),
vector.new(1,0,0),
vector.new(0,0,-1),
vector.new(-1,0,0)
}
minetest.register_craftitem(":rgt_towns:spawner", { minetest.register_craftitem(":rgt_towns:spawner", {
description = "Blank Plot Spawner (you shouldn't have this)", description = "Blank Plot Spawner (you shouldn't have this)",
stack_max = 1, stack_max = 1,
@ -15,17 +22,97 @@ minetest.register_craftitem(":rgt_towns:spawner", {
on_place = function(s, p, pt) on_place = function(s, p, pt)
local m = s:get_meta() local m = s:get_meta()
local owner = m:get("owner") local owner = m:get("owner")
if owner and owner ~= p:get_player_name() then if not owner then
owner = p:get_player_name()
elseif owner ~= p:get_player_name() then
warn("<"..p:get_player_name().."> tried to use <"..owner..">'s '"..(m:get("description") or "Blank Plot Spawner").."' (and failed).") warn("<"..p:get_player_name().."> tried to use <"..owner..">'s '"..(m:get("description") or "Blank Plot Spawner").."' (and failed).")
return return
end end
local plot = m:get("plot") local plot = m:get("plot")
if plot then if plot then
ns.place_plot(plot, p:get_pos()) local def = ns.plots[plot]
local pos = p:get_pos():round()
pos.y = pos.y -ns.plots[plot].ground_level
local q = math.random()
ns.create_grid(owner..":town"..q, pos:offset(0,def.size.y *7.5,0))
if not ns.place_plot(owner..":town"..q, vector.zero(), plot, owner) then
ns.delete_grid(owner..":town"..q)
end
else else
warn("<"..p:get_player_name().."> tried to use an uninitialized plot spawner.") warn("<"..p:get_player_name().."> tried to use an uninitialized plot spawner.")
end end
return ItemStack()
end end
}) })
--[[
+Z
^
|
|
|
-X <----------+----------> +X
|
|
|
v
-Z
]]
function ns.check_recipe(inv, plot)
local recipe = ns.plots[plot].recipe
print(dump(recipe))
for i = 1, 9 do
print("Evaluating: "..inv:get_stack("main", i):to_string().." vs. "..ItemStack(recipe[i]):to_string())
if inv:get_stack("main", i) ~= ItemStack(recipe[i]) then
return false
end
end
return true
end
minetest.register_node(":rgt_towns:constructor", {
tiles = {"[fill:16x16:#8af"},
on_construct = function(pos)
local inv = minetest.get_meta(pos):get_inventory()
inv:set_size("main", 9)
end,
on_rightclick = function(pos, node, p, s)
ns.show_constructor_interface(pos, p)
end,
on_punch = function(pos, node)
local m = minetest.get_meta(pos)
local grid = m:get_string("grid")
local gpos = vector.from_string(m:get_string("build_pos"))
local plot = m:get("plot") or "empty_plot"
local inv = m:get_inventory()
if not ns.check_recipe(inv, plot) then
return
end
ns.place_plot(grid, gpos, plot, tostring(node.param2 *90))
minetest.remove_node(pos)
end
})
function ns.show_constructor_interface(pos, p)
local name = p:get_player_name()
local fs = "\
formspec_version[10]\
size[12,10]\
list[nodemeta:"..pos.x..","..pos.y..","..pos.z..";main;1,1;3.5,3.5]\
list[player:"..name..";main;1,5;10,4]\
"
minetest.show_formspec(name, "constructor:"..pos.x..","..pos.y..","..pos.z, fs)
end
minetest.register_on_player_receive_fields(function(p, form, data)
if form:sub(1, string.len("constructor:")) == "constructor:" then
local x, y, z = form:match "constructor:(%d+),(%d+),(%d+)"
if not (x and y and z) then return end
end
end)

View file

@ -1,14 +1,288 @@
local ns = rgt_towns local ns = rgt_towns
local db = minetest.get_mod_storage()
function ns.get_grid() ns.grids = minetest.parse_json(db:get("grids") or "{}")
-- Calculates the Euclidean distance squared between two points (avoids sqrt for efficiency)
local function distance_squared(p1, p2)
local dx = p1.x - p2.x
local dy = p1.y - p2.y
local dz = p1.z - p2.z
return dx * dx + dy * dy + dz * dz
end end
-- Returns the point in the AABB farthest from the given point
-- aabb: table with {min = {x, y, z}, max = {x, y, z}}
-- point: table with {x, y, z}
-- Returns: table {x, y, z} representing the farthest corner
local function get_farthest_point_in_aabb(aabb, point)
-- Ensure input validity
if not aabb or not aabb.min or not aabb.max or not point then
error("Invalid input: aabb or point is missing required fields")
end
-- List all eight corners of the AABB
local corners = {
{x = aabb.min.x, y = aabb.min.y, z = aabb.min.z},
{x = aabb.min.x, y = aabb.min.y, z = aabb.max.z},
{x = aabb.min.x, y = aabb.max.y, z = aabb.min.z},
{x = aabb.min.x, y = aabb.max.y, z = aabb.max.z},
{x = aabb.max.x, y = aabb.min.y, z = aabb.min.z},
{x = aabb.max.x, y = aabb.min.y, z = aabb.max.z},
{x = aabb.max.x, y = aabb.max.y, z = aabb.min.z},
{x = aabb.max.x, y = aabb.max.y, z = aabb.max.z},
}
local max_dist_sq = -1
local farthest_point = nil
-- Iterate through corners to find the one with maximum distance
for _, corner in ipairs(corners) do
local dist_sq = distance_squared(point, corner)
if dist_sq > max_dist_sq then
max_dist_sq = dist_sq
farthest_point = corner
end
end
return farthest_point
end
function aabb_intersects_sphere(aabb, center, radius)
-- Ensure input validity
if not aabb or not aabb.min or not aabb.max or not center or not radius or radius < 0 then
error("Invalid input: aabb, center, or radius is missing or invalid")
end
-- Find the closest point in the AABB to the sphere's center
local closest = {
x = math.max(aabb.min.x, math.min(center.x, aabb.max.x)),
y = math.max(aabb.min.y, math.min(center.y, aabb.max.y)),
z = math.max(aabb.min.z, math.min(center.z, aabb.max.z))
}
-- Check if the closest point is within the sphere's radius
local dist_squared = distance_squared(center, closest)
return dist_squared <= radius * radius
end
AABB = setmetatable({
intersects = function(a, b, c)
if c then
return a.min.x < c.x and a.max.x > b.x and
a.min.y < c.y and a.max.y > b.y and
a.min.z < c.z and a.max.z > b.z
end
return a.min.x < b.max.x and a.max.x > b.min.x and
a.min.y < b.max.y and a.max.y > b.min.y and
a.min.z < b.max.z and a.max.z > b.min.z
end,
intersects_2d = function(a, b, c)
if c then
return a.min.x < c.x and a.max.x > b.x and
a.min.z < c.z and a.max.z > b.z
end
return a.min.x < b.max.x and a.max.x > b.min.x and
a.min.z < b.max.z and a.max.z > b.min.z
end
}, {
__call = function(_, min, max)
min, max = vector.sort(min, max)
return setmetatable({
min = min,
max = max
}, {__index = AABB})
end
})
local repl = { local repl = {
-- ["default:dirt"] = "adrift:dirt", -- ["default:dirt"] = "adrift:dirt",
-- ["default:wood"] = "adrift:water" -- ["default:wood"] = "adrift:water"
} }
function ns.place_plot(plot, pos) local function get_intersecting_grid_cells(aabb, origin, cell_size)
minetest.place_schematic(pos, ns.plots[plot].schematic, nil, repl, true, {place_center_x = true, place_center_z = true}) -- Ensure input validity
if not aabb or not aabb.min or not aabb.max or not origin or not cell_size then
error("Invalid input: aabb, origin, or cell_size is missing required fields")
end
if cell_size.x <= 0 or cell_size.y <= 0 or cell_size.z <= 0 then
error("Invalid input: cell_size components must be positive")
end
-- Convert AABB min and max to cell coordinates (relative to origin)
local min_cell = {
x = math.floor((aabb.min.x - origin.x) / cell_size.x),
y = math.floor((aabb.min.y - origin.y) / cell_size.y),
z = math.floor((aabb.min.z - origin.z) / cell_size.z)
}
local max_cell = {
x = math.floor((aabb.max.x - origin.x) / cell_size.x),
y = math.floor((aabb.max.y - origin.y) / cell_size.y),
z = math.floor((aabb.max.z - origin.z) / cell_size.z)
}
-- Collect all cell positions in the range [min_cell, max_cell]
local cells = {}
for x = min_cell.x, max_cell.x do
for y = min_cell.y, max_cell.y do
for z = min_cell.z, max_cell.z do
table.insert(cells, {x = x, y = y, z = z})
end
end
end
return cells
end
local function mag_floor(a)
return a < 0 and math.ceil(a) or math.floor(a)
end
local function mag_ceil(a)
return a < 0 and math.floor(a) or math.ceil(a)
end
function ns.can_build_plot(box, grid)
local w = box.max.x -box.min.x
local h = box.max.y -box.min.y
local l = box.max.z -box.min.z
for name, grid2 in pairs(ns.grids) do
local i = aabb_intersects_sphere(box, grid2.origin, grid2.radius)
if name == grid or i then
ns.add_cube(box.min, box.max, {color = "#fff"})
-- Translate box to grid2 local space and compute candidate cell range
local min = vector.floor((box.min -grid2.origin) /15)
local max = vector.ceil((box.max -grid2.origin) /15)
ns.add_cube(min *15 +grid2.origin, max *15 +grid2.origin)
if grid2.type == "3d" then
-- TODO: implement
else
for x = min.x, max.x do
for z = min.z, max.z do
ns.add_point(vector.new(x,0,z) *15 +grid2.origin)
-- print("Box: "..dump(box).."; min: "..min:to_string().."; max: "..max:to_string())
local plot = db:get("plot:"..name.."_"..x.."_0_"..z)
if plot then
plot = minetest.parse_json(plot)
if box:intersects_2d(plot.bb_min, plot.bb_max) then
return false
end
end
end
end
end
end
end
return true
end
--[[
Grid types:
* "2d": A two-dimensional grid. Building multiple plots within the same horizontal space is not allowed, but plots may change Y level in any way.
* "3d": A three-dimensional grid. Multiple plots can be constructed within the same column, but changes in Y level must be quantized to the size of the grid cells.
* "2d_staggered": A staggered 2d grid. This is the same as "2d" except that the center of a given cell's edge will correspond to the top and bottom edges of the two neighboring cells.
]]
function ns.create_grid(grid, pos, type)
ns.grids[grid] = {
type = type,
origin = pos,
radius = 0
}
db:set_string("grids", minetest.write_json(ns.grids))
end
function ns.delete_grid(grid)
ns.grids[grid] = nil
db:set_string("grids", minetest.write_json(ns.grids))
end
--- @param grid: Grid in which to place the plot.
--- @param pos: Position within `grid` at which to build the plot.
function ns.place_plot(grid, pos, plot, owner, rot)
local gpos
if ns.grids[grid] then
gpos = ns.grids[grid].origin
else
warn("Failed to place a plot on unknown grid `"..grid.."`.")
return
end
local def = ns.plots[plot]
local dst = (pos *15 +gpos):offset(-7,-def.size.y *7.5,-7)
local box = AABB(dst, dst +def.size *15)
if not ns.can_build_plot(box, grid) then
return
end
db:set_string("plot:"..grid.."_"..pos.x.."_0_"..pos.z, minetest.write_json {
bb_min = box.min,
bb_max = box.max,
owner = owner,
type = plot,
})
local g = ns.grids[grid]
-- Expand the town's radius to include the new plot, if necessary.
g.radius = math.max(g.radius, vector.distance(get_farthest_point_in_aabb(box, g.origin), g.origin))
local def = ns.plots[plot]
minetest.place_schematic(dst, def.schematic, rot, repl, true, {})
local p, gp, m
-- Place constructors along Z axis
for x = 0, def.size.x -1 do
gp = pos:offset(x, 0,-1)
p = dst:offset(x *15 +7, def.ground_level, 0)
if db:contains("plot:"..grid.."_"..gp.x.."_"..gp.y.."_"..gp.z) then
-- Remove adjacent constructors, and don't place new ones.
minetest.remove_node(p:offset(0,0,-1))
else
minetest.set_node(p, {name = "rgt_towns:constructor"})
m = minetest.get_meta(p)
m:set_string("grid", grid)
m:set_string("build_pos", gp:to_string())
end
gp = pos:offset(x, 0, 1)
p = dst:offset(x *15 +7, def.ground_level, def.size.z *15 -1)
if db:contains("plot:"..grid.."_"..gp.x.."_"..gp.y.."_"..gp.z) then
minetest.remove_node(p:offset(0,0,1))
else
minetest.set_node(p, {name = "rgt_towns:constructor"})
m = minetest.get_meta(p)
m:set_string("grid", grid)
m:set_string("build_pos", gp:to_string())
end
end
-- Place constructors along X axis
for z = 0, def.size.z -1 do
gp = pos:offset(-1, 0, z)
p = dst:offset(0, def.ground_level, z *15 +7)
if db:contains("plot:"..grid.."_"..gp.x.."_"..gp.y.."_"..gp.z) then
minetest.remove_node(p:offset(-1,0,0))
else
minetest.set_node(p, {name = "rgt_towns:constructor"})
m = minetest.get_meta(p)
m:set_string("grid", grid)
m:set_string("build_pos", pos:offset(-1, 0, z):to_string())
end
gp = pos:offset(1, 0, z)
p = dst:offset(def.size.x *15 -1, def.ground_level, z *15 +7)
if db:contains("plot:"..grid.."_"..gp.x.."_"..gp.y.."_"..gp.z) then
minetest.remove_node(p:offset(1,0,0))
else
minetest.set_node(p, {name = "rgt_towns:constructor"})
m = minetest.get_meta(p)
m:set_string("grid", grid)
m:set_string("build_pos", pos:offset(1, 0, z):to_string())
end
end
return true
end end

View file

@ -1,17 +1,35 @@
local ns = rgt_towns local ns = rgt_towns
function ns.add_line(from, to) function ns.add_line(from, to, args)
local dist = vector.distance(from, to) local dist = vector.distance(from, to)
local vel = dist local vel = dist
return minetest.add_particlespawner { local ps = {
pos = from, pos = from,
vel = vector.direction(from, to) *vel, vel = vector.direction(from, to) *vel,
amount = dist *vel, amount = dist *vel,
time = 0, time = 0,
exptime = dist /vel, exptime = dist /vel,
texture = "[fill:1x1:0,0:#35f", texture = "[fill:1x1:0,0:"..(args.color or "#35f"),
glow = 14, glow = 14,
} }
if args.attach then
ps.attached = args.attach
end
return minetest.add_particlespawner(ps)
end
function ns.add_point(point, args)
minetest.add_particlespawner {
pos = point,
velocity = {
min = vector.new(-1,-1,-1),
max = vector.new(1,1,1)
},
texture = args and args.color or "[fill:1x1:0,0:#faa",
time = 10,
amount = 50,
size = 5
}
end end
--[[ --[[
@ -32,7 +50,8 @@ end
7 8 7 8
]] ]]
function ns.add_cube(from, to) function ns.add_cube(from, to, args)
if not args then args = {} end
local p1 = from local p1 = from
local p2 = vector.new(to.x, from.y, from.z) local p2 = vector.new(to.x, from.y, from.z)
local p3 = vector.new(from.x, to.y, from.z) local p3 = vector.new(from.x, to.y, from.z)
@ -42,24 +61,24 @@ function ns.add_cube(from, to)
local p7 = vector.new(from.x, to.y, to.z) local p7 = vector.new(from.x, to.y, to.z)
local p8 = to local p8 = to
return { return {
ns.add_line(p1, p2), ns.add_line(p1, p2, args),
ns.add_line(p1, p3), ns.add_line(p1, p3, args),
ns.add_line(p1, p4), ns.add_line(p1, p4, args),
ns.add_line(p4, p6), ns.add_line(p4, p6, args),
ns.add_line(p4, p7), ns.add_line(p4, p7, args),
ns.add_line(p2, p6), ns.add_line(p2, p6, args),
ns.add_line(p8, p5), ns.add_line(p8, p5, args),
ns.add_line(p8, p6), ns.add_line(p8, p6, args),
ns.add_line(p8, p7), ns.add_line(p8, p7, args),
ns.add_line(p5, p2), ns.add_line(p5, p2, args),
ns.add_line(p5, p3), ns.add_line(p5, p3, args),
ns.add_line(p7, p3), ns.add_line(p7, p3, args),
} }
end end

View file

@ -105,7 +105,7 @@ function ns.place_workspace(name, pos)
pos = pos:round() pos = pos:round()
ws = { ws = {
pos = pos, pos = pos,
size = vector.new(1,4,1) size = vector.new(1,2,1)
} }
db:set_string("workspace:"..name, minetest.serialize(ws)) db:set_string("workspace:"..name, minetest.serialize(ws))
minetest.add_entity(ns.get_workspace_center(pos, ws.size), "rgt_towns_editor:workspace", name) minetest.add_entity(ns.get_workspace_center(pos, ws.size), "rgt_towns_editor:workspace", name)

View file

@ -2,17 +2,20 @@ rgt_towns.main = {}
ns = rgt_towns.main ns = rgt_towns.main
ns.plots = { ns.plots = {
"empty_plot"
} }
rgt_towns.register_spawner{ rgt_towns.register_spawner {
plot = "covered_wagon", plot = "empty_plot",
label = "Town Charter" label = "Town Charter",
on_spawn = function(p)
end
} }
minetest.register_chatcommand("charter", { minetest.register_chatcommand("charter", {
func = function(name, args) func = function(name, args)
minetest.get_player_by_name(name):get_inventory():add_item("main", rgt_towns.create_spawner("covered_wagon")) minetest.get_player_by_name(name):get_inventory():add_item("main", rgt_towns.create_spawner("empty_plot", name))
end end
}) })
@ -21,5 +24,20 @@ rgt_towns.register_plot{
label = "Covered Wagon", label = "Covered Wagon",
description = "Hello world", description = "Hello world",
schematic = minetest.get_modpath(minetest.get_current_modname()).."/schems/covered_wagon.mts", schematic = minetest.get_modpath(minetest.get_current_modname()).."/schems/covered_wagon.mts",
recipe = {},
can_build = ns.plots
}
rgt_towns.register_plot{
name = "empty_plot",
label = "Empty Plot",
description = "Hello world",
schematic = minetest.get_modpath(minetest.get_current_modname()).."/schems/empty_plot.mts",
recipe = {
"", "", "",
"dirt_grass 1", "dirt_grass 1", "dirt_grass 1",
"dirt 1", "dirt 1", "dirt 1",
},
ground_level = 5,
can_build = ns.plots can_build = ns.plots
} }

Binary file not shown.