diff --git a/mods/rgt_towns/rgt_towns_core/init.lua b/mods/rgt_towns/rgt_towns_core/init.lua index 4eb6672..e0a70f7 100644 --- a/mods/rgt_towns/rgt_towns_core/init.lua +++ b/mods/rgt_towns/rgt_towns_core/init.lua @@ -20,10 +20,13 @@ function ns.register_spawner(def) ns.spawners[def.plot] = def end -function ns.create_spawner(for_plot) +function ns.create_spawner(for_plot, owner) local s = ItemStack("rgt_towns:spawner") local m = s:get_meta() m:set_string("plot", for_plot) + if owner then + m:set_string("owner", owner) + end m:set_string("description", ns.spawners[for_plot].label) return s end @@ -37,6 +40,8 @@ end thumbnail = "", -- Optional; defines the thumbnail shown in the guide. size = , -- The amount of space taken up by this plot, in grid squares. schematic = 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. "stone 60", "wood 10", "stone 60", "cobble 25", "diamond 3", "cobble 25", @@ -69,5 +74,29 @@ function ns.register_plot(def) warn "A plot was registered without a name!" return 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 end \ No newline at end of file diff --git a/mods/rgt_towns/rgt_towns_core/items.lua b/mods/rgt_towns/rgt_towns_core/items.lua index c46461a..95ae85d 100644 --- a/mods/rgt_towns/rgt_towns_core/items.lua +++ b/mods/rgt_towns/rgt_towns_core/items.lua @@ -1,5 +1,12 @@ 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", { description = "Blank Plot Spawner (you shouldn't have this)", stack_max = 1, @@ -15,17 +22,97 @@ minetest.register_craftitem(":rgt_towns:spawner", { on_place = function(s, p, pt) local m = s:get_meta() 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).") return end local plot = m:get("plot") 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 warn("<"..p:get_player_name().."> tried to use an uninitialized plot spawner.") end + return ItemStack() 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) diff --git a/mods/rgt_towns/rgt_towns_core/plots.lua b/mods/rgt_towns/rgt_towns_core/plots.lua index c219df0..ae1627b 100644 --- a/mods/rgt_towns/rgt_towns_core/plots.lua +++ b/mods/rgt_towns/rgt_towns_core/plots.lua @@ -1,14 +1,288 @@ 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 +-- 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 = { -- ["default:dirt"] = "adrift:dirt", -- ["default:wood"] = "adrift:water" } -function ns.place_plot(plot, pos) - minetest.place_schematic(pos, ns.plots[plot].schematic, nil, repl, true, {place_center_x = true, place_center_z = true}) +local function get_intersecting_grid_cells(aabb, origin, cell_size) + -- 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 \ No newline at end of file diff --git a/mods/rgt_towns/rgt_towns_core/visuals.lua b/mods/rgt_towns/rgt_towns_core/visuals.lua index 9da78b8..e256098 100644 --- a/mods/rgt_towns/rgt_towns_core/visuals.lua +++ b/mods/rgt_towns/rgt_towns_core/visuals.lua @@ -1,17 +1,35 @@ local ns = rgt_towns -function ns.add_line(from, to) +function ns.add_line(from, to, args) local dist = vector.distance(from, to) local vel = dist - return minetest.add_particlespawner { + local ps = { pos = from, vel = vector.direction(from, to) *vel, amount = dist *vel, time = 0, exptime = dist /vel, - texture = "[fill:1x1:0,0:#35f", + texture = "[fill:1x1:0,0:"..(args.color or "#35f"), 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 --[[ @@ -32,7 +50,8 @@ end 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 p2 = vector.new(to.x, from.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 p8 = to return { - ns.add_line(p1, p2), - ns.add_line(p1, p3), - ns.add_line(p1, p4), + ns.add_line(p1, p2, args), + ns.add_line(p1, p3, args), + ns.add_line(p1, p4, args), - ns.add_line(p4, p6), - ns.add_line(p4, p7), + ns.add_line(p4, p6, args), + 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, p6), - ns.add_line(p8, p7), + ns.add_line(p8, p5, args), + ns.add_line(p8, p6, args), + ns.add_line(p8, p7, args), - ns.add_line(p5, p2), - ns.add_line(p5, p3), + ns.add_line(p5, p2, args), + ns.add_line(p5, p3, args), - ns.add_line(p7, p3), + ns.add_line(p7, p3, args), } end diff --git a/mods/rgt_towns/rgt_towns_editor/init.lua b/mods/rgt_towns/rgt_towns_editor/init.lua index 5adcd08..c654a77 100644 --- a/mods/rgt_towns/rgt_towns_editor/init.lua +++ b/mods/rgt_towns/rgt_towns_editor/init.lua @@ -105,7 +105,7 @@ function ns.place_workspace(name, pos) pos = pos:round() ws = { pos = pos, - size = vector.new(1,4,1) + size = vector.new(1,2,1) } db:set_string("workspace:"..name, minetest.serialize(ws)) minetest.add_entity(ns.get_workspace_center(pos, ws.size), "rgt_towns_editor:workspace", name) diff --git a/mods/rgt_towns/rgt_towns_main/init.lua b/mods/rgt_towns/rgt_towns_main/init.lua index 1b97038..73cb1d8 100644 --- a/mods/rgt_towns/rgt_towns_main/init.lua +++ b/mods/rgt_towns/rgt_towns_main/init.lua @@ -2,17 +2,20 @@ rgt_towns.main = {} ns = rgt_towns.main ns.plots = { - + "empty_plot" } -rgt_towns.register_spawner{ - plot = "covered_wagon", - label = "Town Charter" +rgt_towns.register_spawner { + plot = "empty_plot", + label = "Town Charter", + on_spawn = function(p) + + end } minetest.register_chatcommand("charter", { 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 }) @@ -21,5 +24,20 @@ rgt_towns.register_plot{ label = "Covered Wagon", description = "Hello world", 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 } diff --git a/mods/rgt_towns/rgt_towns_main/schems/empty_plot.mts b/mods/rgt_towns/rgt_towns_main/schems/empty_plot.mts new file mode 100644 index 0000000..144da93 Binary files /dev/null and b/mods/rgt_towns/rgt_towns_main/schems/empty_plot.mts differ