VoxeLibre/mods/MAPGEN/mcl_villages/buildings.lua

415 lines
16 KiB
Lua

local min_jobs = tonumber(minetest.settings:get("mcl_villages_min_jobs")) or 1
local max_jobs = tonumber(minetest.settings:get("mcl_villages_max_jobs")) or 12
local placement_priority = minetest.settings:get("mcl_villages_placement_priority") or "random"
local foundation_materials = {}
foundation_materials["mcl_core:sand"] = "mcl_core:sandstone"
foundation_materials["mcl_core:redsand"] = "mcl_core:redsandstone"
local S = minetest.get_translator(minetest.get_current_modname())
-- initialize settlement_info
function mcl_villages.initialize_settlement_info(pr)
local count_buildings = {
number_of_jobs = pr:next(min_jobs, max_jobs),
num_jobs = 0,
num_beds = 0,
}
return count_buildings
end
-------------------------------------------------------------------------------
-- evaluate settlement_info and place schematics
-------------------------------------------------------------------------------
local function spawn_cats(pos)
local sp=minetest.find_nodes_in_area_under_air(vector.offset(pos,-20,-20,-20),vector.offset(pos,20,20,20),{"group:opaque"})
for _ = 1, math.random(3) do
local v = minetest.add_entity(vector.offset(sp[math.random(#sp)],0,1,0),"mobs_mc:cat")
if v and v:get_luaentity() then v:get_luaentity()._home = pos end
end
end
local function init_nodes(p1, p2, pr)
vl_structures.construct_nodes(p1, p2, {
"mcl_itemframes:item_frame",
"mcl_itemframes:glow_item_frame",
"mcl_furnaces:furnace",
"mcl_anvils:anvil",
"mcl_books:bookshelf",
"mcl_armor_stand:armor_stand",
-- "mcl_smoker:smoker",
-- "mcl_barrels:barrel_closed",
-- "mcl_blast_furnace:blast_furnace",
-- "mcl_brewing:stand_000",
})
-- Support mods with custom job sites
local job_sites = minetest.find_nodes_in_area(p1, p2, mobs_mc.jobsites)
for _, v in pairs(job_sites) do
vl_structures.init_node_construct(v)
end
local nodes = vl_structures.construct_nodes(p1, p2, {"mcl_chests:chest_small", "mcl_chests:chest" }) or {}
for p=1, #nodes do
mcl_villages.fill_chest(nodes[p], pr)
end
end
local function add_building(settlement, building, count_buildings)
table.insert(settlement, building)
count_buildings[building.name] = (count_buildings[building.name] or 0) + 1
count_buildings.num_jobs = count_buildings.num_jobs + (building.num_jobs or 0)
count_buildings.num_beds = count_buildings.num_beds + (building.num_beds or 0)
if building.group then
count_buildings[building.group] = (count_buildings[building.group] or 0) + 1
end
end
local function layout_town(vm, minp, maxp, pr, input_settlement)
local center = vector.new(pr:next(minp.x + 24, maxp.x - 24), maxp.y, pr:next(minp.z + 24, maxp.z - 24))
minetest.log("action", "[mcl_villages] sudo make me a village at: " .. minetest.pos_to_string(minp).." - "..minetest.pos_to_string(maxp))
local possible_rotations = {"0", "90", "180", "270"}
local center_surface
local settlement = {}
-- now some buildings around in a circle, radius = size of town center
local x, y, z, r, lastr = center.x, center.y, center.z, 0, 99
local mindist = 3
if #input_settlement >= 12 then mindist = 2 end
-- draw j circles around center and increase radius by math.random(2,4)
for j = 1,20 do
local steps = math.min(math.floor(math.pi * 2 * r / 2), 30) -- try up to 30 angles
for a = 0, steps - 1 do
if #settlement == #input_settlement then break end -- everything placed
local angle = a * 71 / steps * math.pi * 2 -- prime to increase randomness
local cpos = vector.new(math.floor(x + r * math.cos(angle) + 0.5), y, math.floor(z - r * math.sin(angle) + 0.5))
local building = table.copy(input_settlement[#settlement + 1])
local size = vector.copy(building.size)
--local rotation = possible_rotations[pr:next(1, #possible_rotations)]
local rotation = math.floor(math.atan2(center.z-cpos.z, center.x-cpos.x) / math.pi * 2+6.5)%4
rotation = possible_rotations[1+rotation]
if rotation == "90" or rotation == "270" then size.x, size.z = size.z, size.x end
local tlpos = vector.offset(cpos, -math.floor((size.x-1)/2), 0, -math.floor((size.z-1)/2))
-- ensure we have 3 space for terraforming, and avoid problems with VoxelManip
if tlpos.x - 3 >= minp.x and tlpos.x + size.x + 3 <= maxp.x
and tlpos.z + 3 >= minp.z and tlpos.z + size.y + 3 <= maxp.z then
local pos, surface_material = vl_terraforming.find_level_vm(vm, cpos, size)
-- check distance to other buildings. Note that we still want to add baseplates etc.
if pos and mcl_villages.surface_mat[surface_material.name] and mcl_villages.check_distance(settlement, cpos, size.x, size.z, mindist) then
-- use town bell as new reference point for placement height
if #settlement == 0 then
center_surface, y = cpos, math.min(maxp.y, pos.y + mcl_villages.max_height_difference * 0.5 + 1)
end
-- limit height differences to town center, but gradually allow more
if math.abs(pos.y - center_surface.y) <= mcl_villages.max_height_difference * (0.25 + math.min(r/40,0.5)) then
local minp = vector.offset(pos, -math.floor((size.x-1)/2), building.yadjust, -math.floor((size.z-1)/2))
building.minp = minp
building.maxp = vector.offset(minp, size.x, size.y, size.z)
building.pos = pos
building.size = size
building.rotation = rotation
building.surface_mat = surface_material
table.insert(settlement, building)
-- minetest.log("verbose", "[mcl_villages] Planning "..schema["name"].." at "..minetest.pos_to_string(pos))
lastr = r
else
minetest.log("verbose", "Too large height difference "..math.abs(pos.y - center_surface.y).." at distance "..r)
end
end
end
end
r = r + pr:next(2,4)
if r > lastr + 20 then -- too disconnected
minetest.log("verbose", "Disconnected village "..r.." > "..lastr)
break
end
end
-- minetest.log("verbose", "Planned "..#input_settlement.." buildings, placed "..#settlement)
if #settlement < #input_settlement and #settlement < 6 then
minetest.log("action", "[mcl_villages] Bad village location, could only place "..#settlement.." buildings at "..minetest.pos_to_string(center))
return
end
minetest.log("action", "[mcl_villages] village plan completed at " .. minetest.pos_to_string(center))
return settlement
end
function mcl_villages.create_site_plan(vm, minp, maxp, pr)
local settlement = {}
-- initialize all settlement_info table
local count_buildings = mcl_villages.initialize_settlement_info(pr)
-- first building is townhall in the center
local bindex = pr:next(1, #mcl_villages.schematic_bells)
local bell_info = table.copy(mcl_villages.schematic_bells[bindex])
if mcl_villages.mandatory_buildings['jobs'] then
for _, bld_name in pairs(mcl_villages.mandatory_buildings['jobs']) do
local building_info = info_for_building(bld_name, mcl_villages.schematic_jobs)
add_building(settlement, building_info, count_buildings)
end
end
while count_buildings.num_jobs < count_buildings.number_of_jobs do
local rindex = pr:next(1, #mcl_villages.schematic_jobs)
local building_info = mcl_villages.schematic_jobs[rindex]
if
(building_info.min_jobs == nil or count_buildings.number_of_jobs >= building_info.min_jobs)
and (building_info.max_jobs == nil or count_buildings.number_of_jobs <= building_info.max_jobs)
and (
building_info.num_others == nil
or (count_buildings[building_info.group or building_info.name] or 0) == 0
or building_info.num_others * (count_buildings[building_info.group or building_info.name] or 0) < count_buildings.num_jobs
)
then
add_building(settlement, building_info, count_buildings)
end
end
if mcl_villages.mandatory_buildings['houses'] then
for _, bld_name in pairs(mcl_villages.mandatory_buildings['houses']) do
local building_info = info_for_building(bld_name, mcl_villages.schematic_houses)
add_building(settlement, building_info, count_buildings)
end
end
while count_buildings.num_beds <= count_buildings.num_jobs do
local rindex = pr:next(1, #mcl_villages.schematic_houses)
local building_info = mcl_villages.schematic_houses[rindex]
if
(building_info.min_jobs == nil or count_buildings.number_of_jobs >= building_info.min_jobs)
and (building_info.max_jobs == nil or count_buildings.number_of_jobs <= building_info.max_jobs)
and (
building_info.num_others == nil
or (count_buildings[building_info.group or building_info.name] or 0) == 0
or building_info.num_others * (count_buildings[building_info.group or building_info.name] or 0) < count_buildings.num_jobs
)
then
add_building(settlement, building_info, count_buildings)
end
end
-- Based on number of villagers
local num_wells = pr:next(1, math.ceil(count_buildings.num_beds / 10))
for _ = 1, num_wells do
local windex = pr:next(1, #mcl_villages.schematic_wells)
local cur_schem = table.copy(mcl_villages.schematic_wells[windex])
table.insert(settlement, pr:next(1, #settlement), cur_schem)
end
if placement_priority == "jobs" then
-- keep ordered as is
elseif placement_priority == "houses" then
table.reverse(settlement)
else
settlement = mcl_villages.shuffle(settlement, pr)
end
table.insert(settlement, 1, bell_info)
return layout_town(vm, minp, maxp, pr, settlement)
end
function mcl_villages.place_schematics(vm, settlement, blockseed, pr)
-- local vm = VoxelManip()
local bell_pos = vector.offset(settlement[1].minp, math.floor(settlement[1].size.x/2), 0, math.floor(settlement[1].size.z/2))
local bell_center_pos
local bell_center_node_type
for i, building in ipairs(settlement) do
local minp, cpos, maxp, size, rotation = building.minp, building.pos, building.maxp, building.size, building.rotation
-- adjust the schema to match location and biome
local surface_material = building.surface_mat or {name = "mcl_core:dirt" }
local platform_material = building.platform_mat or building.surface_mat or {name = "mcl_core:stone" }
local schem_lua = building.schem_lua
schem_lua = schem_lua:gsub('"mcl_core:dirt"', '"'..platform_material.name..'"')
schem_lua = schem_lua:gsub('"mcl_core:dirt_with_grass"', '"'..surface_material.name..'"') -- also keeping param2 would be nicer, grass color
schem_lua = mcl_villages.substitute_materials(cpos, schem_lua, pr)
local schematic = loadstring(schem_lua)()
-- the foundation and air space for the building was already built before
-- vm:read_from_map(vector.new(minp.x, minp.y, minp.z), vector.new(maxp.x, maxp.y, maxp.z))
-- vm:get_data()
-- now added in placement code already, pos has the primary height if (building.yadjust or 0) ~= 0 then minp = vector.offset(minp, 0, building.yadjust, 0) end
-- minetest.log("debug", "placing schematics for "..building.name.." at "..minetest.pos_to_string(minp).." on "..surface_material)
minetest.place_schematic_on_vmanip(
vm,
minp,
schematic,
rotation,
nil,
true,
{ place_center_x = false, place_center_y = false, place_center_z = false }
)
-- to help pathing, increase the height of no_path areas
local p = vector.zero()
for z = minp.z,maxp.z do
p.z = z
for x = minp.x,maxp.x do
p.x = x
for y = minp.y,maxp.y-1 do
p.y = y
local n = vm:get_node_at(p)
if n and n.name == "mcl_villages:no_paths" then
p.y = y+1
n = vm:get_node_at(p)
if n and n.name == "air" then
vm:set_node_at(p, {name="mcl_villages:no_paths"})
end
end
end
end
end
mcl_villages.store_path_ends(vm, minp, maxp, cpos, blockseed, bell_pos)
if building.name == "belltower" then -- TODO: allow multiple types?
bell_center_pos = cpos
local center_node = vm:get_node_at(cpos)
bell_center_node_type = center_node.name
end
end
vm:write_to_map(true) -- for path finder and light
local biome_data = minetest.get_biome_data(bell_pos)
local biome_name = minetest.get_biome_name(biome_data.biome)
mcl_villages.paths(blockseed, biome_name)
-- this will run delayed actions, such as spawning mobs
minetest.set_node(bell_center_pos, { name = "mcl_villages:village_block" })
local meta = minetest.get_meta(bell_center_pos)
meta:set_string("blockseed", blockseed)
meta:set_string("node_type", bell_center_node_type)
meta:set_string("infotext", S("The timer for this @1 has not run yet!", bell_center_node_type))
minetest.get_node_timer(bell_center_pos):start(1.0)
for i, building in ipairs(settlement) do
init_nodes(vector.offset(building.minp,-2,-2,-2), vector.offset(building.maxp,2,2,2), pr)
end
-- read back any changes
local emin, emax = vm:get_emerged_area()
vm:read_from_map(emin, emax)
end
function mcl_villages.post_process_village(blockseed)
local village_info = mcl_villages.get_village(blockseed)
if not village_info then
return
end
-- minetest.log("Postprocessing village")
local settlement_info = village_info.data
local jobs = {}
local beds = {}
local bell_pos = vector.copy(settlement_info[1]["pos"])
local bell = vector.offset(bell_pos, 0, 2, 0)
local biome_data = minetest.get_biome_data(bell_pos)
local biome_name = minetest.get_biome_name(biome_data.biome)
--mcl_villages.paths(blockseed, biome_name)
local l = minetest.add_entity(bell, "mobs_mc:iron_golem"):get_luaentity()
if l then
l._home = bell
else
minetest.log("info", "Could not create a golem!")
end
spawn_cats(bell)
for _, building in pairs(settlement_info) do
local has_beds = building["num_beds"] and building["num_beds"] ~= nil
local has_jobs = building["num_jobs"] and building["num_jobs"] ~= nil
local minp, maxp = building["minp"], building["maxp"]
if has_jobs then
local jobsites = minetest.find_nodes_in_area(minp, maxp, mobs_mc.jobsites)
for _, job_pos in pairs(jobsites) do
table.insert(jobs, job_pos)
end
end
if has_beds then
local bld_beds = minetest.find_nodes_in_area(minp, maxp, { "group:bed" })
for _, bed_pos in pairs(bld_beds) do
local bed_node = minetest.get_node(bed_pos)
local bed_group = minetest.get_item_group(bed_node.name, "bed")
-- We only spawn at bed bottoms
-- 1 is bottom, 2 is top
if bed_group == 1 then
table.insert(beds, bed_pos)
end
end
end
end
-- minetest.log("beds: "..#beds.." jobsites: "..#jobs)
if beds then
for _, bed_pos in pairs(beds) do
local res = minetest.forceload_block(bed_pos, true)
if res then
mcl_villages.forced_blocks[minetest.pos_to_string(bed_pos)] = minetest.get_us_time()
end
local m = minetest.get_meta(bed_pos)
m:set_string("bell_pos", minetest.pos_to_string(bell_pos))
if m:get_string("villager") == "" then
local v = minetest.add_entity(vector.offset(bed_pos, 0, 0.06, 0), "mobs_mc:villager")
if v then
local l = v:get_luaentity()
l._bed = bed_pos
l._bell = bell_pos
m:set_string("villager", l._id)
m:set_string("infotext", S("A villager sleeps here"))
local job_pos = table.remove(jobs, 1)
if job_pos then
villager_employ(l, job_pos) -- HACK: merge more MCLA villager code
end
for _, callback in pairs(mcl_villages.on_villager_placed) do
callback(v, blockseed)
end
else
minetest.log("info", "Could not create a villager!")
end
else
minetest.log("info", "bed already owned by " .. m:get_string("villager"))
end
end
end
end
-- Terraform for an entire village
function mcl_villages.terraform(vm, settlement, pr)
-- TODO: sort top-down, then bottom-up, or opposite?
-- we make the foundations 2 node wider than necessary, to have one node for path laying
for i, building in ipairs(settlement) do
if not building.no_clearance then
local pos, size = building.pos, building.size
pos = vector.offset(pos, -math.floor(size.x/2), 0, -math.floor(size.z/2))
vl_terraforming.clearance_vm(vm, pos.x-1, pos.y, pos.z-1, size.x+2, size.y, size.z+2, 2, building.surface_mat, building.dust_mat, pr)
end
end
for i, building in ipairs(settlement) do
if not building.no_ground_turnip then
local pos, size = building.pos, building.size
local surface_mat = building.surface_mat
local platform_mat = building.platform_mat or { name = foundation_materials[surface_mat.name] or "mcl_core:dirt" }
local stone_mat = building.stone_mat or { name = "mcl_core:stone" }
local dust_mat = building.dust_mat
building.platform_mat = platform_mat -- remember for use in schematic placement
building.stone_mat = stone_mat
pos = vector.offset(pos, -math.floor((size.x-1)/2), 0, -math.floor((size.z-1)/2))
vl_terraforming.foundation_vm(vm, pos.x-2, pos.y, pos.z-2, size.x+4, -5, size.z+4, 2, surface_mat, platform_mat, stone_mat, dust_mat, pr)
end
end
end