Add a grid based layout for villages

This commit is contained in:
codiac 2024-05-25 13:56:32 +10:00 committed by cora
parent c23a3f2dd0
commit 14042b4688
8 changed files with 312 additions and 78 deletions

View File

@ -194,6 +194,7 @@ globals = {
"mcl_lush_caves", "mcl_lush_caves",
"mcl_armor_trims", "mcl_armor_trims",
"mcl_villages", "mcl_villages",
"AreaStore",
} }
read_globals = { read_globals = {

View File

@ -1161,10 +1161,18 @@ function mcl_util.traverse_tower(pos, dir, callback)
end end
-- Voxel manip function to replace a node type with another in an area -- Voxel manip function to replace a node type with another in an area
function mcl_util.replace_node_vm(pos1, pos2, mat_from, mat_to) function mcl_util.replace_node_vm(pos1, pos2, mat_from, mat_to, is_group)
local c_from = minetest.get_content_id(mat_from)
local c_to = minetest.get_content_id(mat_to) local c_to = minetest.get_content_id(mat_to)
local group_name
local c_from
if is_group then
group_name = string.match(mat_from, "group:(.+)")
else
c_from = minetest.get_content_id(mat_from)
end
local vm = minetest.get_voxel_manip() local vm = minetest.get_voxel_manip()
local emin, emax = vm:read_from_map(pos1, pos2) local emin, emax = vm:read_from_map(pos1, pos2)
local a = VoxelArea:new({ local a = VoxelArea:new({
@ -1178,8 +1186,15 @@ function mcl_util.replace_node_vm(pos1, pos2, mat_from, mat_to)
for y = pos1.y, pos2.y do for y = pos1.y, pos2.y do
for x = pos1.x, pos2.x do for x = pos1.x, pos2.x do
local vi = a:index(x, y, z) local vi = a:index(x, y, z)
if data[vi] == c_from then if is_group then
data[vi] = c_to local node_name = minetest.get_name_from_content_id(data[vi])
if minetest.get_item_group(node_name, group_name) > 0 then
data[vi] = c_to
end
else
if data[vi] == c_from then
data[vi] = c_to
end
end end
end end
end end

View File

@ -276,3 +276,26 @@ If a crop cannot be found for a crop type in a biome, then a default will be
used. This ensure all farming blocks are full, ven if it's al the same crop. used. This ensure all farming blocks are full, ven if it's al the same crop.
The default is wheat. The default is wheat.
## Village Layout
There are two methods for layout out villages, circle layout is more likely to be
used for small villages and grid for large villages.
The circle layout uses circles (surprise) to calculate if buildings overlap. It
creates fairly widely spaced layouts.
The grid layout uses a predetermined grid layout to positions buildings and uses
AreaStore to adjust building position if there are collisions.
The predetermined grid is below, position 0 is the bell, the other numbers are the order of placement.
||||||||
| -- | -- | -- | -- | -- | -- | -- |
|48|41|33|25|29|37|45|
|40|17|13| 9|11|15|43|
|32|19| 5| 1| 3|22|35|
|28|23| 7| 0| 8|24|27|
|36|21| 4| 2| 6|20|31|
|44|16|12|10|14|18|39|
|46|38|30|26|34|42|47|

View File

@ -92,49 +92,187 @@ local function init_nodes(p1, p2, size, rotation, pr)
end end
end end
local function layout_town(minp, maxp, pr, input_settlement_info) -- TODO consided using mod_storage to persist this...
local settlement_info = {} local areas = AreaStore()
local xdist = math.abs(minp.x - maxp.x) areas:set_cache_params({ enabled = true, block_radius = 16, limit = 100 })
local zdist = math.abs(minp.z - maxp.z)
-- find center of village within interior of chunk local function record_building(record)
local center = vector.new( local placement_pos = vector.copy(record.pos)
minp.x + pr:next(math.floor(xdist * 0.2), math.floor(xdist * 0.8)),
maxp.y,
minp.z + pr:next(math.floor(zdist * 0.2), math.floor(zdist * 0.8))
)
-- find center_surface of village -- Allow adjusting y axis
local center_surface, surface_material = mcl_villages.find_surface(center, true) if record.yadjust then
placement_pos = vector.offset(placement_pos, 0, record.yadjust, 0)
end
local size = math.ceil(math.max(record.size.x, record.size.z) / 2)
local minp = vector.offset(placement_pos, -size, -10, -size)
local maxp = vector.offset(placement_pos, size, record.size.y + 10, size)
areas:insert_area(minp, maxp, record.name)
end
-- TODO is there a good way to calculate this?
local squares = {
-- center
{ x = 0, z = 0 },
--around center
{ x = 1, z = 0 },
{ x = -1, z = 0 },
{ x = 1, z = 1 },
{ x = -1, z = -1 },
{ x = 1, z = -1 },
{ x = -1, z = -1 },
{ x = 0, z = 1 },
{ x = 0, z = -1 },
--one out
{ x = 2, z = 0 },
{ x = -2, z = 0 },
{ x = 2, z = 1 },
{ x = -2, z = -1 },
{ x = 2, z = -1 },
{ x = -2, z = 1 },
{ x = 2, z = 2 },
{ x = -2, z = -2 },
{ x = 2, z = -2 },
{ x = -2, z = 2 },
{ x = 0, z = 2 },
{ x = 0, z = -2 },
{ x = 1, z = 2 },
{ x = -1, z = -2 },
{ x = 1, z = -2 },
{ x = -1, z = 2 },
--two out
{ x = 3, z = 0 }, --25
{ x = -3, z = 0 },
{ x = 0, z = 3 },
{ x = 0, z = -3 },
{ x = 3, z = 1 },
{ x = -3, z = -1 },
{ x = -1, z = 3 },
{ x = 1, z = -3 },
{ x = 3, z = -1 },
{ x = -3, z = 1 },
{ x = 1, z = 3 },
{ x = -1, z = -3 },
{ x = 3, z = 2 },
{ x = -3, z = -2 },
{ x = 3, z = -2 },
{ x = -3, z = 2 }, --40
{ x = 3, z = -2 },
{ x = -3, z = 2 },
{ x = 2, z = 3 },
{ x = -2, z = -3 },
{ x = 3, z = 3 },
{ x = -3, z = -3 },
{ x = -3, z = 3 },
{ x = 3, z = -3 },
}
local function layout_grid(pr, input_settlement_info, settlement_info, center)
local bell_schem = settlement_info[1]
record_building(bell_schem)
local center_surface = bell_schem.pos
for i = 2, #input_settlement_info do
if i > #squares then
minetest.log("warning", "Too many buldings for grid layout, remaining buildings will be missing.")
return settlement_info
end
local cur_schem = input_settlement_info[i]
local placed = false
local iter = 0
local x_step = squares[i].x
local z_step = squares[i].z
local b_length = math.max(bell_schem.size.x, bell_schem.size.z)
local c_length = math.max(cur_schem.size.x, cur_schem.size.z)
local dist = math.ceil(b_length + c_length / 2)
local half_dist = math.ceil(dist / 2)
local next_x = bell_schem.pos.x + ((dist + 1) * x_step)
local next_z = bell_schem.pos.z + ((dist + 1) * z_step)
while not placed do
iter = iter + 1
local pos = vector.new(next_x, center_surface.y, next_z)
local pos_surface, surface_material = mcl_villages.find_surface(pos)
if pos_surface then
pos_surface = vector.round(pos_surface)
local minp = vector.offset(pos_surface, -half_dist, 0, -half_dist)
local maxp = vector.offset(pos_surface, half_dist, cur_schem.size.y, half_dist)
local collisons = areas:get_areas_in_area(minp, maxp, true, true, true)
if table.count(collisons) == 0 then
cur_schem.pos = vector.copy(pos_surface)
cur_schem.surface_mat = surface_material
record_building(cur_schem)
table.insert(settlement_info, cur_schem)
placed = true
else
local col
-- sometimes this is a table with keys, and sometimes not ...
for k, v in pairs(collisons) do
col = v
break
end
if z_step < 0 then
next_z = col.max.z + half_dist + 1
elseif z_step > 0 then
next_z = col.min.z - half_dist - 1
elseif x_step < 0 then
next_x = col.min.x - half_dist - 1
elseif x_step > 0 then
next_x = col.max.x + half_dist + 1
end
end
else
next_x = next_x + (dist + 1 * x_step)
next_z = next_z + (dist + 1 * z_step)
end
if not placed and (iter % 5 == 0) then
x_step = squares[i + (iter % 10)].x
z_step = squares[i + (iter % 10)].z
next_x = bell_schem.pos.x + ((dist + 1) * x_step)
next_z = bell_schem.pos.z + ((dist + 1) * z_step)
end
if not placed and iter >= 30 then
minetest.log("warning", "Could not place " .. cur_schem.name)
break
end
end
end
end
local function layout_circles(pr, input_settlement_info, settlement_info, center)
local center_surface = settlement_info[1].pos
local size = #input_settlement_info
local max_dist = 20 + (size * 3)
-- Cache for chunk surfaces -- Cache for chunk surfaces
local chunks = {} local chunks = {}
chunks[mcl_vars.get_chunk_number(center)] = true chunks[mcl_vars.get_chunk_number(center)] = true
-- build settlement around center
if not center_surface then
minetest.log("info", string.format("[mcl_villages] Cannot build village at %s", minetest.pos_to_string(center)))
return false
else
minetest.log(
"info",
string.format(
"[mcl_villages] Will build a village at position %s with surface material %s",
minetest.pos_to_string(center_surface),
surface_material
)
)
end
local bell_info = table.copy(input_settlement_info[1])
bell_info["pos"] = vector.copy(center_surface)
bell_info["surface_mat"] = surface_material
table.insert(settlement_info, bell_info)
local size = #input_settlement_info
local max_dist = 20 + (size * 3)
for i = 2, size do for i = 2, size do
local cur_schem = input_settlement_info[i] local cur_schem = input_settlement_info[i]
@ -200,6 +338,52 @@ local function layout_town(minp, maxp, pr, input_settlement_info)
end end
end end
end end
end
local function layout_town(minp, maxp, pr, input_settlement_info, grid)
local settlement_info = {}
local xdist = math.abs(minp.x - maxp.x)
local zdist = math.abs(minp.z - maxp.z)
-- find center of village within interior of chunk
local center = vector.new(
minp.x + pr:next(math.floor(xdist * 0.2), math.floor(xdist * 0.8)),
maxp.y,
minp.z + pr:next(math.floor(zdist * 0.2), math.floor(zdist * 0.8))
)
-- find center_surface of village
local center_surface, surface_material = mcl_villages.find_surface(center, true)
-- build settlement around center
if not center_surface then
minetest.log(
"info",
string.format("[mcl_villages] Cannot build village at %s", minetest.pos_to_string(center))
)
return false
else
minetest.log(
"info",
string.format(
"[mcl_villages] Will build a village at position %s with surface material %s",
minetest.pos_to_string(center_surface),
surface_material
)
)
end
local bell_info = table.copy(input_settlement_info[1])
bell_info["pos"] = vector.copy(center_surface)
bell_info["surface_mat"] = surface_material
table.insert(settlement_info, bell_info)
if grid then
layout_grid(pr, input_settlement_info, settlement_info, center)
else
layout_circles(pr, input_settlement_info, settlement_info, center)
end
return settlement_info return settlement_info
end end
@ -305,7 +489,13 @@ function mcl_villages.create_site_plan_new(minp, maxp, pr)
table.insert(shuffled_settlement_info, 1, bell_info) table.insert(shuffled_settlement_info, 1, bell_info)
return layout_town(minp, maxp, pr, shuffled_settlement_info) -- More jobs increases chances of grid layout
local grid = false
if pr:next(1, count_buildings.num_jobs) > 4 then
grid = true
end
return layout_town(minp, maxp, pr, shuffled_settlement_info, grid), grid
end end
function mcl_villages.place_schematics_new(settlement_info, pr, blockseed) function mcl_villages.place_schematics_new(settlement_info, pr, blockseed)

View File

@ -17,7 +17,7 @@ function mcl_villages.grundstellungen()
"mcl_core:sand", "mcl_core:sand",
"mcl_core:redsand", "mcl_core:redsand",
--"mcl_core:silver_sand", --"mcl_core:silver_sand",
"mcl_core:snow" --"mcl_core:snow"
} }
-- allow villages on more surfaces -- allow villages on more surfaces

View File

@ -1,10 +1,7 @@
local circles = minetest.settings:get_bool("mcl_villages_circles", true) local padding = 2
local terrace = minetest.settings:get_bool("mcl_villages_terrace", true) local top_padding = 20
local padding = tonumber(minetest.settings:get("mcl_villages_padding")) or 2 local terrace_max_ext = 6
local top_padding = tonumber(minetest.settings:get("mcl_villages_top_padding")) or 8
local terrace_max_ext = tonumber(minetest.settings:get("mcl_villages_terrace_max_ext")) or 6
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
-- function to fill empty space below baseplate when building on a hill -- function to fill empty space below baseplate when building on a hill
@ -26,16 +23,16 @@ function mcl_villages.ground(pos, pr) -- role model: Wendelsteinkircherl, Branne
end end
-- Empty space above ground -- Empty space above ground
local function overground(pos, fwidth, fdepth, fheight) local function overground(pos, fwidth, fdepth, fheight, grid)
if circles then if not grid then
local y = math.ceil(pos.y + 1) local y = math.ceil(pos.y + 1)
local radius_base = math.max(fwidth, fdepth) local radius_base = math.max(fwidth, fdepth)
local radius = math.round((radius_base / 2) + padding) local radius = math.round((radius_base / 2) + padding)
local dome = fheight + top_padding local dome = fheight + top_padding
for count2 = 1, fheight + top_padding do for count2 = 1, fheight + top_padding do
if terrace and radius_base > 3 then if radius_base > 3 then
if count2 > dome then if count2 > dome then
radius = radius - 1 radius = radius - 1
elseif count2 <= terrace_max_ext then elseif count2 <= terrace_max_ext then
@ -49,32 +46,30 @@ local function overground(pos, fwidth, fdepth, fheight)
end end
else else
local count = 1 local count = 1
if not terrace then
count = count + 2
end
if terrace then -- Must treat as square because rotation is random
for y_adj = 1, pos.y + fheight + top_padding do local adjust = math.round(math.max(fwidth, fdepth) / 2)
local pos1 = vector.offset(pos, -count, y_adj, -count)
local pos2 = vector.offset(pos, fwidth + count, y_adj, fdepth + count)
mcl_util.bulk_set_node_vm(pos1, pos2, "air")
if terrace and count <= terrace_max_ext then for y_adj = 1, fheight + top_padding do
count = count + 1 local pos1 = vector.offset(pos, -(adjust + count), y_adj, -(adjust + count))
end local pos2 = vector.offset(pos, adjust + count, y_adj, adjust + count)
end
else
local x_adjust = fwidth / 2
local z_adjust = fdepth / 2
local pos1 = vector.offset(pos, -x_adjust, 0, -z_adjust)
local pos2 = vector.offset(pos, x_adjust, fheight, z_adjust)
mcl_util.bulk_set_node_vm(pos1, pos2, "air") mcl_util.bulk_set_node_vm(pos1, pos2, "air")
-- Grid layout requires minimal terracing
if count <= padding then
count = count + 1
end
end end
-- clean out stumps and leaves
local pos1 = vector.offset(pos, -(adjust + count), 1, -(adjust + count))
local pos2 = vector.offset(pos, adjust + count, fheight + top_padding, adjust + count)
mcl_util.replace_node_vm(pos1, pos2, "group:tree", "air", true)
mcl_util.replace_node_vm(pos1, pos2, "group:leaves", "air", true)
end end
end end
function mcl_villages.terraform_new(settlement_info) function mcl_villages.terraform_new(settlement_info, grid)
local fheight, fwidth, fdepth local fheight, fwidth, fdepth
-- Do ground first so that we can clear overhang for lower buildings -- Do ground first so that we can clear overhang for lower buildings
@ -96,7 +91,7 @@ function mcl_villages.terraform_new(settlement_info)
fheight = schematic_data["size"]["y"] fheight = schematic_data["size"]["y"]
if not schematic_data["no_clearance"] then if not schematic_data["no_clearance"] then
overground(pos, fwidth, fdepth, fheight) overground(pos, fwidth, fdepth, fheight, grid)
end end
end end
end end

View File

@ -31,13 +31,14 @@ local function build_a_settlement(minp, maxp, blockseed)
local pr = PseudoRandom(blockseed) local pr = PseudoRandom(blockseed)
local settlement_info = mcl_villages.create_site_plan_new(minp, maxp, pr) local settlement_info, grid = mcl_villages.create_site_plan_new(minp, maxp, pr)
if not settlement_info then if not settlement_info then
minetest.log("No village for " .. blockseed)
return return
end end
mcl_villages.terraform_new(settlement_info, pr) mcl_villages.terraform_new(settlement_info, grid)
mcl_villages.place_schematics_new(settlement_info, pr, blockseed) mcl_villages.place_schematics_new(settlement_info, pr, blockseed)
-- TODO when run here minetest.find_path regularly fails :( -- TODO when run here minetest.find_path regularly fails :(

View File

@ -1,5 +1,5 @@
local terrace_max_ext = tonumber(minetest.settings:get("mcl_villages_terrace_max_ext")) or 6 local terrace_max_ext = 6
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
-- function to copy tables -- function to copy tables
@ -26,7 +26,7 @@ end
function mcl_villages.find_surface(pos, wait, quick) function mcl_villages.find_surface(pos, wait, quick)
local p6 = vector.new(pos) local p6 = vector.new(pos)
local cnt = 0 local cnt = 0
local itter = 1 -- count up or down local itter = 1 -- look up
local cnt_max = 200 local cnt_max = 200
local wait_time = 10000000 local wait_time = 10000000
@ -42,9 +42,18 @@ function mcl_villages.find_surface(pos, wait, quick)
else else
surface_node = mcl_vars.get_node(p6) surface_node = mcl_vars.get_node(p6)
end end
if surface_node.name=="air" or surface_node.name=="ignore" then
itter = -1 if
surface_node.name == "air"
or surface_node.name == "ignore"
or surface_node.name == "mcl_core:snow"
or (minetest.get_item_group(surface_node.name, "deco_block") > 0)
or (minetest.get_item_group(surface_node.name, "plant") > 0)
or (minetest.get_item_group(surface_node.name, "tree") > 0)
then
itter = -1 -- look down
end end
-- go through nodes an find surface -- go through nodes an find surface
while cnt < cnt_max do while cnt < cnt_max do
-- Check Surface_node and Node above -- Check Surface_node and Node above
@ -63,7 +72,7 @@ function mcl_villages.find_surface(pos, wait, quick)
mcl_villages.debug("find_surface7: " ..surface_node.name.. " " .. surface_node_plus_1.name) mcl_villages.debug("find_surface7: " ..surface_node.name.. " " .. surface_node_plus_1.name)
return p6, surface_node.name return p6, surface_node.name
else else
mcl_villages.debug("find_surface2: wrong surface+1") mcl_villages.debug("find_surface2: wrong layer above " .. surface_node_plus_1.name)
end end
else else
mcl_villages.debug("find_surface3: wrong surface "..surface_node.name.." at pos "..minetest.pos_to_string(p6)) mcl_villages.debug("find_surface3: wrong surface "..surface_node.name.." at pos "..minetest.pos_to_string(p6))