-- Features: -- * each house has a door and one mese lamp per floor -- * houses can have multiple floors -- * each house comes with a ladder for access to all floors -- * normal saddle roofs and flat roofs supported -- * trees, plants and snow inside the house are not cleared -- -> the houses look abandoned (ready for players to move in) -- * houses look acceptable but leave a lot of room for improvement -- through their future inhabitants -- (no windows in gable, no decoration, no cellar, no furniture, -- no mini-house for elevator/ladder on top of skyscrapers, ...) -- * if the saddle roof does not fit into the height volume that is -- reserved for the house, the top of the roof is made flat -- Technical stuff: -- * used function from handle_schematics to mark parts of the heightmap as used -- * glass panes, glass and obisidan glass are more common than bars -- * windows are no longer "filled" (param2 now set to 0) -- * doors are sourrounded by wall node and not glass panes or bars -- (would look strange and leave gaps otherwise) -- Known issues: -- * cavegen may eat holes into the ground below the house -- * houses may very seldom overlap simple_houses = {}; -- generate at max this many houses per mapchunk; -- Note: This amount will likely only spawn if your mapgen is very flat. -- Else you will see far less houses. simple_houses.max_per_mapchunk = 20; -- how many houses shall be generated on average per mapchunk? simple_houses.houses_wanted_per_mapchunk = 1; -- how many mapchunks have been generated since the server was started? simple_houses.mapchunks_processed = 0; -- how many houses have been generated in these mapchunks? simple_houses.houses_generated = 0; -- build either the two walls of the box that forms the house in x or z direction; -- windows are added randomly -- parameters: -- p starting point of these walls -- sizex length of the entire building in x direction -- sizez same for z direction -- in_x_direction do we have to build the two walls in x direction or the two in z direction? -- materials needs to contain at least the fields -- walls node name of wall material -- glass node name of glass material -- color optional; param2-color-value for wall node -- rotation_1 param2 for materials.wall nodes for the first wall -- rotation_2 param2 for materials.wall nodes for the second wall -- vm voxel manipulator simple_houses.build_two_walls = function( p, sizex, sizez, in_x_direction, materials, vm) local v = 0; if( not( in_x_direction )) then v = 2; end -- param2 (orientation or color) for the first two walls; -- tree logs need to be orientated correctly, colored nodes have to keep their color; local node_wall_1 = {name=materials.walls, param2 = (materials.color or materials.wall_orients[1+v])}; local node_wall_2 = {name=materials.walls, param2 = (materials.color or materials.wall_orients[2+v])}; -- glass panes and metal bars need the correct rotation and no color value local node_glass_1 = {name=materials.glass, param2 = materials.glass_orients[1+v]}; local node_glass_2 = {name=materials.glass, param2 = materials.glass_orients[2+v]}; -- solid glass needs a rotation of 0 (else it would be interpreted as level) if( minetest.registered_nodes[ materials.glass ] and minetest.registered_nodes[ materials.glass ].paramtype2 == "glasslikeliquidlevel") then node_glass_1.param2 = 0; node_glass_2.param2 = 0; end local w1_x; local w2_x; local w1_z; local w2_z; local size; if( in_x_direction ) then w1_x = p.x; w2_x = p.x; w1_z = p.z; w2_z = p.z+sizez; size = sizex+1; else w1_x = p.x; w2_x = p.x+sizex; w1_z = p.z; w2_z = p.z; size = sizez+1; end -- place windows at even or odd rows? -> create some variety local window_at_odd_row = false; if( math.random(1,2)==1 ) then window_at_odd_row = true; end -- place where a door or ladder might be added (no window there); -- we need to avid adding ladders directly in front of windows or -- placing doors right next to glass panes because that would look ugly local special_wall_1 = math.random(3,size-3); local special_wall_2 = math.random(3,size-3); if( special_wall_2 == special_wall_1 ) then special_wall_2 = special_wall_2 - 1; if( special_wall_2 < 3 ) then special_wall_2 = 4; end end local wall_height = #materials.window_at_height; for lauf = 1, size do local wall_1_has_window = false; local wall_2_has_window = false; -- the corners never get glass if( lauf>1 and lauf1) then i = i-1; else i = i+1; end end -- ladders need to be placed on the right side so that people can climb up if( use_this_one and places[use_this_one]) then i = use_this_one; end local at_odd_row = (places[i]%2==1); if( (i==1 or i==2) and (places[5]==at_odd_row)) then return {x=p.x+places[i], y=p.y, z=p.z+1+offset, p2=5, used=i}; elseif( (i==1 or i==2) and (places[5]~=at_odd_row)) then return {x=p.x+places[i], y=p.y, z=p.z-1-offset+sizez, p2=4, used=i}; elseif( (i==3 or i==4) and (places[6]==at_odd_row)) then return {x=p.x+1+offset, y=p.y, z=p.z+places[i], p2=3, used=i}; elseif( (i==3 or i==4) and (places[6]~=at_odd_row)) then return {x=p.x-1-offset+sizex, y=p.y, z=p.z+places[i], p2=2, used=i}; else return {x=p.x, y=p.y, z=p.z, used=0}; end end -- add a ladder from bottom to top (staircases would be nicer but are too difficult to do well) -- if flat_roof is false, the ladder needs to be placed on the smaller side so that people can -- actually climb it; -- ladder_places are the special places simple_houses.build_two_walls(..) has reserved simple_houses.place_ladder = function( p, sizex, sizez, ladder_places, ladder_height, flat_roof, vm ) -- place the ladder at the galbe side in houses with a real roof (else -- climbing the ladder up to the roof would fail due to lack of room) local use_place = nil; if( not( flat_roof) and (sizex < sizez )) then use_place = math.random(1,2); elseif( not( flat_roof) and (sizex >= sizez )) then use_place = math.random(3,4); end -- select one of the four reserved places local res = simple_houses.get_random_place( p, sizex, sizez, ladder_places, use_place, -1, 1 ); local ladder_node = {name="default:ladder_steel", param2 = res.p2}; -- actually place the ladders for height=p.y+1, p.y + ladder_height do vm:set_node_at( {x=res.x, y=height, z=res.z}, ladder_node ); end return res.used; end -- place the door into one of the reserved places simple_houses.place_door = function( p, sizex, sizez, door_places, wall_with_ladder, floor_height, vm ) local res = simple_houses.get_random_place( p, sizex, sizez, door_places, -1, wall_with_ladder, 0 ); vm:set_node_at( {x=res.x, y=p.y+1, z=res.z}, {name="doors:door_wood_a", param2 = 0 }); vm:set_node_at( {x=res.x, y=p.y+2, z=res.z}, {name="doors:hidden"}); -- light so that the door can be found vm:set_node_at( {x=res.x, y=p.y+3, z=res.z}, {name="default:meselamp"}); -- add some light to the upper floors as well for i,height in ipairs( floor_height ) do if( i>2) then vm:set_node_at( {x=res.x,y=height-1,z=res.z},{name="default:meselamp"}); end end return res.used; end -- the chest is placed on one of the upper floors; it contains -- additional building material simple_houses.place_chest = function( p, sizex, sizez, chest_places, wall_with_ladder, floor_height, vm, materials ) -- not each building needs a chest if( math.random(1,2)>1 ) then return; end local res = simple_houses.get_random_place( p, sizex, sizez, chest_places, -1, wall_with_ladder, 1 ); local height = floor_height[ math.random(2,#floor_height)]; -- translate wallmounted (for ladder) to facedir for chest res.p2 = res.p2; if( res.p2 == 5 ) then res.p2n = 2; elseif( res.p2 == 4 ) then res.p2n = 0; elseif( res.p2 == 3 ) then res.p2n = 3; elseif( res.p2 == 2 ) then res.p2n = 1; end -- determine target position local pos = {x=res.x, y=height+1, z=res.z}; -- if plasterwork is installed: place a machine if( materials.color and minetest.registered_nodes["plasterwork:machine"] and math.random(1,10)==1) then vm:set_node_at( pos, {name=materials.walls, param2 = materials.color}); local pos2 = {x=res.x, y=height+2, z=res.z}; vm:set_node_at( pos2, {name="plasterwork:machine", param2 = res.p2n}); minetest.registered_nodes[ "plasterwork:machine" ].after_place_node(pos2, nil, nil); local meta = minetest.get_meta( pos2); meta:set_string( "target_node", materials.walls ); meta:set_int( "target_color", materials.color ); return; end -- place the chest vm:set_node_at( pos, {name="default:chest", param2 = res.p2n}); -- fill chest with building material minetest.registered_nodes[ "default:chest" ].on_construct( pos ); local meta = minetest.get_meta(pos); local inv = meta:get_inventory(); local c = math.random(1,4); for i=1,c do local stack_name = materials.walls.." "..math.random(1,99); if( materials.color ) then stack_name = minetest.itemstring_with_palette( stack_name, materials.color ); end inv:add_item( "main", stack_name ); end inv:add_item( "main", materials.first_floor.." "..math.random(1,49) ); c = math.random(1,2); for i=1,c do inv:add_item( "main", materials.ceiling.." "..math.random(1,99) ); end inv:add_item( "main", materials.glass.." "..math.random(1,20) ); if( not( materials.roof_flat )) then inv:add_item( "main", materials.roof.." "..math.random(1,99) ); inv:add_item( "main", materials.roof_middle.." "..math.random(1,49) ); end end -- locate a place for the "hut" simple_houses.simple_hut_find_place = function( heightmap, minp, maxp, sizex, sizez, minheight, maxheight ) local res = handle_schematics.find_flat_land_get_candidates_fast( heightmap, minp, maxp, sizex, sizez, minheight, maxheight ); -- print( "Places found of size "..tostring( sizex ).."x"..tostring(sizez)..": "..tostring( #res.places_x ).. -- " and "..tostring( sizez ).."x"..tostring(sizex)..": "..tostring( #res.places_z ).. -- "."); if( (#res.places_x + #res.places_z )< 1 ) then -- print( " Aborting. No place found."); return nil; end -- select a random place - either sizex x sizez or sizez x sizex local c = math.random( 1, #res.places_x + #res.places_z ); local i = 1; if( c > #res.places_x ) then i = res.places_z[ c-#res.places_x ]; -- swap x and z due to rotation of 90 or 270 degree local tmp = sizex; sizex = sizez; sizez = tmp; tmp = nil; else i = res.places_x[ c ]; end local chunksize = maxp.x - minp.x + 1; -- translate index back into coordinates local p = {x=minp.x+(i%chunksize)-1, y=heightmap[ i ], z=minp.z+math.floor(i/chunksize), i=i}; return {p1={x=p.x - sizex, y=p.y, z=p.z - sizez }, p2=p, sizex=sizex, sizez=sizez}; end -- actually build the hut simple_houses.simple_hut_get_materials = function( data, amount_in_this_mapchunk, chunk_ends_at_height ) -- select some random materials, height etc. -- wood is always useful local wood_types = replacements_group['wood'].found; local wood = wood_types[ math.random(1,#wood_types)]; local wood_roof = wood_types[ math.random(1,#wood_types)]; -- glass can be glass panes, iron bars or solid glass local glass_materials = {"xpanes:pane_flat","xpanes:pane_flat","xpanes:pane_flat", "default:glass","default:glass", "default:obsidian_glass", "xpanes:bar_flat"}; -- choose random materials local materials = { walls = nil, color = nil, gable = nil, glass = glass_materials[ math.random( 1,#glass_materials )], roof = replacements_group['wood'].data[ wood_roof ][7], -- stair roof_middle = replacements_group['wood'].data[ wood_roof ][8], -- slab first_floor = "default:brick", ceiling = wood_types[ math.random(1,#wood_types)], wall_orients = {0,1,2,3}, glass_orients = {12,18,9,7}, }; -- windows 3 nodes high, 2 high, or just 1? local r = math.random(1,6); if( r==1 or r==2) then materials.window_at_height = {0,1,1,1,0}; elseif( r==3 or r==4 or r==5) then materials.window_at_height = {0,0,1,1,0}; else materials.window_at_height = {0,0,1,0,0}; end -- how many floors will the house have? local max_floors_possible = math.floor((chunk_ends_at_height-1-data.p2.y)/#materials.window_at_height); if( math.random(1,5)==1) then materials.floors = math.random(1,math.min(8,max_floors_possible-1)); else materials.floors = math.random(1,math.min(4,max_floors_possible-1)); end -- some houses may have a flat roof instead of a saddle roof materials.flat_roof = false; if( math.random(1,2)==1) then materials.flat_roof = true; end -- which wall material shall be used? if( plasterwork and math.random(1,2)==1 ) then -- colored plasterwork materials.walls = plasterwork.node_list[ math.random(1, #plasterwork.node_list)]; materials.color = math.random(0,255); else local r = math.random(1,3); -- wooden house if( r==1 ) then materials.walls = wood; -- wooden houses with more than 3 floors would be strange materials.floors = math.random(1, math.min( 3, max_floors_possible-1 )); -- flat roofs do not look good on them either materials.flat_roof = false; -- vertical wood is also pretty decorative if( math.random(1,2)==1 ) then materials.wall_orients = {12,18,9,7}; end -- tree logs elseif( r==2 ) then materials.walls = replacements_group['wood'].data[ wood ][4]; -- tree trunk -- log cabins with more than 2 floors are unlikely materials.floors = math.random(1, math.min( 2, max_floors_possible-1 )); -- log cabins do not have a flat roof either materials.flat_roof = false; materials.wall_orients = {12,18,9,7}; else local wall_options = {"default:brick", "default:stonebrick", "default:desert_stonebrick", "default:sandstonebrick", "default:desert_stonebrick", "default:silver_sandstone_brick", "default:obsidianbrick", "default:stone_block", "default:sandstone_block", "default:desert_sandstone_block", "default:silver_sandstone_block", "default:obsidian_block"}; materials.walls = wall_options[ math.random(1,#wall_options)]; end end -- if there are less than three houses in a mapchunk: do not place skyscrapers if( amount_in_this_mapchunk < 3 ) then -- use saddle roof instead of flat one materials.roof_flat = false; -- at max two floors materials.floors = math.min( 2, materials.floors ); end materials.gable = materials.walls; if( math.random(1,3)==1 ) then materials.gable = wood_types[ math.random(1,#wood_types)]; end local height = materials.floors * #materials.window_at_height +1; if( materials.flat_roof ) then data.p2.ymax = math.min( chunk_ends_at_height, data.p2.y + height + math.ceil( math.min( data.sizex, data.sizez )/2 )); else data.p2.ymax = math.min( chunk_ends_at_height, data.p2.y + height + 4); end data.p2.ymax = math.min( chunk_ends_at_height, data.p2.ymax ); data.materials = materials; return data; end -- actually build the "hut" simple_houses.simple_hut_place_hut = function( data, materials, heightmap ) local p = data.p2; local sizex = data.sizex-1; local sizez = data.sizez-1; -- house too small or too large if( sizex < 3 or sizez < 3 or sizex>64 or sizez>64) then return nil; end print( " Placing house at "..minetest.pos_to_string( p )); local vm = minetest.get_voxel_manip(); local minp2, maxp2 = vm:read_from_map( {x=p.x - sizex, y=p.y-1, z=p.z - sizez }, {x=p.x, y=p.ymax, z=p.z}); -- replaicate the pattern of windows for the other floors local first_floor_height = #materials.window_at_height; local floor_height = {p.y}; local floor_materials = {{name=materials.first_floor}}; for i=1,materials.floors-1 do for k=2,first_floor_height do table.insert( materials.window_at_height, materials.window_at_height[k]); end table.insert( floor_height, floor_height[ #floor_height] + first_floor_height-1); table.insert( floor_materials, {name=materials.ceiling}); end table.insert( floor_height, floor_height[ #floor_height] + first_floor_height-1); if( materials.flat_roof ) then table.insert( floor_materials, {name=materials.walls, param2 = (materials.color or 12)}); table.insert( materials.window_at_height, 0 ); else table.insert( floor_materials, {name=materials.walls, param2 = (materials.color or 12)}); end local p_start = {x=p.x-sizex+1, y=p.y-1, z=p.z-sizez+1}; -- build the two walls in x direction local s1 = simple_houses.build_two_walls(p_start, sizex-2, sizez-2, true, materials, vm ); --12, 18, vm ); -- build the two walls in z direction local s2 = simple_houses.build_two_walls(p_start, sizex-2, sizez-2, false, materials, vm ); -- 9, 7, vm ); -- each floor is 4 blocks heigh local roof_starts_at = p.y + (4*materials.floors); p_start = {x=p.x-sizex, y=roof_starts_at, z=p.z-sizez, ymax = p.ymax}; -- build the roof if( materials.flat_roof ) then -- build a flat roof elseif( sizex < sizez ) then simple_houses.build_roof_and_gable(p_start, sizex, sizez, true, materials, 1, 3, vm ); else simple_houses.build_roof_and_gable(p_start, sizex, sizez, false, materials, 0, 2, vm ); end local do_ceiling = ( math.min( sizex, sizez )>4 ); -- floor and ceiling for dx = p.x-sizex+2, p.x-2 do for dz = p.z-sizez+2, p.z-2 do for i,height in ipairs( floor_height ) do vm:set_node_at( {x=dx,y=height,z=dz},floor_materials[i]); end end end -- index 1 and 2 are offsets in any of the walls; index 3 indicates if the -- windows start at odd indices or not local reserved_places = {s1[1], s1[2], s2[1], s2[2], s1[3], s2[3]}; p_start = {x=p.x-sizex, y=p.y, z=p.z-sizez}; local wall_with_ladder = simple_houses.place_ladder( p_start, sizex, sizez, reserved_places, #materials.window_at_height-1, materials.flat_roof, vm); simple_houses.place_door( p_start, sizex, sizez, reserved_places, wall_with_ladder, floor_height, vm ); simple_houses.place_chest( p_start, sizex, sizez, reserved_places, wall_with_ladder, floor_height, vm, materials ); vm:write_to_map(true); -- return where the hut has been placed return {p1={x=p.x - sizex, y=p.y, z=p.z - sizez }, p2=p}; end simple_houses.simple_hut_get_size_and_place = function( heightmap, minp, maxp) if( minp.y < -64 or minp.y > 500 or not(heightmap)) then return; end -- halfway reasonable house sizes local maxsize = 13; if( math.random(1,5)==1) then maxsize = 17; end -- TODO: if more than 2-3 houses are placed, get voxelmanip for entire area instead of for each house -- TODO: avoid overlapping with mg_villages if that one is installed local sizex = math.random(8,maxsize); local sizez = math.max( 8, math.min( maxsize, math.random( math.floor(sizex/4), sizex*2 ))); -- chooses random materials and a random place without destroying the landscape -- minheight 2: one above water level; avoid below water level and places on ice return simple_houses.simple_hut_find_place( heightmap, minp, maxp, sizex, sizez, 2, 1000 ); end minetest.register_on_generated(function(minp, maxp, seed) if( minp.y < -64 or minp.y > 500) then return; end simple_houses.mapchunks_processed = simple_houses.mapchunks_processed + 1; -- with each map chunk generated, there's more room where houses could be local missing = (simple_houses.mapchunks_processed * simple_houses.houses_wanted_per_mapchunk) - simple_houses.houses_generated; -- some randomness to make it more intresting if( missing < simple_houses.max_per_mapchunk and math.random(1,10)>1) then return; end local heightmap = minetest.get_mapgen_object('heightmap'); local houses_placed = 0; local house_data = {}; for i=1,simple_houses.max_per_mapchunk do local res = simple_houses.simple_hut_get_size_and_place( heightmap, minp, maxp); if( res and res.p1 and res.p2 ) then handle_schematics.mark_flat_land_as_used(heightmap, minp, maxp, res.p2.i, (res.p2.x-res.p1.x), (res.p2.z-res.p1.z)); table.insert( house_data, res ); houses_placed = houses_placed + 1; end end for i,data in ipairs( house_data ) do local res = simple_houses.simple_hut_get_materials( data, #house_data, maxp.y+16 ); simple_houses.simple_hut_place_hut( data, res.materials, heightmap ); end if( houses_placed > 0 ) then simple_houses.houses_generated = simple_houses.houses_generated + houses_placed; -- print("Count: "..tostring( simple_houses.mapchunks_processed ).. -- " Houses: "..tostring( simple_houses.houses_generated )); end end);