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_armor_trims",
"mcl_villages",
"AreaStore",
}
read_globals = {

View File

@ -1161,10 +1161,18 @@ function mcl_util.traverse_tower(pos, dir, callback)
end
-- 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)
local c_from = minetest.get_content_id(mat_from)
function mcl_util.replace_node_vm(pos1, pos2, mat_from, mat_to, is_group)
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 emin, emax = vm:read_from_map(pos1, pos2)
local a = VoxelArea:new({
@ -1178,12 +1186,19 @@ function mcl_util.replace_node_vm(pos1, pos2, mat_from, mat_to)
for y = pos1.y, pos2.y do
for x = pos1.x, pos2.x do
local vi = a:index(x, y, z)
if is_group then
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
-- Write data
vm:set_data(data)

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.
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
local function layout_town(minp, maxp, pr, input_settlement_info)
local settlement_info = {}
local xdist = math.abs(minp.x - maxp.x)
local zdist = math.abs(minp.z - maxp.z)
-- TODO consided using mod_storage to persist this...
local areas = AreaStore()
areas:set_cache_params({ enabled = true, block_radius = 16, limit = 100 })
-- 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))
)
local function record_building(record)
local placement_pos = vector.copy(record.pos)
-- find center_surface of village
local center_surface, surface_material = mcl_villages.find_surface(center, true)
-- Allow adjusting y axis
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
local chunks = {}
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
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
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
end
@ -305,7 +489,13 @@ function mcl_villages.create_site_plan_new(minp, maxp, pr)
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
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:redsand",
--"mcl_core:silver_sand",
"mcl_core:snow"
--"mcl_core:snow"
}
-- allow villages on more surfaces

View File

@ -1,10 +1,7 @@
local circles = minetest.settings:get_bool("mcl_villages_circles", true)
local terrace = minetest.settings:get_bool("mcl_villages_terrace", true)
local padding = tonumber(minetest.settings:get("mcl_villages_padding")) or 2
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
local padding = 2
local top_padding = 20
local terrace_max_ext = 6
-------------------------------------------------------------------------------
-- 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
-- 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 radius_base = math.max(fwidth, fdepth)
local radius = math.round((radius_base / 2) + padding)
local dome = fheight + top_padding
for count2 = 1, fheight + top_padding do
if terrace and radius_base > 3 then
if radius_base > 3 then
if count2 > dome then
radius = radius - 1
elseif count2 <= terrace_max_ext then
@ -49,32 +46,30 @@ local function overground(pos, fwidth, fdepth, fheight)
end
else
local count = 1
if not terrace then
count = count + 2
end
if terrace then
for y_adj = 1, pos.y + fheight + top_padding do
local pos1 = vector.offset(pos, -count, y_adj, -count)
local pos2 = vector.offset(pos, fwidth + count, y_adj, fdepth + count)
-- Must treat as square because rotation is random
local adjust = math.round(math.max(fwidth, fdepth) / 2)
for y_adj = 1, fheight + top_padding do
local pos1 = vector.offset(pos, -(adjust + count), y_adj, -(adjust + count))
local pos2 = vector.offset(pos, adjust + count, y_adj, adjust + count)
mcl_util.bulk_set_node_vm(pos1, pos2, "air")
if terrace and count <= terrace_max_ext then
-- Grid layout requires minimal terracing
if count <= padding then
count = count + 1
end
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")
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
function mcl_villages.terraform_new(settlement_info)
function mcl_villages.terraform_new(settlement_info, grid)
local fheight, fwidth, fdepth
-- 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"]
if not schematic_data["no_clearance"] then
overground(pos, fwidth, fdepth, fheight)
overground(pos, fwidth, fdepth, fheight, grid)
end
end
end

View File

@ -31,13 +31,14 @@ local function build_a_settlement(minp, maxp, 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
minetest.log("No village for " .. blockseed)
return
end
mcl_villages.terraform_new(settlement_info, pr)
mcl_villages.terraform_new(settlement_info, grid)
mcl_villages.place_schematics_new(settlement_info, pr, blockseed)
-- 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
@ -26,7 +26,7 @@ end
function mcl_villages.find_surface(pos, wait, quick)
local p6 = vector.new(pos)
local cnt = 0
local itter = 1 -- count up or down
local itter = 1 -- look up
local cnt_max = 200
local wait_time = 10000000
@ -42,9 +42,18 @@ function mcl_villages.find_surface(pos, wait, quick)
else
surface_node = mcl_vars.get_node(p6)
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
-- go through nodes an find surface
while cnt < cnt_max do
-- 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)
return p6, surface_node.name
else
mcl_villages.debug("find_surface2: wrong surface+1")
mcl_villages.debug("find_surface2: wrong layer above " .. surface_node_plus_1.name)
end
else
mcl_villages.debug("find_surface3: wrong surface "..surface_node.name.." at pos "..minetest.pos_to_string(p6))