clean up villages code, add biome farming support

This commit is contained in:
kno10 2024-09-03 17:16:36 +02:00
parent 92cffe42b4
commit eaaa2a4dd2
6 changed files with 313 additions and 582 deletions

View file

@ -6,39 +6,28 @@ mcl_villages.schematic_wells = {}
mcl_villages.on_village_placed = {} mcl_villages.on_village_placed = {}
mcl_villages.on_villager_placed = {} mcl_villages.on_villager_placed = {}
mcl_villages.mandatory_buildings = {} mcl_villages.mandatory_buildings = {}
mcl_villages.forced_blocks = {}
local S = minetest.get_translator(minetest.get_current_modname()) local S = minetest.get_translator(minetest.get_current_modname())
local function job_count(schem_lua) local function job_count(schem_lua)
-- Local copy so we don't trash the schema for other uses, because apparently
-- there isn't a non-destructive way to count occurrences of a string :(
local str = schem_lua
local count = 0 local count = 0
for _, n in pairs(mobs_mc.jobsites) do for _, n in pairs(mobs_mc.jobsites) do
if string.find(n, "^group:") then if string.find(n, "^group:") then
if n == "group:cauldron" then if n == "group:cauldron" then
count = count + select(2, string.gsub(str, '"mcl_cauldrons:cauldron', "")) count = count + select(2, string.gsub(schem_lua, '"mcl_cauldrons:cauldron', ""))
else else
local name = string.sub(n, 6, -1) local name = string.sub(n, 6, -1)
local num = select(2, string.gsub(str, name, "")) local num = select(2, string.gsub(schem_lua, name, ""))
if num then if num then
minetest.log( minetest.log("info", string.format("[mcl_villages] Guessing how to handle %s counting it as %d job sites", name, num))
"info",
string.format("[mcl_villages] Guessing how to handle %s counting it as %d job sites", name, num)
)
count = count + num count = count + num
else else
minetest.log( minetest.log("warning", string.format("[mcl_villages] Don't know how to handle group %s counting it as 1 job site", n))
"warning",
string.format("[mcl_villages] Don't know how to handle group %s counting it as 1 job site", n)
)
count = count + 1 count = count + 1
end end
end end
else else
count = count + select(2, string.gsub(str, '{name="' .. n .. '"', "")) count = count + select(2, string.gsub(schem_lua, '{name="' .. n .. '"', ""))
end end
end end
@ -62,18 +51,13 @@ local all_optional = { "yadjust", "no_ground_turnip", "no_clearance" }
local function set_all_optional(record, data) local function set_all_optional(record, data)
for _, field in ipairs(all_optional) do for _, field in ipairs(all_optional) do
if record[field] then if record[field] then data[field] = record[field] end
data[field] = record[field]
end
end end
end end
local function set_mandatory(record, type) local function set_mandatory(record, type)
if record['is_mandatory'] then if record['is_mandatory'] then
if not mcl_villages.mandatory_buildings[type] then if not mcl_villages.mandatory_buildings[type] then mcl_villages.mandatory_buildings[type] = {} end
mcl_villages.mandatory_buildings[type] = {}
end
table.insert(mcl_villages.mandatory_buildings[type], record["name"]) table.insert(mcl_villages.mandatory_buildings[type], record["name"])
end end
end end
@ -105,23 +89,15 @@ function mcl_villages.register_building(record)
local data = load_schema(record["name"], record["mts"]) local data = load_schema(record["name"], record["mts"])
set_all_optional(record, data) set_all_optional(record, data)
for _, field in ipairs(optional_fields) do for _, field in ipairs(optional_fields) do
if record[field] then if record[field] then data[field] = record[field] end
data[field] = record[field]
end
end end
-- Local copy so we don't trash the schema for other uses
local str = data["schem_lua"] local str = data["schem_lua"]
local num_beds = select(2, string.gsub(str, '"mcl_beds:bed_[^"]+_bottom"', "")) local num_beds = select(2, string.gsub(str, '"mcl_beds:bed_[^"]+_bottom"', ""))
if num_beds > 0 then data["num_beds"] = num_beds end
if num_beds > 0 then
data["num_beds"] = num_beds
end
local job_count = job_count(data["schem_lua"]) local job_count = job_count(data["schem_lua"])
if job_count > 0 then if job_count > 0 then
data["num_jobs"] = job_count data["num_jobs"] = job_count
table.insert(mcl_villages.schematic_jobs, data) table.insert(mcl_villages.schematic_jobs, data)
@ -132,102 +108,42 @@ function mcl_villages.register_building(record)
end end
end end
local supported_crop_types = {
"grain",
"root",
"gourd",
"bush",
"tree",
"flower",
}
local crop_list = {} local crop_list = {}
function mcl_villages.register_crop(crop_def)
function mcl_villages.default_crop() local crops = crop_list[crop_def.type] or {}
return "mcl_farming:wheat_1" for biome, weight in pairs(crop_def.biomes) do
if crops[biome] == nil then crops[biome] = {} end
crops[biome][crop_def.node] = weight
end end
crop_list[crop_def.type] = crops
local weighted_crops = {}
local function adjust_weights(biome, crop_type)
if weighted_crops[biome] == nil then
weighted_crops[biome] = {}
end
weighted_crops[biome][crop_type] = {}
local factor = 100 / crop_list[biome][crop_type]["total_weight"]
local total = 0
for node, weight in pairs(crop_list[biome][crop_type]) do
if node ~= "total_weight" then
total = total + (math.round(weight * factor))
table.insert(weighted_crops[biome][crop_type], { total = total, node = node })
end
end
table.sort(weighted_crops[biome][crop_type], function(a, b)
return a.total < b.total
end)
end end
function mcl_villages.get_crop_types() function mcl_villages.get_crop_types()
return table.copy(supported_crop_types) local ret = {}
for k, _ in pairs(crop_list) do
table.insert(ret, k)
end end
return ret
function mcl_villages.get_crops()
return table.copy(crop_list)
end end
function mcl_villages.get_weighted_crop(biome, crop_type, pr) function mcl_villages.get_weighted_crop(biome, crop_type, pr)
if weighted_crops[biome] == nil then local crops = crop_list[crop_type]
biome = "plains" if not crops then return end -- unknown crop
end local crops = crops[biome] or crops["plains"]
if weighted_crops[biome][crop_type] == nil then local total = 0
return for _, weight in pairs(crops) do total = total + weight end
end
local rand = pr:next(1, 99)
for i, rec in ipairs(weighted_crops[biome][crop_type]) do
local weight = rec.total
local node = rec.node
local rand = pr:next(0, 1e7) * 1e-7 * total
for node, weight in pairs(crops) do
if rand <= weight then if rand <= weight then
return node return node
end end
rand = rand - weight
end end
return return
end end
function mcl_villages.register_crop(crop_def)
local node = crop_def.node
local crop_type = crop_def.type
if table.indexof(supported_crop_types, crop_type) == -1 then
minetest.log("warning", S("Crop type @1 is not supported", crop_type))
return
end
for biome, weight in pairs(crop_def.biomes) do
if crop_list[biome] == nil then
crop_list[biome] = {}
end
if crop_list[biome][crop_type] == nil then
crop_list[biome][crop_type] = { total_weight = 0 }
end
crop_list[biome][crop_type][node] = weight
crop_list[biome][crop_type]["total_weight"] = crop_list[biome][crop_type]["total_weight"] + weight
adjust_weights(biome, crop_type)
end
end
function mcl_villages.register_on_village_placed(func) function mcl_villages.register_on_village_placed(func)
table.insert(mcl_villages.on_village_placed, func) table.insert(mcl_villages.on_village_placed, func)
end end
@ -235,3 +151,4 @@ end
function mcl_villages.register_on_villager_spawned(func) function mcl_villages.register_on_villager_spawned(func)
table.insert(mcl_villages.on_villager_placed, func) table.insert(mcl_villages.on_villager_placed, func)
end end

View file

@ -2,59 +2,8 @@ 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 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 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()) 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) local function add_building(settlement, building, count_buildings)
table.insert(settlement, building) table.insert(settlement, building)
count_buildings[building.name] = (count_buildings[building.name] or 0) + 1 count_buildings[building.name] = (count_buildings[building.name] or 0) + 1
@ -138,7 +87,7 @@ function mcl_villages.create_site_plan(vm, minp, maxp, pr)
local settlement = {} local settlement = {}
-- initialize all settlement_info table -- initialize all settlement_info table
local count_buildings = mcl_villages.initialize_settlement_info(pr) local count_buildings = { num_jobs = 0, num_beds = 0, target_jobs = pr:next(min_jobs, max_jobs) }
-- first building is townhall in the center -- first building is townhall in the center
local bindex = pr:next(1, #mcl_villages.schematic_bells) local bindex = pr:next(1, #mcl_villages.schematic_bells)
@ -151,13 +100,13 @@ function mcl_villages.create_site_plan(vm, minp, maxp, pr)
end end
end end
while count_buildings.num_jobs < count_buildings.number_of_jobs do while count_buildings.num_jobs < count_buildings.target_jobs do
local rindex = pr:next(1, #mcl_villages.schematic_jobs) local rindex = pr:next(1, #mcl_villages.schematic_jobs)
local building_info = mcl_villages.schematic_jobs[rindex] local building_info = mcl_villages.schematic_jobs[rindex]
if if
(building_info.min_jobs == nil or count_buildings.number_of_jobs >= building_info.min_jobs) (building_info.min_jobs == nil or count_buildings.target_jobs >= building_info.min_jobs)
and (building_info.max_jobs == nil or count_buildings.number_of_jobs <= building_info.max_jobs) and (building_info.max_jobs == nil or count_buildings.target_jobs <= building_info.max_jobs)
and ( and (
building_info.num_others == nil building_info.num_others == nil
or (count_buildings[building_info.group or building_info.name] or 0) == 0 or (count_buildings[building_info.group or building_info.name] or 0) == 0
@ -180,8 +129,8 @@ function mcl_villages.create_site_plan(vm, minp, maxp, pr)
local building_info = mcl_villages.schematic_houses[rindex] local building_info = mcl_villages.schematic_houses[rindex]
if if
(building_info.min_jobs == nil or count_buildings.number_of_jobs >= building_info.min_jobs) (building_info.min_jobs == nil or count_buildings.target_jobs >= building_info.min_jobs)
and (building_info.max_jobs == nil or count_buildings.number_of_jobs <= building_info.max_jobs) and (building_info.max_jobs == nil or count_buildings.target_jobs <= building_info.max_jobs)
and ( and (
building_info.num_others == nil building_info.num_others == nil
or (count_buildings[building_info.group or building_info.name] or 0) == 0 or (count_buildings[building_info.group or building_info.name] or 0) == 0
@ -212,12 +161,32 @@ function mcl_villages.create_site_plan(vm, minp, maxp, pr)
return layout_town(vm, minp, maxp, pr, settlement) return layout_town(vm, minp, maxp, pr, settlement)
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",
-- jobsite: "mcl_smoker:smoker",
-- jobsite: "mcl_barrels:barrel_closed",
-- jobsite: "mcl_blast_furnace:blast_furnace",
-- jobsite: "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" })
for _, n in pairs(nodes) do mcl_villages.fill_chest(n, pr) end
end
-- important: the vm will be written and then is outdated!
function mcl_villages.place_schematics(vm, settlement, blockseed, pr) function mcl_villages.place_schematics(vm, settlement, blockseed, pr)
-- local vm = VoxelManip() -- first building is always the bell
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_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 for i, building in ipairs(settlement) do
local minp, cpos, maxp, size, rotation = building.minp, building.pos, building.maxp, building.size, building.rotation local minp, cpos, maxp, size, rotation = building.minp, building.pos, building.maxp, building.size, building.rotation
@ -232,87 +201,60 @@ function mcl_villages.place_schematics(vm, settlement, blockseed, pr)
local schematic = loadstring(schem_lua)() local schematic = loadstring(schem_lua)()
-- the foundation and air space for the building was already built before -- 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.log("debug", "placing schematics for "..building.name.." at "..minetest.pos_to_string(minp).." on "..surface_material)
minetest.place_schematic_on_vmanip( minetest.place_schematic_on_vmanip(vm, minp, schematic, rotation, nil, true, { place_center_x = false, place_center_y = false, place_center_z = false })
vm,
minp,
schematic,
rotation,
{ ["mcl_core:dirt_with_grass"]=schematic.surface_mat or "mcl_core:dirt" },
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) mcl_villages.store_path_ends(vm, minp, maxp, cpos, blockseed, bell_pos)
mcl_villages.increase_no_paths(vm, minp, maxp) -- help the path finder
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 end
vm:write_to_map(true) -- for path finder and light vm:write_to_map(true) -- for path finder and light
local biome_data = minetest.get_biome_data(bell_pos) -- Path planning and placement
local biome_name = minetest.get_biome_name(biome_data.biome) mcl_villages.paths(blockseed, minetest.get_biome_name(minetest.get_biome_data(bell_pos).biome))
mcl_villages.paths(blockseed, biome_name) -- Clean up paths and initialize nodes
for i, building in ipairs(settlement) do for i, building in ipairs(settlement) do
mcl_villages.clean_no_paths(building.minp, building.maxp)
init_nodes(building.minp, building.maxp, pr) init_nodes(building.minp, building.maxp, pr)
end end
-- this will run delayed actions, such as spawning mobs -- Replace center block with a temporary block, which will be used run delayed actions
minetest.set_node(bell_center_pos, { name = "mcl_villages:village_block" }) local block_name = minetest.get_node(bell_pos).name -- to restore the node afterwards
local meta = minetest.get_meta(bell_center_pos) minetest.swap_node(bell_pos, { name = "mcl_villages:village_block" })
local meta = minetest.get_meta(bell_pos)
meta:set_string("node_type", block_name)
meta:set_string("blockseed", blockseed) meta:set_string("blockseed", blockseed)
meta:set_string("node_type", bell_center_node_type) meta:set_string("infotext", S("The timer for this village has not run yet!"))
meta:set_string("infotext", S("The timer for this @1 has not run yet!", bell_center_node_type)) minetest.get_node_timer(bell_pos):start(1.0)
minetest.get_node_timer(bell_center_pos):start(1.0)
-- read back any changes (fixme: would be better if we would not need this often.
--local emin, emax = vm:get_emerged_area()
--vm:read_from_map(emin, emax)
end end
minetest.register_node("mcl_villages:village_block", {
drawtype = "glasslike",
groups = { not_in_creative_inventory = 1 },
light_source = 14, -- This is a light source so that lamps don't get placed near it
-- Somethings don't work reliably when done in the map building
-- so we use a timer to run them later when they work more reliably
-- e.g. spawning mobs, running minetest.find_path
on_timer = function(pos, _)
local meta = minetest.get_meta(pos)
minetest.swap_node(pos, { name = meta:get_string("node_type") })
mcl_villages.post_process_village(meta:get_string("blockseed"))
return false
end,
})
function mcl_villages.post_process_village(blockseed) function mcl_villages.post_process_village(blockseed)
local village_info = mcl_villages.get_village(blockseed) local village_info = mcl_villages.get_village(blockseed)
if not village_info then if not village_info then return end
return
end
-- minetest.log("Postprocessing village") -- minetest.log("Postprocessing village")
local settlement_info = village_info.data local settlement_info = village_info.data
local jobs = {} local jobs, beds = {}, {}
local beds = {}
local bell_pos = vector.copy(settlement_info[1]["pos"]) local bell_pos = vector.copy(settlement_info[1].pos)
local bell = vector.offset(bell_pos, 0, 2, 0) local bell = vector.offset(bell_pos, 0, 1, 0)
local biome_data = minetest.get_biome_data(bell_pos) local biome_name = minetest.get_biome_name(minetest.get_biome_data(bell_pos).biome)
local biome_name = minetest.get_biome_name(biome_data.biome)
--mcl_villages.paths(blockseed, biome_name)
-- Spawn Golem
local l = minetest.add_entity(bell, "mobs_mc:iron_golem"):get_luaentity() local l = minetest.add_entity(bell, "mobs_mc:iron_golem"):get_luaentity()
if l then if l then
l._home = bell l._home = bell
@ -320,45 +262,41 @@ function mcl_villages.post_process_village(blockseed)
minetest.log("info", "Could not create a golem!") minetest.log("info", "Could not create a golem!")
end end
spawn_cats(bell) -- Spawn cats
local sp = minetest.find_nodes_in_area_under_air(vector.offset(bell, -20, -10, -20),vector.offset(bell, 20, 10, 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 = bell_pos -- help them stay local
else
minetest.log("info", "Could not spawn a cat")
break
end
end
-- collect beds and job sites
for _, building in pairs(settlement_info) do for _, building in pairs(settlement_info) do
local has_beds = building["num_beds"] and building["num_beds"] ~= nil local minp, maxp = building.minp, building.maxp
local has_jobs = building["num_jobs"] and building["num_jobs"] ~= nil if building.num_jobs then
local minp, maxp = building["minp"], building["maxp"]
if has_jobs then
local jobsites = minetest.find_nodes_in_area(minp, maxp, mobs_mc.jobsites) 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
for _, job_pos in pairs(jobsites) do
table.insert(jobs, job_pos)
end
end end
if has_beds then if building.num_beds then
local bld_beds = minetest.find_nodes_in_area(minp, maxp, { "group:bed" }) local bld_beds = minetest.find_nodes_in_area(minp, maxp, { "group:bed" })
for _, bed_pos in pairs(bld_beds) do for _, bed_pos in pairs(bld_beds) do
local bed_node = minetest.get_node(bed_pos) local bed_group = minetest.get_item_group(minetest.get_node(bed_pos).name, "bed")
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
-- 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 end
end end
-- TODO: shuffle jobs?
-- minetest.log("beds: "..#beds.." jobsites: "..#jobs) -- minetest.log("beds: "..#beds.." jobsites: "..#jobs)
if beds then if beds then
for _, bed_pos in pairs(beds) do for _, bed_pos in pairs(beds) do
local res = minetest.forceload_block(bed_pos, true) 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) local m = minetest.get_meta(bed_pos)
m:set_string("bell_pos", minetest.pos_to_string(bell_pos)) m:set_string("bell_pos", minetest.pos_to_string(bell_pos))
if m:get_string("villager") == "" then if m:get_string("villager") == "" then
@ -371,18 +309,13 @@ function mcl_villages.post_process_village(blockseed)
m:set_string("infotext", S("A villager sleeps here")) m:set_string("infotext", S("A villager sleeps here"))
local job_pos = table.remove(jobs, 1) local job_pos = table.remove(jobs, 1)
if job_pos then if job_pos then villager_employ(l, job_pos) end -- HACK: merge more MCLA villager job code?
villager_employ(l, job_pos) -- HACK: merge more MCLA villager code for _, callback in pairs(mcl_villages.on_villager_placed) do callback(v, blockseed) end
end
for _, callback in pairs(mcl_villages.on_villager_placed) do
callback(v, blockseed)
end
else else
minetest.log("info", "Could not create a villager!") minetest.log("info", "Could not create a villager!")
end end
else else
minetest.log("info", "bed already owned by " .. m:get_string("villager")) minetest.log("info", "bed already owned by " .. m:get_string("villager")) -- should not happen unless villages overlap
end end
end end
end end
@ -395,7 +328,8 @@ function mcl_villages.terraform(vm, settlement, pr)
for i, building in ipairs(settlement) do for i, building in ipairs(settlement) do
if not building.no_clearance then if not building.no_clearance then
local pos, size = building.pos, building.size local pos, size = building.pos, building.size
pos = vector.offset(pos, -math.floor(size.x/2), 0, -math.floor(size.z/2)) pos = vector.offset(pos, -math.floor((size.x-1)/2), 0, -math.floor((size.z-1)/2))
-- TODO: allow different clearance for different buildings?
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) 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
end end
@ -403,8 +337,8 @@ function mcl_villages.terraform(vm, settlement, pr)
if not building.no_ground_turnip then if not building.no_ground_turnip then
local pos, size = building.pos, building.size local pos, size = building.pos, building.size
local surface_mat = building.surface_mat local surface_mat = building.surface_mat
local platform_mat = building.platform_mat or { name = foundation_materials[surface_mat.name] or "mcl_core:dirt" } local platform_mat = building.platform_mat or { name = mcl_villages.foundation_materials[surface_mat.name] or "mcl_core:dirt" }
local stone_mat = building.stone_mat or { name = "mcl_core:stone" } local stone_mat = building.stone_mat or { name = mcl_villages.stone_materials[surface_mat.name] or "mcl_core:stone" }
local dust_mat = building.dust_mat local dust_mat = building.dust_mat
building.platform_mat = platform_mat -- remember for use in schematic placement building.platform_mat = platform_mat -- remember for use in schematic placement
building.stone_mat = stone_mat building.stone_mat = stone_mat

View file

@ -1,7 +1,8 @@
-- switch for debugging -- maximum allowed difference in height for building a settlement
function mcl_villages.debug(message) mcl_villages.max_height_difference = 56
minetest.log("verbose", "[mcl_villages] "..message)
end -- legacy type in old schematics
minetest.register_alias("mcl_villages:stonebrickcarved", "mcl_core:stonebrickcarved")
-- possible surfaces where buildings can be built -- possible surfaces where buildings can be built
mcl_villages.surface_mat = {} mcl_villages.surface_mat = {}
@ -30,10 +31,15 @@ mcl_villages.surface_mat["mcl_colorblocks:hardened_clay_orange"] = true
mcl_villages.surface_mat["mcl_colorblocks:hardened_clay_red"] = true mcl_villages.surface_mat["mcl_colorblocks:hardened_clay_red"] = true
mcl_villages.surface_mat["mcl_colorblocks:hardened_clay_white"] = true mcl_villages.surface_mat["mcl_colorblocks:hardened_clay_white"] = true
-- maximum allowed difference in height for building a settlement -- substitute foundation materials
mcl_villages.max_height_difference = 56 mcl_villages.foundation_materials = {}
mcl_villages.foundation_materials["mcl_core:sand"] = "mcl_core:sandstone"
mcl_villages.foundation_materials["mcl_core:redsand"] = "mcl_core:redsandstone"
mcl_villages.half_map_chunk_size = 40 -- substitute stone materials in foundation
mcl_villages.stone_materials = {}
mcl_villages.default_crop = "mcl_farming:wheat_1"
-- --
-- Biome based block substitutions -- Biome based block substitutions
@ -119,16 +125,6 @@ mcl_villages.vl_to_mcla = {
{ '"mcl_bamboo:bamboo_door', '"mcl_doors:door_bamboo'}, { '"mcl_bamboo:bamboo_door', '"mcl_doors:door_bamboo'},
} }
mcl_villages.mcla_to_vl = { mcl_villages.mcla_to_vl = {
-- oneway
--{ '"mcl_villages:no_paths"', '"air"'}, -- TODO: support these
--{ '"mcl_villages:path_endpoint"', '"air"'}, -- TODO: support these
{ '"mcl_villages:crop_root', '"mcl_farming:potato'}, -- TODO: support biome specific farming
{ '"mcl_villages:crop_grain', '"mcl_farming:wheat'}, -- TODO: support biome specific farming
{ '"mcl_villages:crop_gourd', '"mcl_farming:pumpkin'}, -- TODO: support biome specific farming
{ '"mcl_villages:crop_flower_0"', '"mcl_flowers:tulip_red"'}, -- TODO: support biome specific farming
{ '"mcl_villages:crop_flower_1"', '"mcl_flowers:tulip_orange"'}, -- TODO: support biome specific farming
{ '"mcl_villages:crop_flower_2"', '"mcl_flowers:tulip_pink"'}, -- TODO: support biome specific farming
{ '"mcl_villages:crop_flower_3"', '"mcl_flowers:tulip_white"'}, -- TODO: support biome specific farming
-- bidirectional -- bidirectional
{ '"mcl_trees:tree_oak"', '"mcl_core:tree"'}, { '"mcl_trees:tree_oak"', '"mcl_core:tree"'},
{ '"mcl_trees:tree_dark_oak"', '"mcl_core:darktree"'}, { '"mcl_trees:tree_dark_oak"', '"mcl_core:darktree"'},

View file

@ -11,37 +11,23 @@ dofile(mcl_villages.modpath.."/api.lua")
local S = minetest.get_translator(minetest.get_current_modname()) local S = minetest.get_translator(minetest.get_current_modname())
minetest.register_alias("mcl_villages:stonebrickcarved", "mcl_core:stonebrickcarved") local function ecb_village(blockpos, action, calls_remaining, param)
minetest.register_node("mcl_villages:structblock", {drawtype="airlike",groups = {not_in_creative_inventory=1},}) if calls_remaining >= 1 then return end
-- we currently do not support/use these from MCLA: if mcl_villages.village_exists(param.blockseed) then return end
--minetest.register_alias("mcl_villages:village_block", "air") local pr = PcgRandom(param.blockseed)
--minetest.register_alias("mcl_villages:building_block", "air") local vm = VoxelManip(param.minp, param.maxp)
-- local settlement = mcl_villages.create_site_plan(vm, param.minp, param.maxp, pr)
-- on map generation, try to build a settlement
--
local function build_a_settlement(minp, maxp, blockseed)
if mcl_villages.village_exists(blockseed) then return end
local pr = PcgRandom(blockseed)
local vm = VoxelManip(minp, maxp)
local settlement = mcl_villages.create_site_plan(vm, minp, maxp, pr)
if not settlement then return false, false end if not settlement then return false, false end
-- all foundations first, then all buildings, to avoid damaging very close buildings -- all foundations first, then all buildings, to avoid damaging very close buildings
mcl_villages.terraform(vm, settlement, pr) mcl_villages.terraform(vm, settlement, pr)
mcl_villages.place_schematics(vm, settlement, blockseed, pr) mcl_villages.place_schematics(vm, settlement, param.blockseed, pr)
mcl_villages.add_village(blockseed, settlement) mcl_villages.add_village(param.blockseed, settlement)
--lvm:write_to_map(true) -- destory paths as of now --lvm:write_to_map(true) -- destorys paths as of now, as they are placed afterwards
--mcl_villages.paths(blockseed) -- TODO: biome
for _, on_village_placed_callback in pairs(mcl_villages.on_village_placed) do for _, on_village_placed_callback in pairs(mcl_villages.on_village_placed) do
on_village_placed_callback(settlement, blockseed) on_village_placed_callback(settlement, param.blockseed)
end end
end end
local function ecb_village(blockpos, action, calls_remaining, param)
if calls_remaining >= 1 then return end
local minp, maxp, blockseed = param.minp, param.maxp, param.blockseed
build_a_settlement(minp, maxp, blockseed)
end
-- Disable natural generation in singlenode. -- Disable natural generation in singlenode.
local mg_name = minetest.get_mapgen_setting("mg_name") local mg_name = minetest.get_mapgen_setting("mg_name")
if mg_name ~= "singlenode" then if mg_name ~= "singlenode" then
@ -50,85 +36,22 @@ if mg_name ~= "singlenode" then
if village_boost == 0 then return end if village_boost == 0 then return end
local pr = PcgRandom(blockseed) local pr = PcgRandom(blockseed)
if pr:next(0,1e9) * 100e-9 >= village_boost then return end if pr:next(0,1e9) * 100e-9 >= village_boost then return end
local big_minp = vector.copy(minp) --vector.offset(minp, -16, -16, -16) if village_boost < 25 then -- otherwise, this tends to transitively emerge too much
local big_maxp = vector.copy(maxp) --vector.offset(maxp, 16, 16, 16) minp, maxp = vector.offset(minp, -16, 0, -16), vector.offset(maxp, 16, 0, 16)
minetest.emerge_area(big_minp, big_maxp, ecb_village, end
{ minp = vector.copy(minp), maxp = vector.copy(maxp), blockseed = blockseed } minetest.emerge_area(minp, maxp, ecb_village, { minp = minp, maxp = maxp, blockseed = blockseed })
)
end) end)
--[[ did not work, because later structure generation would make holes in our schematics
mcl_mapgen_core.register_generator("villages", function(lvm, data, data2, e1, e2, area, minp, maxp, blockseed)
if mcl_villages.village_exists(blockseed) then return false, false end
lvm:set_data(data) -- FIXME: ugly hack, better directly manipulate the data array
lvm:set_param2_data(data2)
local pr = PcgRandom(blockseed)
if pr:next(0,1e9) * 100e-9 > village_boost then return end
local settlement = mcl_villages.create_site_plan(lvm, minp, maxp, pr)
if not settlement then return false, false end
-- all foundations first, then all buildings, to avoid damaging very close buildings
mcl_villages.terraform(lvm, settlement, pr)
mcl_villages.place_schematics(lvm, settlement, pr)
-- TODO: replace with MCLA code: mcl_villages.paths(settlement)
mcl_villages.add_village(blockseed, settlement)
lvm:get_data(data) -- FIXME: ugly hack, better directly manipulate the data array
lvm:get_param2_data(data2)
return true, true
end, function(minp, maxp, blockseed)
for _, on_village_placed_callback in pairs(mcl_villages.on_village_placed) do
on_village_placed_callback(settlement, blockseed)
end
end, 15000)
]]--
--[[ causes issues when vertically close to the chunk boundary:
mcl_mapgen_core.register_generator("villages", nil, function(minp, maxp, blockseed)
if maxp.y < 0 or mcl_villages.village_exists(blockseed) then return end
local pr = PcgRandom(blockseed)
if pr:next(0,1e9) * 10ee-9 > village_boost then return end
--local lvm, emin, emax = minetest.get_mapgen_object("voxelmanip") -- did not get the lighting fixed?
local lvm = VoxelManip()
lvm:read_from_map(minp, maxp)
local settlement = mcl_villages.create_site_plan(lvm, minp, maxp, pr)
if not settlement then return false, false end
-- all foundations first, then all buildings, to avoid damaging very close buildings
mcl_villages.terraform(lvm, settlement, pr)
mcl_villages.place_schematics(lvm, settlement, blockseed, pr)
mcl_villages.add_village(blockseed, settlement)
--lvm:write_to_map(true)
--mcl_villages.paths(blockseed) -- TODO: biome
end, 15000)
]]--
end end
-- This is a light source so that lamps don't get placed near it -- Handle legacy structblocks that are not fully emerged yet.
minetest.register_node("mcl_villages:village_block", { minetest.register_node("mcl_villages:structblock", {drawtype="airlike",groups = {not_in_creative_inventory=1}})
drawtype = "airlike",
groups = { not_in_creative_inventory = 1 },
light_source = 14,
-- Somethings don't work reliably when done in the map building
-- so we use a timer to run them later when they work more reliably
-- e.g. spawning mobs, running minetest.find_path
on_timer = function(pos, _)
local meta = minetest.get_meta(pos)
local blockseed = meta:get_string("blockseed")
local node_type = meta:get_string("node_type")
minetest.set_node(pos, { name = node_type })
mcl_villages.post_process_village(blockseed)
return false
end,
})
-- struct to cause a village spawn when it can be fully emerged
minetest.register_lbm({ minetest.register_lbm({
name = "mcl_villages:structblock", name = "mcl_villages:structblock",
run_at_every_load = true, run_at_every_load = true,
nodenames = {"mcl_villages:structblock"}, nodenames = {"mcl_villages:structblock"},
action = function(pos, node) action = function(pos, node)
minetest.set_node(pos, {name = "air"}) minetest.set_node(pos, {name = "air"})
local minp=vector.offset(pos, -40, -40, -40) local minp, maxp = vector.offset(pos, -40, -40, -40), vector.offset(pos, 40, 40, 40)
local maxp=vector.offset(pos, 40, 40, 40)
local blockseed = PcgRandom(minetest.hash_node_position(pos)):next() local blockseed = PcgRandom(minetest.hash_node_position(pos)):next()
minetest.emerge_area(minp, maxp, ecb_village, { minp = minp, maxp = maxp, blockseed = blockseed}) minetest.emerge_area(minp, maxp, ecb_village, { minp = minp, maxp = maxp, blockseed = blockseed})
end end
@ -146,24 +69,21 @@ if minetest.is_creative_enabled("") then
minetest.chat_send_player(placer:get_player_name(), S("Placement denied. You need the “server” privilege to place villages.")) minetest.chat_send_player(placer:get_player_name(), S("Placement denied. You need the “server” privilege to place villages."))
return return
end end
local minp = vector.subtract(pointed_thing.under, mcl_villages.half_map_chunk_size) local pos = pointed_thing.under
local maxp = vector.add(pointed_thing.under, mcl_villages.half_map_chunk_size) local minp, maxp = vector.offset(pos, -40, -40, -40), vector.offset(pos, 40, 40, 40)
build_a_settlement(minp, maxp, math.random(0,32767)) local blockseed = PcgRandom(minetest.hash_node_position(pos)):next()
minetest.emerge_area(minp, maxp, ecb_village, { minp = minp, maxp = maxp, blockseed = blockseed })
end end
}) })
mcl_wip.register_experimental_item("mcl_villages:tool") mcl_wip.register_experimental_item("mcl_villages:tool")
end end
-- This makes the temporary node invisble unless in creative mode -- This makes the temporary node invisble unless in creative mode
local drawtype = "airlike" local drawtype = minetest.is_creative_enabled("") and "glasslike" or "airlike"
if minetest.is_creative_enabled("") then
drawtype = "glasslike"
end
-- Special node for schematics editing: no path on this place
minetest.register_node("mcl_villages:no_paths", { minetest.register_node("mcl_villages:no_paths", {
description = S( description = S("Prevent paths from being placed during villager generation. Replaced by air after village path generation"),
"Prevent paths from being placed during villager generation. Replaced by air after village path generation"
),
paramtype = "light", paramtype = "light",
drawtype = drawtype, drawtype = drawtype,
inventory_image = "mcl_core_barrier.png", inventory_image = "mcl_core_barrier.png",
@ -173,6 +93,7 @@ minetest.register_node("mcl_villages:no_paths", {
groups = { creative_breakable = 1, not_solid = 1, not_in_creative_inventory = 1 }, groups = { creative_breakable = 1, not_solid = 1, not_in_creative_inventory = 1 },
}) })
-- Special node for schematics editing: path endpoint
minetest.register_node("mcl_villages:path_endpoint", { minetest.register_node("mcl_villages:path_endpoint", {
description = S("Mark the node as a good place for paths to connect to"), description = S("Mark the node as a good place for paths to connect to"),
is_ground_content = false, is_ground_content = false,
@ -184,12 +105,7 @@ minetest.register_node("mcl_villages:path_endpoint", {
paramtype = "light", paramtype = "light",
sunlight_propagates = true, sunlight_propagates = true,
drawtype = "nodebox", drawtype = "nodebox",
node_box = { node_box = { type = "fixed", fixed = { { -0.5, -0.5, -0.5, 0.5, -0.45, 0.5 } } },
type = "fixed",
fixed = {
{ -8 / 16, -8 / 16, -8 / 16, 8 / 16, -7 / 16, 8 / 16 },
},
},
_mcl_hardness = 0.1, _mcl_hardness = 0.1,
_mcl_blast_resistance = 0.1, _mcl_blast_resistance = 0.1,
}) })
@ -412,31 +328,6 @@ mcl_villages.register_building({
yadjust = 1, yadjust = 1,
}) })
for _, crop_type in pairs(mcl_villages.get_crop_types()) do
for count = 1, 8 do
local tile = crop_type .. "_" .. count .. ".png"
minetest.register_node("mcl_villages:crop_" .. crop_type .. "_" .. count, {
description = S("A place to plant @1 crops", crop_type),
is_ground_content = false,
tiles = { tile },
wield_image = tile,
wield_scale = { x = 1, y = 1, z = 0.5 },
groups = { handy = 1, supported_node = 1, not_in_creative_inventory = 1 },
paramtype = "light",
sunlight_propagates = true,
drawtype = "nodebox",
node_box = {
type = "fixed",
fixed = {
{ -8 / 16, -8 / 16, -8 / 16, 8 / 16, -7 / 16, 8 / 16 },
},
},
_mcl_hardness = 0.1,
_mcl_blast_resistance = 0.1,
})
end
end
mcl_villages.register_crop({ mcl_villages.register_crop({
type = "grain", type = "grain",
node = "mcl_farming:wheat_1", node = "mcl_farming:wheat_1",
@ -512,8 +403,9 @@ mcl_villages.register_crop({
}, },
}) })
-- TODO: make flowers biome-specific
for name, def in pairs(minetest.registered_nodes) do for name, def in pairs(minetest.registered_nodes) do
if def.groups["flower"] and not def.groups["double_plant"] and name ~= "mcl_flowers:wither_rose" then if def.groups.flower and not def.groups.double_plant and name ~= "mcl_flowers:wither_rose" then
mcl_villages.register_crop({ mcl_villages.register_crop({
type = "flower", type = "flower",
node = name, node = name,
@ -528,3 +420,25 @@ for name, def in pairs(minetest.registered_nodes) do
}) })
end end
end end
-- Crop placeholder nodes at different growth stages, for designing schematics
for _, crop_type in ipairs(mcl_villages.get_crop_types()) do
for count = 1, 8 do
local tile = crop_type .. "_" .. count .. ".png"
minetest.register_node("mcl_villages:crop_" .. crop_type .. "_" .. count, {
description = S("A place to plant @1 crops", crop_type),
is_ground_content = false,
tiles = { tile },
wield_image = tile,
wield_scale = { x = 1, y = 1, z = 0.5 },
groups = { handy = 1, supported_node = 1, not_in_creative_inventory = 1 },
paramtype = "light",
sunlight_propagates = true,
drawtype = "nodebox",
node_box = { type = "fixed", fixed = { { -0.5, -0.5, -0.5, 0.5, -0.45, 0.5 } } },
_mcl_hardness = 0.1,
_mcl_blast_resistance = 0.1,
})
end
end

View file

@ -1,7 +1,6 @@
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
-- generate paths between buildings -- generate paths between buildings
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
local light_threshold = tonumber(minetest.settings:get("mcl_villages_light_threshold")) or 5 local light_threshold = tonumber(minetest.settings:get("mcl_villages_light_threshold")) or 5
-- This ends up being a nested table. -- This ends up being a nested table.
@ -10,8 +9,40 @@ local light_threshold = tonumber(minetest.settings:get("mcl_villages_light_thres
-- 3rd is the pos of the end points -- 3rd is the pos of the end points
local path_ends = {} local path_ends = {}
-- note: not using LVM here, as this runs after the pathfinder
-- simple function to increase "no_paths" walls
function mcl_villages.clean_no_paths(minp, maxp)
local no_paths_nodes = minetest.find_nodes_in_area(minp, maxp, { "mcl_villages:no_paths" })
if #no_paths_nodes > 0 then
minetest.bulk_set_node(no_paths_nodes, { name = "air" })
end
end
-- this can still run in LVM
-- simple function to increase "no_paths" walls
function mcl_villages.increase_no_paths(vm, minp, maxp)
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
end
-- Insert end points in to the nested tables -- Insert end points in to the nested tables
function mcl_villages.store_path_ends(lvm, minp, maxp, pos, blockseed, bell_pos) function mcl_villages.store_path_ends(vm, minp, maxp, pos, blockseed, bell_pos)
-- We store by distance because we create paths far away from the bell first -- We store by distance because we create paths far away from the bell first
local dist = vector.distance(bell_pos, pos) local dist = vector.distance(bell_pos, pos)
local id = "block_" .. blockseed -- cannot use integers as keys local id = "block_" .. blockseed -- cannot use integers as keys
@ -21,20 +52,18 @@ function mcl_villages.store_path_ends(lvm, minp, maxp, pos, blockseed, bell_pos)
path_ends[id] = tab path_ends[id] = tab
end end
if tab[dist] == nil then tab[dist] = {} end if tab[dist] == nil then tab[dist] = {} end
-- TODO: improve, use LVM data instead of nodes -- TODO: use LVM data instead of nodes? But we only process a subset of the area
local v = vector.zero() local v = vector.zero()
local i = 0 for zi = minp.z, maxp.z do
for zi = minp.z-2, maxp.z+2 do
v.z = zi v.z = zi
for yi = minp.y-2, maxp.y+2 do for yi = minp.y, maxp.y do
v.y = yi v.y = yi
for xi = minp.x-2, maxp.x+2 do for xi = minp.x, maxp.x do
v.x = xi v.x = xi
local n = lvm:get_node_at(v) local n = vm:get_node_at(v)
if n and n.name == "mcl_villages:path_endpoint" then if n and n.name == "mcl_villages:path_endpoint" then
i = i + 1
table.insert(tab[dist], minetest.pos_to_string(v)) table.insert(tab[dist], minetest.pos_to_string(v))
lvm:set_node_at(v, { name = "air" }) vm:set_node_at(v, { name = "air" })
end end
end end
end end
@ -47,16 +76,14 @@ local function place_lamp(pos, pr)
local schem_lua = mcl_villages.substitute_materials(pos, schema.schem_lua, pr) local schem_lua = mcl_villages.substitute_materials(pos, schema.schem_lua, pr)
local schematic = loadstring(schem_lua)() local schematic = loadstring(schem_lua)()
minetest.place_schematic( minetest.place_schematic(vector.offset(pos, 0, schema.yadjust or 0, 0), schematic, "0",
vector.offset(pos, 0, schema.yadjust or 0, 0),
schematic,
"0",
{["air"] = "ignore"}, -- avoid destroying stairs etc. {["air"] = "ignore"}, -- avoid destroying stairs etc.
true, true,
{ place_center_x = true, place_center_y = false, place_center_z = true } { place_center_x = true, place_center_y = false, place_center_z = true }
) )
end end
-- TODO: port this to lvm.
local function smooth_path(path) local function smooth_path(path)
-- Smooth out bumps in path or stairs can look naf -- Smooth out bumps in path or stairs can look naf
for pass = 1, 3 do for pass = 1, 3 do
@ -64,10 +91,10 @@ local function smooth_path(path)
local prev_y = path[i - 1].y local prev_y = path[i - 1].y
local y = path[i].y local y = path[i].y
local next_y = path[i + 1].y local next_y = path[i + 1].y
local bump_node = minetest.get_node(path[i]) local bump = minetest.get_node(path[i]).name
-- TODO: replace bamboo underneath with dirt here? -- TODO: also replace bamboo underneath with dirt here?
if minetest.get_item_group(bump_node.name, "water") ~= 0 then if minetest.get_item_group(bump, "water") ~= 0 then
-- ignore in this pass -- ignore in this pass
elseif y >= next_y + 2 and y <= prev_y then elseif y >= next_y + 2 and y <= prev_y then
minetest.swap_node(vector.offset(path[i], 0, -1, 0), { name = "air" }) minetest.swap_node(vector.offset(path[i], 0, -1, 0), { name = "air" })
@ -94,25 +121,25 @@ local function smooth_path(path)
end end
end end
-- TODO: port this to lvm.
local function place_path(path, pr, stair, slab) local function place_path(path, pr, stair, slab)
-- Smooth out bumps in path or stairs can look naf -- Smooth out bumps in path or stairs can look naf
for i = 2, #path - 1 do for i = 2, #path - 1 do
local prev_y = path[i - 1].y local prev_y = path[i - 1].y
local y = path[i].y local y = path[i].y
local next_y = path[i + 1].y local next_y = path[i + 1].y
local bump_node = minetest.get_node(path[i]) local bump = minetest.get_node(path[i]).name
if minetest.get_item_group(bump_node.name, "water") ~= 0 then if minetest.get_item_group(bump, "water") ~= 0 then
-- Find air -- Find air
local found_surface = false local up_pos = vector.copy(path[i])
local up_pos = path[i] while true do
while not found_surface do up_pos.y = up_pos.y + 1
up_pos = vector.offset(up_pos, 0, 1, 0) local up_node = minetest.get_node(up_pos).name
local up_node = minetest.get_node(up_pos) if minetest.get_item_group(up_node, "water") == 0 then
if up_node and minetest.get_item_group(up_node.name, "water") == 0 then
found_surface = true
minetest.swap_node(up_pos, { name = "air" }) minetest.swap_node(up_pos, { name = "air" })
path[i] = up_pos path[i] = up_pos
break
end end
end end
elseif y < prev_y and y < next_y then elseif y < prev_y and y < next_y then
@ -129,36 +156,32 @@ local function place_path(path, pr, stair, slab)
end end
for i, pos in ipairs(path) do for i, pos in ipairs(path) do
-- replace decorations, grass and flowers, with air local n0 = minetest.get_node(pos).name
local n0 = minetest.get_node(pos) if n0 ~= "air" then minetest.swap_node(pos, { name = "air" }) end
if n0.name ~= "air" then
minetest.swap_node(pos, { name = "air" })
end
local under_pos = vector.offset(pos, 0, -1, 0) local under_pos = vector.offset(pos, 0, -1, 0)
local n = minetest.get_node(under_pos) local n = minetest.get_node(under_pos).name
local ndef = minetest.registered_nodes[n]
local groups = ndef and ndef.groups or {}
local done = false local done = false
local is_stair = minetest.get_item_group(n.name, "stair") ~= 0
if i > 1 and pos.y > path[i - 1].y then if i > 1 and pos.y > path[i - 1].y then
-- stairs up -- stairs up
if not is_stair then if not groups.stair then
done = true done = true
local param2 = minetest.dir_to_facedir(vector.subtract(pos, path[i - 1])) local param2 = minetest.dir_to_facedir(vector.subtract(pos, path[i - 1]))
minetest.swap_node(under_pos, { name = stair, param2 = param2 }) minetest.swap_node(under_pos, { name = stair, param2 = param2 })
end end
elseif i < #path-1 and pos.y > path[i + 1].y then elseif i < #path-1 and pos.y > path[i + 1].y then
-- stairs down -- stairs down
if not is_stair then if not groups.stair then
done = true done = true
local param2 = minetest.dir_to_facedir(vector.subtract(pos, path[i + 1])) local param2 = minetest.dir_to_facedir(vector.subtract(pos, path[i + 1]))
minetest.swap_node(under_pos, { name = stair, param2 = param2 }) minetest.swap_node(under_pos, { name = stair, param2 = param2 })
end end
elseif not is_stair and i > 1 and pos.y < path[i - 1].y then elseif not groups.stair and i > 1 and pos.y < path[i - 1].y then
-- stairs down -- stairs down
local n2 = minetest.get_node(vector.offset(path[i - 1], 0, -1, 0)) local n2 = minetest.get_node(vector.offset(path[i - 1], 0, -1, 0)).name
is_stair = minetest.get_item_group(n2.name, "stair") ~= 0 if not minetest.get_item_group(n2, "stair") then
if not is_stair then
done = true done = true
local param2 = minetest.dir_to_facedir(vector.subtract(path[i - 1], pos)) local param2 = minetest.dir_to_facedir(vector.subtract(path[i - 1], pos))
if i < #path - 1 then -- uglier, but easier to walk up? if i < #path - 1 then -- uglier, but easier to walk up?
@ -167,11 +190,10 @@ local function place_path(path, pr, stair, slab)
minetest.add_node(pos, { name = stair, param2 = param2 }) minetest.add_node(pos, { name = stair, param2 = param2 })
pos.y = pos.y + 1 pos.y = pos.y + 1
end end
elseif not is_stair and i < #path-1 and pos.y < path[i + 1].y then elseif not groups.stair and i < #path-1 and pos.y < path[i + 1].y then
-- stairs up -- stairs up
local n2 = minetest.get_node(vector.offset(path[i + 1], 0, -1, 0)) local n2 = minetest.get_node(vector.offset(path[i + 1], 0, -1, 0)).name
is_stair = minetest.get_item_group(n2.name, "stair") ~= 0 if not minetest.get_item_group(n2, "stair") then
if not is_stair then
done = true done = true
local param2 = minetest.dir_to_facedir(vector.subtract(path[i + 1], pos)) local param2 = minetest.dir_to_facedir(vector.subtract(path[i + 1], pos))
if i > 1 then -- uglier, but easier to walk up? if i > 1 then -- uglier, but easier to walk up?
@ -184,13 +206,11 @@ local function place_path(path, pr, stair, slab)
-- flat -- flat
if not done then if not done then
if minetest.get_item_group(n.name, "water") ~= 0 then if groups.water then
minetest.add_node(under_pos, { name = slab }) minetest.add_node(under_pos, { name = slab })
elseif n.name == "mcl_core:sand" or n.name == "mcl_core:redsand" then elseif groups.sand then
minetest.swap_node(under_pos, { name = "mcl_core:sandstonesmooth2" }) minetest.swap_node(under_pos, { name = "mcl_core:sandstonesmooth2" })
elseif minetest.get_item_group(n.name, "soil") > 0 elseif groups.soil and not groups.dirtifies_below_solid then
and minetest.get_item_group(n.name, "dirtifies_below_solid") == 0
then
minetest.swap_node(under_pos, { name = "mcl_core:grass_path" }) minetest.swap_node(under_pos, { name = "mcl_core:grass_path" })
end end
end end
@ -198,8 +218,7 @@ local function place_path(path, pr, stair, slab)
-- Clear space for villagers to walk -- Clear space for villagers to walk
for j = 1, 2 do for j = 1, 2 do
local over_pos = vector.offset(pos, 0, j, 0) local over_pos = vector.offset(pos, 0, j, 0)
local m = minetest.get_node(over_pos) if minetest.get_node(over_pos).name ~= "air" then
if m.name ~= "air" then
minetest.swap_node(over_pos, { name = "air" }) minetest.swap_node(over_pos, { name = "air" })
end end
end end
@ -208,17 +227,15 @@ local function place_path(path, pr, stair, slab)
-- Do lamps afterwards so we don't put them where a path will be laid -- Do lamps afterwards so we don't put them where a path will be laid
for _, pos in ipairs(path) do for _, pos in ipairs(path) do
if minetest.get_node_light(pos, 0) < light_threshold then if minetest.get_node_light(pos, 0) < light_threshold then
local nn = minetest.find_nodes_in_area_under_air( local nn = minetest.find_nodes_in_area_under_air(vector.offset(pos, -1, -1, -1), vector.offset(pos, 1, 1, 1),
vector.offset(pos, -1, -1, -1),
vector.offset(pos, 1, 1, 1),
{ "group:material_sand", "group:material_stone", "group:grass_block", "group:wood_slab" } { "group:material_sand", "group:material_stone", "group:grass_block", "group:wood_slab" }
) )
-- todo: shuffle nn?
for _, npos in ipairs(nn) do for _, npos in ipairs(nn) do
local node = minetest.get_node(npos) local node = minetest.get_node(npos).name
if node.name ~= "mcl_core:grass_path" and minetest.get_item_group(node.name, "stair") == 0 then if node ~= "mcl_core:grass_path" and minetest.get_item_group(node, "stair") == 0 then
if minetest.get_item_group(node.name, "wood_slab") ~= 0 then if minetest.get_item_group(node, "wood_slab") ~= 0 then
local over_pos = vector.offset(npos, 0, 1, 0) minetest.add_node(vector.offset(npos, 0, 1, 0), { name = "mcl_torches:torch", param2 = 1 })
minetest.add_node(over_pos, { name = "mcl_torches:torch", param2 = 1 })
else else
place_lamp(npos, pr) place_lamp(npos, pr)
end end
@ -229,20 +246,11 @@ local function place_path(path, pr, stair, slab)
end end
end end
-- Work out which end points should be connected -- FIXME: ugly
-- works from the outside of the village in function get_biome_stair_slab(biome_name)
function mcl_villages.paths(blockseed, biome_name)
local pr = PseudoRandom(blockseed)
local pathends = path_ends["block_" .. blockseed]
if pathends == nil then
minetest.log("warning", string.format("[mcl_villages] Tried to set paths for block seed that doesn't exist %d", blockseed))
return
end
-- Use the same stair and slab throughout the entire village -- Use the same stair and slab throughout the entire village
-- The quotes are necessary to be matched as JSON strings
local stair, slab = '"mcl_stairs:stair_oak"', '"mcl_stairs:slab_oak_top"' local stair, slab = '"mcl_stairs:stair_oak"', '"mcl_stairs:slab_oak_top"'
-- Change stair and slab for biome -- Change stair and slab for biome
if mcl_villages.biome_map[biome_name] and mcl_villages.material_substitions[mcl_villages.biome_map[biome_name]] then if mcl_villages.biome_map[biome_name] and mcl_villages.material_substitions[mcl_villages.biome_map[biome_name]] then
for _, sub in pairs(mcl_villages.material_substitions[mcl_villages.biome_map[biome_name]]) do for _, sub in pairs(mcl_villages.material_substitions[mcl_villages.biome_map[biome_name]]) do
@ -255,44 +263,45 @@ function mcl_villages.paths(blockseed, biome_name)
stair = stair:gsub(sub[1], sub[2]) stair = stair:gsub(sub[1], sub[2])
slab = slab:gsub(sub[1], sub[2]) slab = slab:gsub(sub[1], sub[2])
end end
-- The quotes are to match what is in schemas, but we don't want them now -- The quotes are to match what is in JSON schemas, but we don't want them now
stair = stair:gsub('"', "") return stair:gsub('"', ""), slab:gsub('"', "")
slab = slab:gsub('"', "") end
-- Work out which end points should be connected
-- works from the outside of the village in
function mcl_villages.paths(blockseed, biome_name)
local pr = PcgRandom(blockseed)
local pathends = path_ends["block_" .. blockseed]
if pathends == nil then
minetest.log("warning", string.format("[mcl_villages] Tried to set paths for block seed that doesn't exist %d", blockseed))
return
end
-- Stair and slab style of the village
local stair, slab = get_biome_stair_slab(biome_name)
-- Keep track of connections -- Keep track of connections
local connected = {} local connected = {}
-- get a list of reverse sorted keys, which are distances -- get a list of reverse sorted keys, which are distances
local dist_keys = {} local dist_keys = {}
for k in pairs(pathends) do for k in pairs(pathends) do table.insert(dist_keys, k) end
table.insert(dist_keys, k) table.sort(dist_keys, function(a, b) return a > b end)
end
table.sort(dist_keys, function(a, b)
return a > b
end)
--minetest.log("Planning paths with "..#dist_keys.." nodes") --minetest.log("Planning paths with "..#dist_keys.." nodes")
for i, from in ipairs(dist_keys) do for i, from in ipairs(dist_keys) do
-- ep == end_point -- ep == end_point
for _, from_ep in ipairs(pathends[from]) do for _, from_ep in ipairs(pathends[from]) do
local from_ep_pos = minetest.string_to_pos(from_ep) local from_ep_pos = minetest.string_to_pos(from_ep)
local closest_pos local closest_pos, closest_bld, best = nil, nil, 10000000
local closest_bld
local best = 10000000
-- Most buildings only do other buildings that are closer to the bell -- Most buildings only do other buildings that are closer to the bell
-- for the bell do any end points that don't have paths near them -- for the bell do any end points that don't have paths near them
local lindex = i + 1 local j = from == 0 and 1 or (i + 1)
if i == 0 then for j = j, #dist_keys do
lindex = 1
end
for j = lindex, #dist_keys do
local to = dist_keys[j] local to = dist_keys[j]
if from ~= to and connected[from .. "-" .. to] == nil and connected[to .. "-" .. from] == nil then if from ~= to and connected[from .. "-" .. to] == nil and connected[to .. "-" .. from] == nil then
for _, to_ep in ipairs(pathends[to]) do for _, to_ep in ipairs(pathends[to]) do
local to_ep_pos = minetest.string_to_pos(to_ep) local to_ep_pos = minetest.string_to_pos(to_ep)
local dist = vector.distance(from_ep_pos, to_ep_pos) local dist = vector.distance(from_ep_pos, to_ep_pos)
if dist < best then if dist < best then
best = dist best = dist
@ -315,10 +324,9 @@ function mcl_villages.paths(blockseed, biome_name)
place_path(path, pr, stair, slab) place_path(path, pr, stair, slab)
connected[from .. "-" .. closest_bld] = 1 connected[from .. "-" .. closest_bld] = 1
else else
minetest.log( minetest.log("warning",
"warning",
string.format( string.format(
"[mcl_villages] No path from %s to %s, distance %d", "[mcl_villages] No good path from %s to %s, distance %d",
minetest.pos_to_string(from_ep_pos), minetest.pos_to_string(from_ep_pos),
minetest.pos_to_string(closest_pos), minetest.pos_to_string(closest_pos),
vector.distance(from_ep_pos, closest_pos) vector.distance(from_ep_pos, closest_pos)
@ -329,20 +337,5 @@ function mcl_villages.paths(blockseed, biome_name)
end end
end end
-- Loop again to blow away no path nodes
for _, from in ipairs(dist_keys) do
for _, from_ep in ipairs(pathends[from]) do
local from_ep_pos = minetest.string_to_pos(from_ep)
local no_paths_nodes = minetest.find_nodes_in_area(
vector.offset(from_ep_pos, -32, -32, -32),
vector.offset(from_ep_pos, 32, 32, 32),
{ "mcl_villages:no_paths" }
)
if #no_paths_nodes > 0 then
minetest.bulk_set_node(no_paths_nodes, { name = "air" })
end
end
end
path_ends["block_" .. blockseed] = nil path_ends["block_" .. blockseed] = nil
end end

View file

@ -1,4 +1,5 @@
-- check the minimum distance of two squares, on axes -- check the minimum distance of two squares, on axes
-- TODO: make local in village planning code only?
function mcl_villages.check_distance(settlement, cpos, sizex, sizez, limit) function mcl_villages.check_distance(settlement, cpos, sizex, sizez, limit)
for i, building in ipairs(settlement) do for i, building in ipairs(settlement) do
local opos, osizex, osizez = building.pos, building.size.x, building.size.z local opos, osizex, osizez = building.pos, building.size.x, building.size.z
@ -22,8 +23,7 @@ function mcl_villages.fill_chest(pos, pr)
end end
-- fill chest -- fill chest
local inv = minetest.get_inventory( {type="node", pos=pos} ) local inv = minetest.get_inventory( {type="node", pos=pos} )
local function get_treasures(pr)
local function get_treasures(prand)
local loottable = {{ local loottable = {{
stacks_min = 3, stacks_min = 3,
stacks_max = 8, stacks_max = 8,
@ -47,46 +47,11 @@ function mcl_villages.fill_chest(pos, pr)
{ itemstring = "mcl_mobitems:diamond_horse_armor", weight = 1 }, { itemstring = "mcl_mobitems:diamond_horse_armor", weight = 1 },
} }
}} }}
local items = mcl_loot.get_multi_loot(loottable, prand) return mcl_loot.get_multi_loot(loottable, pr)
return items end
mcl_loot.fill_inventory(inv, "main", get_treasures(pr), pr)
end end
local items = get_treasures(pr)
mcl_loot.fill_inventory(inv, "main", items, pr)
end
-------------------------------------------------------------------------------
-- initialize furnace
-------------------------------------------------------------------------------
function mcl_villages.initialize_furnace(pos)
-- find chests within radius
local furnacepos = minetest.find_node_near(pos,
7, --radius
{"mcl_furnaces:furnace"})
-- initialize furnacepos (mts furnacepos don't have meta)
if furnacepos then
local meta = minetest.get_meta(furnacepos)
if meta:get_string("infotext") ~= "furnace" then
minetest.registered_nodes["mcl_furnaces:furnace"].on_construct(furnacepos)
end
end
end
-------------------------------------------------------------------------------
-- initialize anvil
-------------------------------------------------------------------------------
function mcl_villages.initialize_anvil(pos)
-- find chests within radius
local anvilpos = minetest.find_node_near(pos,
7, --radius
{"mcl_anvils:anvil"})
-- initialize anvilpos (mts anvilpos don't have meta)
if anvilpos then
local meta = minetest.get_meta(anvilpos)
if meta:get_string("infotext") ~= "anvil" then
minetest.registered_nodes["mcl_anvils:anvil"].on_construct(anvilpos)
end
end
end
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
-- randomize table -- randomize table
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
@ -100,28 +65,41 @@ end
-- Load a schema and replace nodes in it based on biome -- Load a schema and replace nodes in it based on biome
function mcl_villages.substitute_materials(pos, schem_lua, pr) function mcl_villages.substitute_materials(pos, schem_lua, pr)
local modified_schem_lua = schem_lua local biome_name = minetest.get_biome_name(minetest.get_biome_data(pos).biome)
local biome_data = minetest.get_biome_data(pos)
local biome_name = minetest.get_biome_name(biome_data.biome)
-- for now, map to MCLA, later back, so we can keep their rules unchanged -- for now, map to MCLA, later back, so we can keep their rules unchanged
for _, sub in pairs(mcl_villages.vl_to_mcla) do for _, sub in pairs(mcl_villages.vl_to_mcla) do
modified_schem_lua = modified_schem_lua:gsub(sub[1], sub[2]) schem_lua = schem_lua:gsub(sub[1], sub[2])
end end
if mcl_villages.biome_map[biome_name] and mcl_villages.material_substitions[mcl_villages.biome_map[biome_name]] then if mcl_villages.biome_map[biome_name] and mcl_villages.material_substitions[mcl_villages.biome_map[biome_name]] then
for _, sub in pairs(mcl_villages.material_substitions[mcl_villages.biome_map[biome_name]]) do for _, sub in pairs(mcl_villages.material_substitions[mcl_villages.biome_map[biome_name]]) do
modified_schem_lua = modified_schem_lua:gsub(sub[1], sub[2]) schem_lua = schem_lua:gsub(sub[1], sub[2])
end end
end end
-- MCLA node names back to VL -- MCLA node names back to VL
for _, sub in pairs(mcl_villages.mcla_to_vl) do for _, sub in pairs(mcl_villages.mcla_to_vl) do
modified_schem_lua = modified_schem_lua:gsub(sub[1], sub[2]) schem_lua = schem_lua:gsub(sub[1], sub[2])
end
return modified_schem_lua
end end
-- Farming: place crops
if string.find(schem_lua, "mcl_villages:crop_") then
local map_name = mcl_villages.biome_map[biome_name] or "plains"
for _, crop in ipairs(mcl_villages.get_crop_types()) do
if string.find(schem_lua, "mcl_villages:crop_" .. crop) then
for count = 1, 8 do
local name = "mcl_villages:crop_" .. crop .. "_" .. count
local replacement = mcl_villages.get_weighted_crop(map_name, crop, pr)
schem_lua = schem_lua:gsub(name, replacement or mcl_villages.default_crop)
end
end
end
end
return schem_lua
end
-- Persistent registry for villages
local villages = {} local villages = {}
local mod_storage = minetest.get_mod_storage() local mod_storage = minetest.get_mod_storage()
@ -152,8 +130,7 @@ function mcl_villages.add_village(name, data)
minetest.log("info", "Village already exists: " .. name ) minetest.log("info", "Village already exists: " .. name )
return false return false
end end
mod_storage:set_string("mcl_villages." .. name, minetest.serialize({ name = name, data = data }))
local new_village = {name = name, data = data}
mod_storage:set_string("mcl_villages." .. name, minetest.serialize(new_village))
return true return true
end end