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, } for k, v in pairs(mcl_villages.schematic_houses) do count_buildings[v["name"]] = 0 end for k, v in pairs(mcl_villages.schematic_jobs) do count_buildings[v["name"]] = 0 end return count_buildings end ------------------------------------------------------------------------------- -- evaluate settlement_info and place schematics ------------------------------------------------------------------------------- -- Initialize node local function construct_node(p1, p2, name) local r = minetest.registered_nodes[name] if not r or not r.on_construct then minetest.log("warning", "[mcl_villages] No on_construct defined for node name " .. name) end local nodes = minetest.find_nodes_in_area(p1, p2, name) for p=1, #nodes do r.on_construct(nodes[p]) end return nodes end 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 i=1,math.random(3) do local v = minetest.add_entity(vector.offset(sp[math.random(#sp)],0,1,0),"mobs_mc:cat"):get_luaentity() if v then v._home = pos end end end local function init_nodes(p1, p2, pr) mcl_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 mcl_structures.init_node_construct(v) end -- Do new chest nodes first local nodes = construct_node(p1, p2, "mcl_chests:chest_small") if nodes and #nodes > 0 then for p=1, #nodes do mcl_villages.fill_chest(nodes[p], pr) end end -- Do old chest nodes after local nodes = construct_node(p1, p2, "mcl_chests:chest") if nodes and #nodes > 0 then for p=1, #nodes do mcl_villages.fill_chest(nodes[p], pr) end end end -- check ground for a single building, adjust position local function check_ground(lvm, cpos, size) local cpos, surface_material = mcl_villages.find_surface(lvm, cpos) if not cpos then return nil, nil end local pos = vector.offset(cpos, -math.floor((size.x-1)/2), 0, -math.floor((size.z-1)/2)) local ys = {pos.y} local pos_c = mcl_villages.find_surface_down(lvm, vector.offset(pos, 0, size.y, 0)) if pos_c then table.insert(ys, pos_c.y) end local pos_c = mcl_villages.find_surface_down(lvm, vector.offset(pos, size.x-1, size.y, 0)) if pos_c then table.insert(ys, pos_c.y) end local pos_c = mcl_villages.find_surface_down(lvm, vector.offset(pos, 0, size.y, size.z-1)) if pos_c then table.insert(ys, pos_c.y) end local pos_c = mcl_villages.find_surface_down(lvm, vector.offset(pos, size.x-1, size.y, size.z-1)) if pos_c then table.insert(ys, pos_c.y) end table.sort(ys) -- well supported base, not too uneven? if #ys < 5 or ys[#ys]-ys[1] > 8 then return nil, nil end cpos.y = math.floor(0.5 * (ys[math.floor(#ys/2)] + ys[math.ceil(#ys/2)]) + 0.5) -- median, rounded return cpos, surface_material end local function add_building(settlement, building, count_buildings) table.insert(settlement, building) count_buildings[building["name"]] = count_buildings[building["name"]] + 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) end local function layout_town(lvm, 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 = 4 if #input_settlement >= 12 then mindist = 3 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 = check_ground(lvm, cpos, size) -- check distance to other buildings. Note that we still want to add baseplates etc. if pos 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(lvm, 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["name"]] == 0 or building_info["num_others"] * count_buildings[building_info["name"]] < 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["name"]] == 0 or building_info["num_others"] * count_buildings[building_info["name"]] < 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 i = 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(lvm, minp, maxp, pr, settlement) end function mcl_villages.place_schematics(lvm, settlement, blockseed, pr) -- local lvm = 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 -- lvm:read_from_map(vector.new(minp.x, minp.y, minp.z), vector.new(maxp.x, maxp.y, maxp.z)) -- lvm: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( lvm, 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 = lvm:get_node_at(p) if n and n.name == "mcl_villages:no_paths" then p.y = y+1 n = lvm:get_node_at(p) if n and n.name == "air" then lvm:set_node_at(p, {name="mcl_villages:no_paths"}) end end end end end mcl_villages.store_path_ends(lvm, minp, maxp, cpos, blockseed, bell_pos) if building.name == "belltower" then -- TODO: allow multiple types? bell_center_pos = cpos local center_node = lvm:get_node_at(cpos) bell_center_node_type = center_node.name end end lvm: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 = lvm:get_emerged_area() lvm: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 = core.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(bed_pos, "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(lvm, settlement, pr) -- TODO: further optimize by using raw data arrays instead of set_node_at. But OK for a first draft. -- we make the foundations 1 node wider than requested, 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)) mcl_structures.clearance(lvm, pos.x-1, pos.y, pos.z-1, size.x+2, size.y, size.z+2, 2, building.surface_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" } 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)) mcl_structures.foundation(lvm, pos.x-2, pos.y, pos.z-2, size.x+4, -4, size.z+4, 2, surface_mat, platform_mat, stone_mat, pr) end end end