mirror of
https://git.minetest.land/VoxeLibre/VoxeLibre.git
synced 2024-11-17 08:31:08 +01:00
415 lines
16 KiB
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
|