1757 lines
60 KiB
Lua
1757 lines
60 KiB
Lua
|
|
--
|
|
-- Single village generation
|
|
--
|
|
|
|
village.villages = {}
|
|
|
|
-- Sidelength of the square of a village chunk, in nodes
|
|
local VILLAGE_CHUNK_SIZE = 12
|
|
-- Maximum height of village chunks (buildings)
|
|
local VILLAGE_CHUNK_HEIGHT = 20
|
|
|
|
-- Hill width and height
|
|
local HILL_W, HILL_H = 24, 6
|
|
-- Number of dirt nodes to extend below hill
|
|
local HILL_EXTEND_BELOW = 15
|
|
|
|
-- Chance values. Each chance is provided in a 1:x value,
|
|
-- e.g. a value of 8 means a chance of 1:8.
|
|
-- Only positive integers are allowed.
|
|
|
|
-- Chance that a ground node has a decor node on top (grass, etc.)
|
|
local DECOR_CHANCE = 8
|
|
|
|
-- Chance that a village is abandoned
|
|
local ABANDONED_CHANCE = 25
|
|
|
|
-- Chance a village chunk in an abandoned village spawns using
|
|
-- a 'ruins' variant (if one is available)
|
|
local ABANDONED_RUINS_CHANCE = 2
|
|
|
|
-- VoxelManip buffers for get_data() for more efficient memory usage
|
|
local vdata_main = {}
|
|
local vdata_spawn_chunk = {}
|
|
|
|
-- Savefile
|
|
|
|
local village_file = minetest.get_worldpath() .. "/villages.dat"
|
|
|
|
local modpath = minetest.get_modpath("rp_village")
|
|
local mod_locks = minetest.get_modpath("rp_locks") ~= nil
|
|
local mod_paint = minetest.get_modpath("rp_paint") ~= nil
|
|
local mapseed = minetest.get_mapgen_setting("seed")
|
|
local water_level = tonumber(minetest.get_mapgen_setting("water_level"))
|
|
|
|
local wood_materials = {
|
|
wood = {
|
|
"rp_default:planks",
|
|
"rp_partialblocks:stair_wood",
|
|
"rp_partialblocks:slab_wood",
|
|
"rp_default:tree",
|
|
"rp_default:fence",
|
|
"rp_default:fence_gate_closed",
|
|
"rp_door:door_wood_t_1",
|
|
"rp_door:door_wood_b_1",
|
|
"rp_door:door_wood_t_2",
|
|
"rp_door:door_wood_b_2",
|
|
},
|
|
birch = {
|
|
"rp_default:planks_birch",
|
|
"rp_partialblocks:stair_birch",
|
|
"rp_partialblocks:slab_birch",
|
|
"rp_default:tree_birch",
|
|
"rp_default:fence_birch",
|
|
"rp_default:fence_gate_birch_closed",
|
|
"rp_door:door_wood_birch_t_1",
|
|
"rp_door:door_wood_birch_b_1",
|
|
"rp_door:door_wood_birch_t_2",
|
|
"rp_door:door_wood_birch_b_2",
|
|
},
|
|
oak = {
|
|
"rp_default:planks_oak",
|
|
"rp_partialblocks:stair_oak",
|
|
"rp_partialblocks:slab_oak",
|
|
"rp_default:tree_oak",
|
|
"rp_default:fence_oak",
|
|
"rp_default:fence_gate_oak_closed",
|
|
"rp_door:door_wood_oak_t_1",
|
|
"rp_door:door_wood_oak_b_1",
|
|
"rp_door:door_wood_oak_t_2",
|
|
"rp_door:door_wood_oak_b_2",
|
|
},
|
|
fir = {
|
|
"rp_default:planks_fir",
|
|
"rp_partialblocks:stair_fir",
|
|
"rp_partialblocks:slab_fir",
|
|
"rp_default:tree_fir",
|
|
"rp_default:fence_fir",
|
|
"rp_default:fence_gate_fir_closed",
|
|
"rp_door:door_wood_fir_t_1",
|
|
"rp_door:door_wood_fir_b_1",
|
|
"rp_door:door_wood_fir_t_2",
|
|
"rp_door:door_wood_fir_b_2",
|
|
},
|
|
}
|
|
|
|
-- List of wood types used in the source schematic files
|
|
local SOURCE_STYLES = { "birch", "oak", "wood" }
|
|
--[[ List of village wood materials (schematic replacements)
|
|
One of these will be chosen at random per village. ]]
|
|
local village_wood_styles = {
|
|
{ "birch", "oak", "wood" },
|
|
{ "wood", "oak", "wood" },
|
|
{ "oak", "fir", "wood" },
|
|
{ "wood", "birch", "wood" },
|
|
{ "fir", "birch", "wood" },
|
|
{ "wood", "wood", "wood" },
|
|
{ "birch", "birch", "birch" },
|
|
{ "oak", "oak", "oak" }, -- must be synced with VILLAGE_REPLACE_OAK
|
|
{ "fir", "fir", "fir" },
|
|
}
|
|
-- Index of oak-only village wood style (must be synced with village_wood_styles
|
|
-- table)
|
|
local VILLAGE_REPLACE_OAK = 8
|
|
|
|
--[[ Generate list of replacable house blocks.
|
|
It will be a table indexed by the original node name (used in the schematic)
|
|
with the value being the new node name (to replace it with).
|
|
This allows houses to vary in wood styles ]]
|
|
local village_replaces = {}
|
|
for s=1, #village_wood_styles do
|
|
local style = village_wood_styles[s]
|
|
local replace = {}
|
|
for t=1, #style do
|
|
local materials = wood_materials[style[t]]
|
|
local source_materials = wood_materials[SOURCE_STYLES[t]]
|
|
for w=1, #materials do
|
|
local mat1 = source_materials[w]
|
|
local mat2 = materials[w]
|
|
if mat1 ~= mat2 then
|
|
replace[mat1] = mat2
|
|
end
|
|
end
|
|
end
|
|
table.insert(village_replaces, replace)
|
|
end
|
|
|
|
local schematic_cache = {}
|
|
-- Wrapper around minetest.read_schematic to
|
|
-- speed up loading time on subsequent reads.
|
|
-- returns a schematic specifier.
|
|
local function read_cached_chunk_schematic(subchunktype)
|
|
if schematic_cache[subchunktype] then
|
|
return schematic_cache[subchunktype], true
|
|
end
|
|
local schem_path = modpath .. "/schematics/village_" .. subchunktype .. ".mts"
|
|
local schem_spec = minetest.read_schematic(schem_path, {})
|
|
schematic_cache[subchunktype] = schem_spec
|
|
return schem_spec, false
|
|
end
|
|
|
|
function village.get_id(name, pos)
|
|
return name .. string.format("%d", minetest.hash_node_position(pos))
|
|
end
|
|
|
|
function village.save_villages()
|
|
local f = io.open(village_file, "w")
|
|
|
|
for name, def in pairs(village.villages) do
|
|
f:write(name .. " " .. def.name .. " "
|
|
.. string.format("%d", minetest.hash_node_position(def.pos)) .. "\n")
|
|
end
|
|
|
|
io.close(f)
|
|
end
|
|
|
|
function village.load_villages()
|
|
local f = io.open(village_file, "r")
|
|
|
|
if f then
|
|
repeat
|
|
local l = f:read("*l")
|
|
if l == nil then break end
|
|
|
|
for name, fname, pos in string.gmatch(l, "(.+) (%a+) (%d.+)") do
|
|
village.villages[name] = {
|
|
name = fname,
|
|
pos = minetest.get_position_from_hash(pos),
|
|
}
|
|
village.name.used[fname] = true
|
|
end
|
|
until f:read(0) == nil
|
|
|
|
io.close(f)
|
|
else
|
|
village.save_villages()
|
|
end
|
|
|
|
village.load_waypoints()
|
|
end
|
|
|
|
function village.load_waypoints()
|
|
for name, def in pairs(village.villages) do
|
|
nav.remove_waypoint("village_" .. name)
|
|
nav.add_waypoint(
|
|
def.pos,
|
|
"village_" .. name,
|
|
def.name .. " village",
|
|
true,
|
|
"village"
|
|
)
|
|
end
|
|
end
|
|
|
|
function village.get_nearest_village(pos)
|
|
local nearest = math.huge
|
|
local npos = nil -- village pos
|
|
local fname = nil -- human-readable village name
|
|
local name = nil -- village ID
|
|
|
|
for name, def in pairs(village.villages) do
|
|
local dist = vector.distance(pos, def.pos)
|
|
if dist < nearest then
|
|
nearest = dist
|
|
name = name
|
|
fname = def.name
|
|
npos = def.pos
|
|
end
|
|
end
|
|
if not fname then
|
|
return nil
|
|
end
|
|
return {dist = nearest, pos = npos, name = name, fname = fname}
|
|
end
|
|
|
|
village.chunkdefs = {}
|
|
|
|
--[[
|
|
Village chunks are the square sections of a village. This includes buildings,
|
|
farms, roads, and the like.
|
|
|
|
village chunk definition:
|
|
{
|
|
-- every field is optional
|
|
can_cache = <bool>, -- if true, schematic can be cached by Luanti
|
|
-- use this if no random node replacements (like wood)
|
|
-- are required (default: false)
|
|
variants = { "variant_1", ..., "variant_n" },
|
|
-- list of chunktype variants. One random variatn will be picked at random
|
|
-- on placement. Each name must correspond to a file
|
|
-- in `schematics/village_<variant_name>.mts`. By default, a chunktype has
|
|
-- 1 variant with the name equal to the chunktype identifier
|
|
groundclass_variants = {
|
|
[ "groundclass_1"] = { "variant_1", ... "variant_n" },
|
|
...,
|
|
[ "groundclass_n"] = { "another_variant_1", ... "another_variant_n" },
|
|
},
|
|
-- An alternative way to specify variants. Instead of a single list of
|
|
-- variants, this specifies multiple lists of variants, each assigned
|
|
-- a groundclass. In this case, the chunktype can only be placed if
|
|
-- the village has this given groundclass. If it has that groundclass,
|
|
-- a random variant in that groundclass is selected.
|
|
|
|
-- Note: either variants, groundclass_variants or neither can be specified,
|
|
-- but not both.
|
|
|
|
ruins = { "ruins_1", ..., "ruins_n" },
|
|
-- Like variants, but for a ruined version of this chunktype. In abandoned
|
|
-- villages, one random ruined variant MAY be chosen from the list, or the
|
|
-- 'intact' schematic is placed (but "generic" ruinations like broken glass
|
|
-- still apply).
|
|
-- If unused, the 'intact' schematic will be placed.
|
|
|
|
groundclass_ruins = {
|
|
[ "groundclass_1"] = { "ruins_1", ... "ruins_n" },
|
|
...,
|
|
[ "groundclass_n"] = { "another_ruins_1", ... "another_ruins_n" },
|
|
},
|
|
-- Like groundclass_variants, but for ruins.
|
|
|
|
entities = {
|
|
[entity_1] = <number>,
|
|
...
|
|
[entity_n] = <number>,
|
|
},
|
|
-- list of entities that can spawn (needs entity spawner node in schematic).
|
|
-- For villagers, the key must be of the form '__villager_<profession>',
|
|
-- e.g. '__villager_farmer'.
|
|
-- For all other entities, use the entitystring, e.g. 'rp_mobs_mobs:sheep'.
|
|
|
|
entity_chance = <number>,
|
|
-- Chance for an entity to spawn by an entity spawner. Chance is 1/<number>
|
|
}
|
|
]]
|
|
|
|
village.chunkdefs["livestock_pen"] = {
|
|
groundclass_variants = {
|
|
["grassland"] = {"livestock_pen", "livestock_pen_mirrored"},
|
|
},
|
|
entities = {
|
|
["rp_mobs_mobs:sheep"] = 3,
|
|
["rp_mobs_mobs:boar"] = 1,
|
|
},
|
|
}
|
|
village.chunkdefs["lamppost"] = { -- not road because of road height limit of 1 nodes
|
|
groundclass_variants = {
|
|
["grassland"] = {"lamppost"},
|
|
["dry"] = {"lamppost"},
|
|
["savanna"] = {"lamppost"},
|
|
["swamp"] = {"swamp_lamppost"},
|
|
},
|
|
groundclass_ruins = {
|
|
["grassland"] = {"lamppost_ruins"},
|
|
["dry"] = {"lamppost_ruins"},
|
|
["savanna"] = {"lamppost_ruins"},
|
|
["swamp"] = {"swamp_lamppost_ruins", "swamp_lamppost_ruins_2"},
|
|
},
|
|
can_cache = true,
|
|
entity_chance = 2,
|
|
entities = {
|
|
["__villager_carpenter"] = 1,
|
|
},
|
|
}
|
|
village.chunkdefs["well"] = {
|
|
ruins = {"well_ruins"},
|
|
entities = {
|
|
["__villager_farmer"] = 1,
|
|
["__villager_tavernkeeper"] = 1,
|
|
},
|
|
}
|
|
village.chunkdefs["house"] = {
|
|
groundclass_variants = {
|
|
["grassland"] = {"house", "house_2", "house_3", "house_4", "house_5", "house_6", "house_7", "house_8", "house_9"},
|
|
["dry"] = {"house", "house_2", "house_3", "house_4", "house_5", "house_6", "house_7", "house_8", "house_9"},
|
|
["savanna"] = {"house", "house_2", "house_3", "house_4", "house_5", "house_6", "house_7", "house_8", "house_9"},
|
|
},
|
|
ruins = {"house_ruins", "house_ruins_2"},
|
|
entity_chance = 2,
|
|
entities = {
|
|
["__villager_carpenter"] = 1,
|
|
},
|
|
}
|
|
village.chunkdefs["hut_s"] = {
|
|
groundclass_variants = {
|
|
["swamp"] = {"reed_hut_s_1","reed_hut_s_2","reed_hut_s_3","reed_hut_s_4","reed_hut_s_5","reed_hut_s_6","reed_hut_s_7"},
|
|
},
|
|
ruins = {"reed_hut_s_ruins", "reed_hut_s_ruins_2"},
|
|
entitity_chance = 2,
|
|
entities = {
|
|
["__villager_farmer"] = 1,
|
|
},
|
|
}
|
|
village.chunkdefs["hut_m"] = {
|
|
groundclass_variants = {
|
|
["swamp"] = {"reed_hut_m_1","reed_hut_m_2","reed_hut_m_3","reed_hut_m_4","reed_hut_m_5","reed_hut_m_6","reed_hut_m_7"},
|
|
},
|
|
ruins = {"reed_hut_m_ruins", "reed_hut_m_ruins_2"},
|
|
entitity_chance = 2,
|
|
entities = {
|
|
["__villager_farmer"] = 1,
|
|
},
|
|
}
|
|
|
|
village.chunkdefs["workshop"] = {
|
|
groundclass_variants = {
|
|
grassland = {"workshop"},
|
|
dry = {"workshop"},
|
|
savanna = {"workshop"},
|
|
swamp = {"reed_workshop"},
|
|
},
|
|
groundclass_ruins = {
|
|
grassland = {"workshop_ruins", "rubble"},
|
|
dry = {"workshop_ruins", "rubble"},
|
|
savanna = {"workshop_ruins", "rubble"},
|
|
swamp = {"reed_workshop_ruins"},
|
|
},
|
|
entity_chance = 2,
|
|
entities = {
|
|
["__villager_carpenter"] = 1,
|
|
},
|
|
}
|
|
village.chunkdefs["townhall"] = {
|
|
groundclass_variants = {
|
|
grassland = {"townhall"},
|
|
dry = {"townhall"},
|
|
savanna = {"townhall"},
|
|
swamp = {"reed_townhall"},
|
|
},
|
|
groundclass_ruins = {
|
|
grassland = {"townhall_ruins", "rubble"},
|
|
dry = {"townhall_ruins", "rubble"},
|
|
savanna = {"townhall_ruins", "rubble"},
|
|
swamp = {"reed_townhall_ruins"},
|
|
},
|
|
entity_chance = 1,
|
|
entities = {
|
|
["__villager_tavernkeeper"] = 1,
|
|
["__villager_farmer"] = 1,
|
|
["__villager_blacksmith"] = 1,
|
|
["__villager_carpenter"] = 1,
|
|
},
|
|
}
|
|
|
|
|
|
village.chunkdefs["tavern"] = {
|
|
groundclass_variants = {
|
|
grassland = {"tavern"},
|
|
dry = {"tavern"},
|
|
savanna = {"tavern"},
|
|
swamp = {"reed_tavern_1"},
|
|
},
|
|
groundclass_ruins = {
|
|
grassland = {"tavern_ruins"},
|
|
dry = {"tavern_ruins"},
|
|
savanna = {"tavern_ruins"},
|
|
swamp = {"reed_tavern_ruins_1", "reed_tavern_ruins_2"},
|
|
},
|
|
entity_chance = 2,
|
|
entities = {
|
|
["__villager_tavernkeeper"] = 1,
|
|
},
|
|
}
|
|
|
|
village.chunkdefs["inn"] = {
|
|
groundclass_variants = {
|
|
grassland = {"inn"},
|
|
dry = {"inn"},
|
|
savanna = {"inn"},
|
|
},
|
|
groundclass_ruins = {
|
|
grassland = {"tavern_ruins"},
|
|
dry = {"tavern_ruins"},
|
|
savanna = {"tavern_ruins"},
|
|
},
|
|
entity_chance = 2,
|
|
entities = {
|
|
["__villager_tavernkeeper"] = 1,
|
|
},
|
|
}
|
|
|
|
village.chunkdefs["library"] = {
|
|
groundclass_variants = {
|
|
grassland = {"library"},
|
|
dry = {"library"},
|
|
savanna = {"library"},
|
|
swamp = {"reed_library"},
|
|
},
|
|
groundclass_ruins = {
|
|
grassland = {"library_ruins"},
|
|
dry = {"library_ruins"},
|
|
savanna = {"library_ruins"},
|
|
swamp = {"reed_library_ruins"},
|
|
},
|
|
entity_chance = 3,
|
|
entities = {
|
|
["__villager_carpenter"] = 1,
|
|
},
|
|
}
|
|
village.chunkdefs["reading_club"] = {
|
|
groundclass_variants = {
|
|
grassland = {"reading_club"},
|
|
dry = {"reading_club"},
|
|
savanna = {"reading_club"},
|
|
},
|
|
ruins = {"house_ruins", "house_ruins_2"},
|
|
entity_chance = 3,
|
|
entities = {
|
|
["__villager_farmer"] = 1,
|
|
["__villager_blacksmith"] = 1,
|
|
},
|
|
}
|
|
|
|
village.chunkdefs["bakery"] = {
|
|
groundclass_variants = {
|
|
grassland = {"bakery"},
|
|
dry = {"bakery"},
|
|
savanna = {"bakery"},
|
|
},
|
|
ruins = {"bakery_ruins"},
|
|
entity_chance = 2,
|
|
entities = {
|
|
["__villager_farmer"] = 1,
|
|
},
|
|
}
|
|
|
|
|
|
village.chunkdefs["forge"] = {
|
|
groundclass_variants = {
|
|
grassland = {"forge"},
|
|
dry = {"forge"},
|
|
savanna = {"forge"},
|
|
swamp = {"reed_forge"},
|
|
},
|
|
groundclass_ruins = {
|
|
grassland = {"forge_ruins", "rubble"},
|
|
dry = {"forge_ruins", "rubble"},
|
|
savanna = {"forge_ruins", "rubble"},
|
|
swamp = {"reed_forge_ruins"},
|
|
},
|
|
entity_chance = 2,
|
|
entities = {
|
|
["__villager_blacksmith"] = 1,
|
|
},
|
|
}
|
|
village.chunkdefs["orchard"] = {
|
|
groundclass_variants = {
|
|
["grassland"] = {"orchard"},
|
|
},
|
|
ruins = {"orchard_ragged"},
|
|
can_cache = true,
|
|
entity_chance = 2,
|
|
entities = {
|
|
["__villager_farmer"] = 1,
|
|
},
|
|
}
|
|
village.chunkdefs["road"] = {
|
|
can_cache = true,
|
|
}
|
|
|
|
-- Farm chunktypes.
|
|
--
|
|
-- Farm chunktype naming scheme:
|
|
--
|
|
-- farm_<water><lines>_<plants>
|
|
--
|
|
-- * <water>: water position:
|
|
-- * "v": vertical lines
|
|
-- * "h": horizontal lines
|
|
-- * "c": center
|
|
-- * "o": outwards
|
|
-- * <lines>:
|
|
-- * for v/h: list of numbers at where the water will be
|
|
-- * for c/o: how much water in total
|
|
-- * <plants>: List of plants (from left to right)
|
|
|
|
village.chunkdefs["farm_small_plants"] = {
|
|
groundclass_variants = {
|
|
["grassland"] = {
|
|
"farm_v24_potato",
|
|
"farm_v24_potato_wheat",
|
|
"farm_v24_wheat",
|
|
"farm_v24_wheat_cotton",
|
|
"farm_v24_cotton",
|
|
"farm_h246_potato",
|
|
"farm_h246_wheat",
|
|
"farm_h246_cotton",
|
|
},
|
|
["swamp"] = {
|
|
"farm_swamp_v24_asparagus",
|
|
"farm_swamp_h246_asparagus",
|
|
},
|
|
["savanna"] = {
|
|
"farm_dry_v24_cotton",
|
|
"farm_dry_h246_cotton",
|
|
},
|
|
["dry"] = {
|
|
"farm_dryd_v24_carrot",
|
|
"farm_dryd_h246_carrot",
|
|
},
|
|
},
|
|
entity_chance = 2,
|
|
entities = {
|
|
["__villager_farmer"] = 1,
|
|
}
|
|
}
|
|
|
|
village.chunkdefs["farm_papyrus"] = {
|
|
groundclass_variants = {
|
|
["grassland"] = {
|
|
"farm_c4_papyrus",
|
|
"farm_o4_papyrus",
|
|
},
|
|
["swamp"] = {
|
|
"farm_swamp_c4_papyrus",
|
|
"farm_swamp_o4_papyrus",
|
|
},
|
|
},
|
|
entity_chance = 2,
|
|
entities = {
|
|
["__villager_farmer"] = 1,
|
|
}
|
|
}
|
|
|
|
-- List of chunk types. Chunk types are structurs and buildings
|
|
-- that are not the well and are placed next to roads.
|
|
-- The number is their absolute frequency. The higher the number,
|
|
-- the more likely it will occur.
|
|
-- The well is not listed here because it acts as the start point.
|
|
village.chunktypes = {
|
|
-- { chunktype, absolute frequency }
|
|
|
|
-- houses
|
|
{ "house", 210 },
|
|
{ "hut_s", 105 },
|
|
{ "hut_m", 105 },
|
|
-- meeting rooms
|
|
{ "tavern", 120 },
|
|
{ "townhall", 60 },
|
|
{ "library", 20 },
|
|
{ "reading_club", 30 },
|
|
{ "inn", 20 },
|
|
-- workplaces
|
|
{ "forge", 100 },
|
|
{ "workshop", 100 },
|
|
{ "bakery", 60 },
|
|
-- farming
|
|
{ "farm_small_plants", 120 },
|
|
{ "farm_papyrus", 120 },
|
|
{ "orchard", 60 },
|
|
{ "livestock_pen", 60 },
|
|
}
|
|
|
|
-- List of chunktypes to be used as fallback for the starting
|
|
-- village chunk if the village failed to place any buildings
|
|
-- outside the starting point.
|
|
-- In this case, the "starter well" will be a house
|
|
-- instead. This will create nice "lonely huts".
|
|
village.chunktypes_start_fallback = {
|
|
-- chunktype, absolute frequency
|
|
{ "house", 14 },
|
|
{ "tavern", 7 },
|
|
{ "forge", 3 },
|
|
}
|
|
|
|
-- Calculate cumulated absolute frequency of a chunktypes
|
|
-- table and put it in index 3 of each entry. Puts the sum
|
|
-- of all absolute frequencies in `chunksum`.
|
|
local write_absolute_frequencies = function(chunktypes)
|
|
local chunksum = 0
|
|
for i=1, #chunktypes do
|
|
chunksum = chunksum + chunktypes[i][2]
|
|
chunktypes[i][3] = chunksum
|
|
end
|
|
chunktypes.chunksum = chunksum
|
|
end
|
|
write_absolute_frequencies(village.chunktypes)
|
|
write_absolute_frequencies(village.chunktypes_start_fallback)
|
|
|
|
-- Select a random chunk. The probability of a chunk being selected is
|
|
-- <absolute frequency> / <sum of all absolute frequencies>.
|
|
-- * `pr`: PseudoRandom object
|
|
-- * `chunktypes`: A table of chunktypes (see above) (default: `village.chunktypes`)
|
|
-- * `groundclass`: Restrict chunktypes to this ground class
|
|
local function random_chunktype(pr, chunktypes, groundclass)
|
|
if not chunktypes then
|
|
chunktypes = village.chunktypes
|
|
end
|
|
local check_chunktypes = table.copy(chunktypes)
|
|
while #check_chunktypes > 0 do
|
|
local rnd = pr:next(1, check_chunktypes.chunksum)
|
|
for i=1, #check_chunktypes do
|
|
if rnd <= check_chunktypes[i][3] then
|
|
local chunktype = check_chunktypes[i][1]
|
|
if groundclass and village.chunkdefs[chunktype].groundclass_variants and village.chunkdefs[chunktype].groundclass_variants[groundclass] == nil then
|
|
table.remove(check_chunktypes, i)
|
|
break
|
|
else
|
|
return chunktype
|
|
end
|
|
end
|
|
end
|
|
end
|
|
minetest.log("error", "[rp_village] random_chunktype: Failed to find a chunktype, using a fallback")
|
|
return "house" -- fallback
|
|
end
|
|
|
|
local function get_chunktype_variant(pr, chunktype, groundclass)
|
|
local ctd = village.chunkdefs[chunktype]
|
|
if not ctd then
|
|
return chunktype
|
|
end
|
|
if ctd.variants then
|
|
return ctd.variants[pr:next(1, #ctd.variants)]
|
|
elseif groundclass and ctd.groundclass_variants and ctd.groundclass_variants[groundclass] then
|
|
return ctd.groundclass_variants[groundclass][pr:next(1, #ctd.groundclass_variants[groundclass])]
|
|
else
|
|
return chunktype
|
|
end
|
|
end
|
|
|
|
-- Given a chunktype, returns a random 'ruins' version
|
|
-- for that chunktype if one is available. Otherwise,
|
|
-- returns `chunktype`.
|
|
-- * `pr`: PseudoRandom object
|
|
-- * `chunktype`: Chunktype identifier
|
|
-- * `groundclass`: Restrict chunktypes to this ground class
|
|
local function get_ruined_chunktype(pr, chunktype, groundclass)
|
|
local ctd = village.chunkdefs[chunktype]
|
|
if not ctd then
|
|
return chunktype
|
|
end
|
|
if ctd.ruins then
|
|
return ctd.ruins[pr:next(1, #ctd.ruins)]
|
|
elseif groundclass and ctd.groundclass_ruins and ctd.groundclass_ruins[groundclass] then
|
|
return ctd.groundclass_ruins[groundclass][pr:next(1, #ctd.groundclass_ruins[groundclass])]
|
|
else
|
|
return get_chunktype_variant(pr, chunktype, groundclass)
|
|
end
|
|
end
|
|
|
|
local function check_column_end(nn)
|
|
local nd = minetest.registered_nodes[nn]
|
|
return (not nd) or nn == "ignore" or (not (nn == "air" or (not nd.walkable) or ((minetest.get_item_group(nn, "dirt") > 0) and minetest.get_item_group(nn, "grass_cover") > 0)))
|
|
end
|
|
|
|
function village.get_column_nodes(vmanip, pos, scanheight, dirtnodes)
|
|
local nn = vmanip:get_node_at({x=pos.x,y=pos.y+1,z=pos.z}).name
|
|
if check_column_end(nn) then
|
|
return
|
|
end
|
|
|
|
for y = pos.y, pos.y - scanheight, -1 do
|
|
local p = {x = pos.x, y = y, z = pos.z}
|
|
|
|
nn = vmanip:get_node_at(p).name
|
|
if check_column_end(nn) then
|
|
break
|
|
else
|
|
table.insert(dirtnodes, p)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Generate a hill.
|
|
--
|
|
-- * vmanip: VoxelMapnip object
|
|
-- * vdata: VoxelManip data table
|
|
-- * pos: Hill position
|
|
-- * ground: Ground nodename (below surface)
|
|
-- * ground_top: Ground nodename (surface)
|
|
-- * top_decors: Optional table of possible decorations to place on top of ground_top
|
|
-- * decors_to_place: Table in which positions of decor nodes will be stored (call-by-reference)
|
|
-- Must be provided if `top_decors` is set
|
|
function village.generate_hill(vmanip, vdata, pos, ground, ground_top, top_decors, decors_to_place)
|
|
local c_ground = minetest.get_content_id(ground)
|
|
local c_ground_top = minetest.get_content_id(ground_top)
|
|
local c_decors = {}
|
|
local seed = 13 + minetest.hash_node_position(pos) + mapseed
|
|
local decor_pr = PcgRandom(seed)
|
|
if top_decors then
|
|
for d=1, #top_decors do
|
|
c_decors[d] = minetest.get_content_id(top_decors[d])
|
|
end
|
|
end
|
|
local dirts = {}
|
|
local dirts_with_grass = {}
|
|
local vmin, vmax = vmanip:get_emerged_area()
|
|
local varea = VoxelArea:new({MinEdge = vmin, MaxEdge = vmax})
|
|
for y=HILL_H-1, 0, -1 do
|
|
-- Count number of nodes that were actually changed on this layer
|
|
local nodes_set = 0
|
|
for z=y,HILL_W-1-y do
|
|
for x=y,HILL_W-1-y do
|
|
local p = {x=pos.x+x, y=pos.y+y, z=pos.z+z}
|
|
local vindex = varea:index(p.x,p.y,p.z)
|
|
local vindex_above = varea:index(p.x,p.y+1,p.z)
|
|
local n_content = vdata[vindex]
|
|
if n_content then
|
|
local nname = minetest.get_name_from_content_id(n_content)
|
|
local def = minetest.registered_nodes[nname]
|
|
local is_any_dirt = minetest.get_item_group(nname, "dirt") == 1
|
|
local is_dirt = nname == "rp_default:dirt" or nname == "rp_default:swamp_dirt"
|
|
local is_dry_dirt = nname == "rp_default:dry_dirt"
|
|
if (not is_dry_dirt) and (is_dirt or (not is_any_dirt)) and (nname == "air" or nname == "ignore" or (def and (def.liquidtype ~= "none" or (def.is_ground_content)))) then
|
|
local prev_was_ground = n_content == c_ground or n_content == c_ground_top
|
|
if (y == HILL_H-1 or z == y or x == y or z == HILL_W-1-y or x == HILL_W-1-y) and (p.y >= water_level) then
|
|
-- set surface node (e.g. dirt-with-grass)
|
|
vdata[vindex] = c_ground_top
|
|
else
|
|
-- set 'below ground' node (e.g. dirt)
|
|
vdata[vindex] = c_ground
|
|
end
|
|
if not prev_was_ground then
|
|
nodes_set = nodes_set + 1
|
|
end
|
|
end
|
|
-- chance to spawn a decor node (like grass) above ground_top
|
|
local vindex_above = varea:index(p.x,p.y+1,p.z)
|
|
if top_decors and #c_decors > 0 and vdata[vindex] == c_ground_top and vdata[vindex_above] == minetest.CONTENT_AIR and decor_pr:next(1,DECOR_CHANCE) == 1 then
|
|
local decor = c_decors[decor_pr:next(1, #c_decors)]
|
|
-- Don't place the decor immediately, instead remember this position and decor nodename
|
|
-- and place all decorations at the end. This makes it easier to avoid conflicts
|
|
-- with the rest of the generation algorithm.
|
|
table.insert(decors_to_place, {
|
|
-- VManip data index of decor position
|
|
index_decor = vindex_above,
|
|
-- content ID of decor node
|
|
content_decor = c_decors[decor_pr:next(1, #c_decors)],
|
|
-- VManip data index of floor position (on which decor will be placed)
|
|
index_floor = vindex,
|
|
-- content ID of floor node
|
|
content_floor = vdata[vindex],
|
|
})
|
|
-- decors_to_place is call-by-reference, the caller can use this table afterwards
|
|
end
|
|
end
|
|
end
|
|
end
|
|
-- Stop hill generation if no nodes were changed in this layer,
|
|
-- because the building already has a foundation.
|
|
if nodes_set == 0 then
|
|
-- Partial / no hill generated (because not neccessary)
|
|
return false
|
|
end
|
|
end
|
|
-- Full hill generated
|
|
return true
|
|
end
|
|
|
|
local function check_empty(pos)
|
|
local min = { x = pos.x, y = pos.y + 1, z = pos.z }
|
|
local max = { x = pos.x+12, y = pos.y+12, z = pos.z+12 }
|
|
local ignores = minetest.find_nodes_in_area(min, max, "ignore")
|
|
-- Treat an area of ignore nodes as non-empty (we err on the side of caution)
|
|
if #ignores > 0 then
|
|
minetest.log("action", "[rp_village] check_empty: Ignore found! pos="..minetest.pos_to_string(pos, 0).."; number of ignores="..(#ignores))
|
|
return false
|
|
end
|
|
local stones = minetest.find_nodes_in_area(min, max, "group:stone")
|
|
if #stones > 15 then
|
|
return false
|
|
end
|
|
local leaves = minetest.find_nodes_in_area(min, max, "group:leaves")
|
|
if #leaves > 2 then
|
|
return false
|
|
end
|
|
local trees = minetest.find_nodes_in_area(min, max, "group:tree")
|
|
if #trees > 0 then
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- Map ground nodes with appropiate decor nodes to place on top
|
|
-- (e.g. grass)
|
|
-- Decors for normal villages
|
|
local decors_from_ground = {
|
|
["rp_default:dirt_with_grass"] = { "rp_default:grass" },
|
|
["rp_default:dirt_with_dry_grass"] = { "rp_default:dry_grass" },
|
|
["rp_default:dirt_with_swamp_grass"] = { "rp_default:swamp_grass" },
|
|
}
|
|
-- Decors for abandoned villages
|
|
local decors_from_ground_abandoned = table.copy(decors_from_ground)
|
|
-- Same as normal villages, except there's also tall grass
|
|
decors_from_ground_abandoned["rp_default:dirt_with_grass"] =
|
|
{"rp_default:grass", "rp_default:grass", "rp_default:grass", "rp_default:tall_grass"}
|
|
|
|
-- Spawns a village chunk. This is a section of a village.
|
|
-- By default, this checks for empty space first (fails if no space),
|
|
-- then it generates a foundation of ground nodes, then it deletes
|
|
-- nodes above, then places the building as specified in chunktype.
|
|
--
|
|
-- Parameters:
|
|
-- * vmanip: VoxelManip object
|
|
-- * pos: pos to spawn chunk in
|
|
-- * state: table for internal state (call-by-reference)
|
|
-- * orient: orientation (for minetest.place_schematic)
|
|
-- * replace: one of these:
|
|
-- * node replacements table (for minetest.place_schematic)
|
|
-- * number of village replacements ID (from village_replacements table)
|
|
-- * pr: PseudoRandom object for random stuff
|
|
-- * chunktype: village chunk type ID
|
|
-- * noclear: If true, won't delete nodes before spawning
|
|
-- * nofill: If true, won't build a dirt foundation
|
|
-- * dont_check_empty: If true, don't fail if there is no empty space
|
|
-- * ground: ground node below surface
|
|
-- * ground_top: ground node on surface
|
|
--
|
|
-- returns true if chunk was placed, false otherwise.
|
|
function village.spawn_chunk(vmanip, pos, state, orient, replace, pr, chunktype, noclear, nofill, dont_check_empty, ground, ground_top)
|
|
if not dont_check_empty and not check_empty(pos) then
|
|
minetest.log("verbose", "[rp_village] Chunk not generated (too many stone/leaves/trees in the way) at "..minetest.pos_to_string(pos))
|
|
return false
|
|
end
|
|
|
|
if noclear ~= true then
|
|
local ok = minetest.place_schematic_on_vmanip(
|
|
vmanip,
|
|
pos,
|
|
modpath .. "/schematics/village_empty.mts",
|
|
"0",
|
|
{},
|
|
true
|
|
)
|
|
if not ok then
|
|
minetest.log("warning", "[rp_village] Could not fully place empty schematic in village at "..minetest.pos_to_string(pos, 0))
|
|
end
|
|
end
|
|
|
|
if nofill ~= true then
|
|
vmanip:get_data(vdata_spawn_chunk)
|
|
-- Make a hill for the buildings to stand on
|
|
local decors
|
|
if state.is_abandoned then
|
|
decors = decors_from_ground_abandoned[ground_top] or {}
|
|
else
|
|
decors = decors_from_ground[ground_top] or {}
|
|
end
|
|
if not state.decors_to_place then
|
|
state.decors_to_place = {}
|
|
end
|
|
local full_hill = village.generate_hill(vmanip, vdata_spawn_chunk, {x=pos.x-6, y=pos.y-5, z=pos.z-6}, ground, ground_top, decors, state.decors_to_place)
|
|
|
|
if full_hill then
|
|
-- Extend the dirt below the hill, in case the hill is floating
|
|
-- in mid-air
|
|
local py = pos.y-6
|
|
local dirtnodes = {}
|
|
local vmin, vmax = vmanip:get_emerged_area()
|
|
local varea = VoxelArea:new({MinEdge=vmin, MaxEdge=vmax})
|
|
local c_ground = minetest.get_content_id(ground)
|
|
for z=pos.z-6, pos.z+17 do
|
|
for x=pos.x-6, pos.x+17 do
|
|
village.get_column_nodes(vmanip, {x=x, y=py, z=z}, HILL_EXTEND_BELOW, dirtnodes)
|
|
for d=1, #dirtnodes do
|
|
local vindex = varea:index(dirtnodes[d].x, dirtnodes[d].y, dirtnodes[d].z)
|
|
vdata_spawn_chunk[vindex] = c_ground
|
|
end
|
|
end
|
|
end
|
|
end
|
|
vmanip:set_data(vdata_spawn_chunk)
|
|
end
|
|
|
|
if type(replace) == "number" then
|
|
replace = village_replaces[replace]
|
|
end
|
|
local sreplace = table.copy(replace)
|
|
if chunktype == "orchard" or chunktype == "orchard_ragged" then
|
|
sreplace["rp_default:tree"] = nil
|
|
end
|
|
|
|
-- Select random variant (ruins or normal) for schematic name
|
|
local schem_segment = chunktype
|
|
if state.is_abandoned and pr:next(1, ABANDONED_RUINS_CHANCE) == 1 then
|
|
schem_segment = get_ruined_chunktype(pr, chunktype, state.groundclass)
|
|
else
|
|
schem_segment = get_chunktype_variant(pr, chunktype, state.groundclass)
|
|
end
|
|
|
|
local schem_spec
|
|
if village.chunkdefs[chunktype] and village.chunkdefs[chunktype].can_cache then
|
|
-- Luanti's caching is allowed for this chunktype, so we call the schematic place function
|
|
-- in the normal way (schematics are cached by Luanti if the schematic path is
|
|
-- specified in the place function)
|
|
schem_spec = modpath .. "/schematics/village_" .. schem_segment .. ".mts"
|
|
else
|
|
-- load schematic from table definition (read_schematic). This will force Luanti
|
|
-- to skip its schematic cache and guarantee that node replacements are
|
|
-- applied every time.
|
|
-- However, this mod still caches the result of read_schematic itself to save
|
|
-- a bit of time.
|
|
local cached
|
|
schem_spec, cached = read_cached_chunk_schematic(schem_segment)
|
|
end
|
|
local ok = minetest.place_schematic_on_vmanip(
|
|
vmanip,
|
|
pos,
|
|
schem_spec,
|
|
orient,
|
|
sreplace,
|
|
true
|
|
)
|
|
if not ok then
|
|
minetest.log("warning", "[rp_village] Could not fully place village chunk in village at "..minetest.pos_to_string(pos, 0))
|
|
end
|
|
|
|
if not state.nodeupdates then
|
|
state.nodeupdates = {}
|
|
end
|
|
table.insert(state.nodeupdates, {pos=pos, chunktype=chunktype})
|
|
|
|
minetest.log("verbose", "[rp_village] Chunk generated at "..minetest.pos_to_string(pos))
|
|
return true
|
|
end
|
|
|
|
function village.spawn_road(vmanip, pos, state, houses, built, roads, depth, pr, replace, dont_check_empty, dist_from_start, ground, ground_top)
|
|
if not dont_check_empty and not check_empty(pos) then
|
|
minetest.log("verbose", "[rp_village] Road not generated (too many stone/leaves/trees in the way) at "..minetest.pos_to_string(pos))
|
|
return false
|
|
end
|
|
|
|
for i=1,4 do
|
|
local nextpos = {x = pos.x, y = pos.y, z = pos.z}
|
|
local orient = "random"
|
|
|
|
local new_dist_from_start = vector.new(dist_from_start.x, dist_from_start.y, dist_from_start.z)
|
|
if i == 1 then
|
|
orient = "0"
|
|
new_dist_from_start.z = new_dist_from_start.z - 1
|
|
nextpos.z = nextpos.z - 12
|
|
elseif i == 2 then
|
|
orient = "90"
|
|
new_dist_from_start.x = new_dist_from_start.x - 1
|
|
nextpos.x = nextpos.x - 12
|
|
elseif i == 3 then
|
|
orient = "180"
|
|
new_dist_from_start.z = new_dist_from_start.z + 1
|
|
nextpos.z = nextpos.z + 12
|
|
else
|
|
orient = "270"
|
|
new_dist_from_start.x = new_dist_from_start.x + 1
|
|
nextpos.x = nextpos.x + 12
|
|
end
|
|
|
|
local hnp = minetest.hash_node_position(nextpos)
|
|
|
|
local chunk_ok
|
|
if built[hnp] == nil then
|
|
built[hnp] = true
|
|
|
|
-- True is the next position is at or beyond the maximum village boundaries.
|
|
-- This will ensure the village does not spread too far from the starting
|
|
-- point.
|
|
local is_at_village_border = math.abs(new_dist_from_start.x) >= village.max_village_spread or math.abs(new_dist_from_start.z) >= village.max_village_spread
|
|
if is_at_village_border then
|
|
minetest.log("verbose", "[rp_village] Border hit at "..minetest.pos_to_string(nextpos).." "..minetest.pos_to_string(new_dist_from_start))
|
|
end
|
|
|
|
if depth <= 0 or is_at_village_border or pr:next(1, 8) < 6 then
|
|
houses[hnp] = {pos = nextpos, front = pos}
|
|
|
|
local structure = random_chunktype(pr, nil, state.groundclass)
|
|
chunk_ok = village.spawn_chunk(vmanip, nextpos, state, orient, replace, pr, structure, nil, nil, nil, ground, ground_top)
|
|
if not chunk_ok then
|
|
houses[hnp] = false
|
|
end
|
|
else
|
|
roads[hnp] = {pos = nextpos}
|
|
chunk_ok = village.spawn_road(vmanip, nextpos, state, houses, built, roads, depth - 1, pr, replace, false, new_dist_from_start, ground, ground_top)
|
|
if not chunk_ok then
|
|
roads[hnp] = false
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- Village modifiy functions: These are called after the VManip has placed
|
|
-- the village for further changes like setting metadata or tweak
|
|
-- nodes.
|
|
--
|
|
-- Parameters for all village_modify_* functions:
|
|
-- * upos, upos2: Lower and upper bounds of the village
|
|
-- * pr: PseudoRandom object used for randomness
|
|
-- * extras: Table with extra infos (function-specific, not always used)
|
|
|
|
-- Village modifier: Abandoned village. A complex modifier that
|
|
-- makes a village look like it was abandoned. It does these things:
|
|
-- * Turns all torches into dead torches
|
|
-- * Removes music players
|
|
-- * Makes grass overgrow on floor
|
|
-- * Randomly destroys farming plants, fences, glass, doors
|
|
-- * Generates seagrass and algae in water
|
|
--
|
|
-- The `extras` parameter must specify:
|
|
-- {
|
|
-- path = <itemname of path node>,
|
|
-- path_slab = <itemname of path node slab>,
|
|
-- ground_top = <itemname of top surface node outdoors (e.g. rp_default:dirt_with_grass>>,
|
|
-- }
|
|
local function village_modify_abandoned_village(upos, upos2, pr, extras)
|
|
-- Replace all torches with dead torches
|
|
util.nodefunc(
|
|
upos, upos2,
|
|
{"rp_default:torch", "rp_default:torch_weak"},
|
|
function(pos)
|
|
local node = minetest.get_node(pos)
|
|
minetest.set_node(pos, {name="rp_default:torch_dead", param2=node.param2})
|
|
end, true)
|
|
util.nodefunc(
|
|
upos, upos2,
|
|
{"rp_default:torch_wall", "rp_default:torch_weak_wall"},
|
|
function(pos)
|
|
local node = minetest.get_node(pos)
|
|
minetest.set_node(pos, {name="rp_default:torch_dead_wall", param2=node.param2})
|
|
end, true)
|
|
|
|
-- Remove all music players
|
|
util.nodefunc(
|
|
upos, upos2,
|
|
"rp_music:player",
|
|
function(pos)
|
|
minetest.remove_node(pos)
|
|
end, true)
|
|
|
|
-- Remove 95% of farming plants
|
|
util.nodefunc(
|
|
upos, upos2,
|
|
"group:farming_plant",
|
|
function(pos)
|
|
if pr:next(1,100) <= 95 then
|
|
-- 30% chance to replace with a decor (grass), if the ground type allows it
|
|
if pr:next(1,10) <= 3 then
|
|
local below = vector.add(pos, vector.new(0,-1,0))
|
|
local belownode = minetest.get_node(below)
|
|
local decors = decors_from_ground_abandoned[belownode.name]
|
|
if decors then
|
|
local plant = decors[pr:next(1, #decors)]
|
|
minetest.set_node(pos, {name=plant})
|
|
else
|
|
minetest.remove_node(pos)
|
|
end
|
|
else
|
|
minetest.remove_node(pos)
|
|
end
|
|
end
|
|
end, true)
|
|
|
|
-- Remove 80% of glass
|
|
util.nodefunc(
|
|
upos, upos2,
|
|
"group:glass",
|
|
function(pos)
|
|
if pr:next(1,5) >= 4 then
|
|
minetest.remove_node(pos)
|
|
end
|
|
end, true)
|
|
|
|
-- Replace 25% of path nodes
|
|
util.nodefunc(
|
|
upos, upos2,
|
|
{extras.path},
|
|
function(pos)
|
|
if pr:next(1,4) == 1 then
|
|
minetest.set_node(pos, {name=extras.ground_top})
|
|
local above = {x=pos.x,y=pos.y+1,z=pos.z}
|
|
local abovenode = minetest.get_node(above)
|
|
if abovenode.name == "air" and pr:next(1,DECOR_CHANCE) == 1 then
|
|
local decors = decors_from_ground_abandoned[extras.ground_top]
|
|
if decors then
|
|
local decor = decors[pr:next(1, #decors)]
|
|
minetest.set_node(above, {name=decor})
|
|
end
|
|
end
|
|
end
|
|
end, true)
|
|
-- Remove 25% of path slab nodes
|
|
util.nodefunc(
|
|
upos, upos2,
|
|
{extras.path_slab},
|
|
function(pos)
|
|
if pr:next(1,4) == 1 then
|
|
minetest.remove_node(pos)
|
|
local below = vector.add(pos, vector.new(0,-1,0))
|
|
if minetest.get_node(below).name == "rp_default:dirt" then
|
|
if pr:next(1,3) == 1 then
|
|
minetest.set_node(below, {name=extras.path})
|
|
else
|
|
minetest.set_node(below, {name=extras.ground_top})
|
|
end
|
|
end
|
|
end
|
|
end, true)
|
|
|
|
-- Replace 25% of brick/cobble floor with ground
|
|
util.nodefunc(
|
|
upos, upos2,
|
|
{"rp_default:cobble", "rp_default:brick"},
|
|
function(pos)
|
|
if pr:next(1,4) == 1 then
|
|
local below = vector.add(pos, vector.new(0,-1,0))
|
|
local above = vector.add(pos, vector.new(0,1,0))
|
|
local nbelow = minetest.get_node(below)
|
|
local nabove = minetest.get_node(above)
|
|
if nabove.name == "air" and (nbelow.name == "rp_default:dirt" or nbelow.name == "rp_default:stone") then
|
|
minetest.set_node(pos, {name=extras.ground_top})
|
|
local plant = pr:next(1,5)
|
|
if plant == 1 then
|
|
minetest.set_node(above, {name="rp_default:grass"})
|
|
end
|
|
end
|
|
end
|
|
end, true)
|
|
|
|
-- Remove 50% of doors
|
|
util.nodefunc(
|
|
upos, upos2,
|
|
"group:door",
|
|
function(pos)
|
|
if pr:next(1,2) == 1 then
|
|
local posup = vector.add(pos, vector.new(0,1,0))
|
|
local posdn = vector.add(pos, vector.new(0,-1,0))
|
|
|
|
local nup = minetest.get_node(posup)
|
|
local ndn = minetest.get_node(posdn)
|
|
if minetest.get_item_group(ndn.name, "door") == 1 then
|
|
return
|
|
end
|
|
minetest.remove_node(pos)
|
|
if minetest.get_item_group(nup.name, "door") == 1 then
|
|
minetest.remove_node(posup)
|
|
end
|
|
end
|
|
end, true)
|
|
|
|
-- Remove 10% of fences
|
|
util.nodefunc(
|
|
upos, upos2,
|
|
"group:fence",
|
|
function(pos)
|
|
if pr:next(1,10) == 1 then
|
|
local posup = vector.add(pos, vector.new(0,1,0))
|
|
local posdn = vector.add(pos, vector.new(0,-1,0))
|
|
local nup = minetest.get_node(posup)
|
|
local ndn = minetest.get_node(posdn)
|
|
-- make sure only fences on floor and below air are removed so we don't
|
|
-- leave floating fences behind
|
|
if nup.name == "air" and minetest.get_item_group(ndn.name, "group:fence") == 0 then
|
|
minetest.remove_node(pos)
|
|
end
|
|
end
|
|
end, true)
|
|
|
|
-- Place seagrass or alga underwater
|
|
util.nodefunc(
|
|
upos, upos2,
|
|
{"rp_default:water_source", "rp_default:swamp_water_source"},
|
|
function(pos)
|
|
if pr:next(1,2) == 1 then
|
|
local posdn = vector.add(pos, vector.new(0,-1,0))
|
|
local posup = vector.add(pos, vector.new(0,1,0))
|
|
local ndn = minetest.get_node(posdn)
|
|
local nup = minetest.get_node(posup)
|
|
-- Alga may replaces seagrass if water is at least 2 nodes deep and if we're VERY lucky
|
|
local alga = pr:next(1,100) == 1 and minetest.get_item_group(nup.name, "water") ~= 0
|
|
local plant, p2
|
|
if alga then
|
|
plant = "alga"
|
|
p2 = 16
|
|
else
|
|
plant = "seagrass"
|
|
p2 = 0
|
|
end
|
|
if ndn.name == "rp_default:dirt" or ndn.name == "rp_default:dirt_with_grass" or ndn.name == "rp_default:dirt_with_dry_grass" then
|
|
minetest.set_node(posdn, {name="rp_default:"..plant.."_on_dirt", param2=p2})
|
|
elseif ndn.name == "rp_default:swamp_dirt" or ndn.name == "rp_default:dirt_with_swamp_grass" then
|
|
minetest.set_node(posdn, {name="rp_default:"..plant.."_on_swamp_dirt", param2=p2})
|
|
end
|
|
end
|
|
end, true)
|
|
end
|
|
|
|
-- Village modifier: Fills containers with goodies
|
|
local function village_modify_populate_containers(upos, upos2, pr, extras)
|
|
-- Populate chests
|
|
-- TODO: Damaged tools in abandoned villages
|
|
util.nodefunc(
|
|
upos, upos2,
|
|
{"rp_default:chest", "rp_locks:chest"},
|
|
function(pos)
|
|
goodies.fill(pos, extras.chunktype, pr, "main", 3)
|
|
end, true)
|
|
|
|
-- Populate bookshelves
|
|
util.nodefunc(
|
|
upos, upos2,
|
|
{"rp_default:bookshelf"},
|
|
function(pos)
|
|
goodies.fill(pos, "BOOKSHELF", pr, "main", 1)
|
|
end, true)
|
|
|
|
-- Populate furnaces
|
|
if extras.chunktype == "forge" or extras.chunktype == "bakery" then
|
|
local g_src, g_fuel, g_dst
|
|
if extras.chunktype == "bakery" then
|
|
g_src = "FURNACE_SRC_bakery"
|
|
g_fuel = "FURNACE_FUEL_bakery"
|
|
g_dst = "FURNACE_DST_bakery"
|
|
else
|
|
g_src = "FURNACE_SRC_general"
|
|
g_fuel = "FURNACE_FUEL_general"
|
|
g_dst = "FURNACE_DST_general"
|
|
end
|
|
util.nodefunc(
|
|
upos, upos2,
|
|
"rp_default:furnace",
|
|
function(pos)
|
|
goodies.fill(pos, g_src, pr, "src", 1)
|
|
goodies.fill(pos, g_fuel, pr, "fuel", 1)
|
|
goodies.fill(pos, g_dst, pr, "dst", 1)
|
|
-- If both the src and fuel slots have an item,
|
|
-- simulate the cooking process.
|
|
-- We convert the src item into its cooked version,,,,
|
|
-- put it into dst and reduce the fuel itemstack by 1.
|
|
-- This prevents the furnace from going into
|
|
-- active state when the village generates.
|
|
local inv = minetest.get_meta(pos):get_inventory()
|
|
local src = inv:get_stack("src", 1)
|
|
local fuel = inv:get_stack("fuel", 1)
|
|
if not src:is_empty() and not fuel:is_empty() then
|
|
local output = minetest.get_craft_result({method="cooking", items={src:get_name()}, width=1})
|
|
if output and not output.item:is_empty() then
|
|
local cooked = output.item
|
|
cooked:set_count(src:get_count()*cooked:get_count())
|
|
if cooked:get_count() > cooked:get_stack_max() then
|
|
cooked:set_count(cooked:set_stack_max())
|
|
end
|
|
inv:set_stack("src", 1, "")
|
|
inv:add_item("dst", cooked)
|
|
fuel:set_count(fuel:get_count()-1)
|
|
inv:set_stack("fuel", 1, fuel)
|
|
end
|
|
end
|
|
end, true)
|
|
end
|
|
end
|
|
|
|
-- Village modifier: Limit number of music players in village to 1.
|
|
-- Also set random color for the remaning music player.
|
|
local function village_modify_limit_music_players(upos, upos2, pr)
|
|
-- Maximum of 1 music player per village; remove excess music players
|
|
local music_players = 0
|
|
util.nodefunc(
|
|
upos, upos2,
|
|
"rp_music:player",
|
|
function(pos)
|
|
if music_players >= 1 or pr:next(1,8) > 1 then
|
|
minetest.remove_node(pos)
|
|
else
|
|
music_players = music_players + 1
|
|
|
|
-- Also initialize music box with random color
|
|
local color = math.random(1, rp_paint.COLOR_COUNT)
|
|
minetest.swap_node(pos, {name="rp_music:player", param2 = color-1})
|
|
local meta = minetest.get_meta(pos)
|
|
meta:set_int("music_player_legacy_color", 1)
|
|
end
|
|
end, true)
|
|
end
|
|
|
|
-- Village modifier: Random bed color
|
|
local function village_modify_bed_colors(upos, upos2, pr)
|
|
if not mod_paint then
|
|
return
|
|
end
|
|
util.nodefunc(
|
|
upos, upos2,
|
|
"rp_bed:bed_foot",
|
|
function(pos)
|
|
local node = minetest.get_node(pos)
|
|
local dir = minetest.fourdir_to_dir(node.param2)
|
|
local param2 = node.param2 % 4 + math.random(0, rp_paint.COLOR_COUNT-1)*4
|
|
node.param2 = param2
|
|
minetest.swap_node(pos, node)
|
|
|
|
local pos2 = vector.add(pos, dir)
|
|
local node2 = minetest.get_node(pos2)
|
|
node2.param2 = param2
|
|
if node2.name == "rp_bed:bed_head" then
|
|
minetest.swap_node(pos2, node2)
|
|
end
|
|
end, true)
|
|
end
|
|
|
|
-- Village modifier: Randomly turn some chests into locked chests
|
|
local function village_modify_lock_chests(upos, upos2, pr)
|
|
-- Replace 25% of chests with locked chests
|
|
if mod_locks then
|
|
util.nodefunc(
|
|
upos, upos2,
|
|
"rp_default:chest",
|
|
function(pos)
|
|
if pr:next(1,4) == 1 then
|
|
local node = minetest.get_node(pos)
|
|
node.name = "rp_locks:chest"
|
|
minetest.swap_node(pos, node)
|
|
end
|
|
end, true)
|
|
end
|
|
end
|
|
|
|
-- Village modifier: Inizialize doors
|
|
local function village_modify_init_doors(upos, upos2, pr)
|
|
util.nodefunc(
|
|
upos, upos2,
|
|
"group:door",
|
|
function(pos)
|
|
-- The `is_open` parameter is false because we assume all
|
|
-- doors in the village chunks are closed
|
|
door.init_segment(pos, false)
|
|
end, true)
|
|
end
|
|
|
|
local function after_village_area_emerged(blockpos, action, calls_remaining, params)
|
|
local done = action == minetest.EMERGE_GENERATED or action == minetest.EMERGE_FROM_DISK or action == minetest.EMERGE_FROM_MEMORY
|
|
if not done or calls_remaining > 0 then
|
|
return
|
|
end
|
|
local vmanip = VoxelManip(params.emin, params.emax)
|
|
local vmin, vmax = vmanip:get_emerged_area()
|
|
local varea = VoxelArea:new({MinEdge=vmin, MaxEdge=vmax})
|
|
local pos = params.pos
|
|
local poshash = minetest.hash_node_position(pos)
|
|
local pr = params.pr
|
|
local ground = params.ground
|
|
local ground_top = params.ground_top
|
|
local force_place_starter = params.force_place_starter
|
|
local is_abandoned = params.is_abandoned == true
|
|
local village_name = params.village_name
|
|
|
|
minetest.log("info", "[rp_village] Village area emerged at startpos = "..minetest.pos_to_string(pos))
|
|
|
|
local depth = pr:next(village.min_size, village.max_size)
|
|
|
|
local houses = {}
|
|
local built = {}
|
|
local roads = {}
|
|
local state = {}
|
|
|
|
state.is_abandoned = is_abandoned
|
|
state.groundclass = "grassland"
|
|
if ground_top == "rp_default:dirt_with_swamp_grass" or ground_top == "rp_default:swamp_dirt" then
|
|
state.groundclass = "swamp"
|
|
elseif ground_top == "rp_default:dirt_with_dry_grass" then
|
|
state.groundclass = "savanna"
|
|
elseif ground_top == "rp_default:dry_dirt" then
|
|
state.groundclass = "dry"
|
|
end
|
|
|
|
local spawnpos = pos
|
|
|
|
-- Get village wood type for this village
|
|
local vpr = PcgRandom(mapseed + poshash)
|
|
local village_replace_id
|
|
if state.groundclass == "swamp" then
|
|
-- swamp village: always oak wood
|
|
village_replace_id = VILLAGE_REPLACE_OAK
|
|
else
|
|
-- other villages: random
|
|
village_replace_id = vpr:next(1,#village_replaces)
|
|
end
|
|
minetest.log("verbose", "[rp_village] village_replace_id="..village_replace_id)
|
|
local replace = village_replaces[village_replace_id]
|
|
local dirt_path = "rp_default:dirt_path"
|
|
local dirt_path_slab = "rp_default:path_slab"
|
|
|
|
-- For measuring the generation time
|
|
local t1 = os.clock()
|
|
|
|
built[poshash] = true
|
|
|
|
-- Generate a road below the starting position. The road tries to grow in 4 directions
|
|
-- growing either recursively more roads or buildings (where the road
|
|
-- terminates)
|
|
village.spawn_road(vmanip, pos, state, houses, built, roads, depth, pr, replace, true, vector.zero(), ground, ground_top)
|
|
|
|
local function connects(pos, nextpos)
|
|
local hnp = minetest.hash_node_position(nextpos)
|
|
|
|
if houses[hnp] ~= nil and houses[hnp] ~= false then
|
|
if vector.equals(houses[hnp].front, pos) then
|
|
return true
|
|
end
|
|
end
|
|
|
|
if roads[hnp] ~= nil and roads[hnp] ~= false then
|
|
return true
|
|
end
|
|
|
|
if vector.equals(pos, nextpos) or vector.equals(nextpos, spawnpos) then
|
|
return true
|
|
end
|
|
end
|
|
|
|
-- Add position of starter chunk to roads list to connect it properly with
|
|
-- the road network.
|
|
roads[poshash] = { pos = pos, is_starter = true }
|
|
|
|
-- Connect dirt paths with other village tiles.
|
|
-- The road schematic uses planks and cobble for each of the 4 cardinal
|
|
-- directions and it will be replaced either with a dirt path or
|
|
-- the ground.
|
|
|
|
local c_path = minetest.get_content_id(dirt_path)
|
|
local c_ground_top = minetest.get_content_id(ground_top)
|
|
|
|
-- Generate road center tiles
|
|
for _,road in pairs(roads) do
|
|
-- No road center tile for starter chunk since we expect it to occupy the center
|
|
if road ~= false and not road.is_starter then
|
|
-- This only places the center of the road, the connections will be manually placed
|
|
village.spawn_chunk(vmanip, road.pos, state, "0", {}, pr, "road", false, false, true, ground, ground_top)
|
|
end
|
|
end
|
|
|
|
-- Iterate through the road tiles and determine where to place dirt path nodes and lamps
|
|
|
|
-- Lamp positions
|
|
local lamps = {}
|
|
-- Store positions of nodes to replace, they will be set after the last village chunk
|
|
-- was generated
|
|
local road_bulk_set = {}
|
|
|
|
for _,road in pairs(roads) do
|
|
if road ~= false then
|
|
local amt_connections = 0
|
|
local all_nodes = {}
|
|
for i = 1, 4 do
|
|
local nextpos = {x = road.pos.x, y = road.pos.y, z = road.pos.z}
|
|
|
|
if i == 1 then -- North (planks)
|
|
nextpos.z = nextpos.z + 12
|
|
if connects(road.pos, nextpos) then
|
|
amt_connections = amt_connections + 1
|
|
table.insert(road_bulk_set, {vector.add(road.pos, {x=4, y=0, z=8}), vector.add(road.pos, {x=7,y=0,z=11}), c_path})
|
|
end
|
|
elseif i == 2 then -- East (cobble)
|
|
nextpos.x = nextpos.x + 12
|
|
if connects(road.pos, nextpos) then
|
|
amt_connections = amt_connections + 1
|
|
table.insert(road_bulk_set, {vector.add(road.pos, {x=8, y=0, z=4}), vector.add(road.pos, {x=11, y=0, z=7}), c_path})
|
|
end
|
|
elseif i == 3 then -- South (oak planks)
|
|
nextpos.z = nextpos.z - 12
|
|
if connects(road.pos, nextpos) then
|
|
amt_connections = amt_connections + 1
|
|
table.insert(road_bulk_set, {vector.add(road.pos, {x=4, y=0, z=0}), vector.add(road.pos, {x=7, y=0, z=3}), c_path})
|
|
end
|
|
else
|
|
nextpos.x = nextpos.x - 12 -- West (birch planks)
|
|
if connects(road.pos, nextpos) then
|
|
amt_connections = amt_connections + 1
|
|
table.insert(road_bulk_set, {vector.add(road.pos, {x=0, y=0, z=4}), vector.add(road.pos, {x=3, y=0, z=7}), c_path})
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
if amt_connections >= 2 and not road.is_starter then
|
|
table.insert(lamps, {x=road.pos.x, y=road.pos.y, z=road.pos.z})
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Place lamp posts
|
|
for l=1, #lamps do
|
|
village.spawn_chunk(
|
|
vmanip,
|
|
lamps[l],
|
|
state,
|
|
"0",
|
|
{},
|
|
pr,
|
|
"lamppost",
|
|
true,
|
|
true,
|
|
true,
|
|
ground,
|
|
ground_top
|
|
)
|
|
end
|
|
|
|
-- <<< FINAL VILLAGE CHUNK! >>>
|
|
|
|
-- Check if this village has created any houses so far
|
|
local has_house = false
|
|
for k,v in pairs(houses) do
|
|
if v ~= false then
|
|
has_house = true
|
|
break
|
|
end
|
|
end
|
|
-- Place a building at the start position as the final step.
|
|
-- Normally, this is the well.
|
|
local chunk_ok
|
|
if has_house then
|
|
chunk_ok = village.spawn_chunk(vmanip, pos, state, "0", replace, pr, "well", true, nil, true, ground, ground_top)
|
|
else
|
|
-- Place a fallback building instead of the well if the village does not have any buildings yet.
|
|
-- A nice side-effect of this is that this will create 'lonely huts'.
|
|
local structure = random_chunktype(pr, village.chunktypes_start_fallback, state.groundclass)
|
|
chunk_ok = village.spawn_chunk(vmanip, pos, state, "random", replace, pr, structure, true, nil, true, ground, ground_top)
|
|
minetest.log("info", "[rp_village] Village generated with fallback building instead of well")
|
|
end
|
|
if not chunk_ok then
|
|
minetest.log("warning", string.format("[rp_village] Failed to generated starter chunk at %s", minetest.pos_to_string(pos)))
|
|
end
|
|
|
|
-- <<< END OF VILLAGE CHUNK GENERATION >>>
|
|
|
|
-- All village chunks have been generated!
|
|
-- Now we apply changes to the VoxelManip data
|
|
vmanip:get_data(vdata_main)
|
|
|
|
local vdata_bulk_set_node = function(vdata, varea, minpos, maxpos, content_id)
|
|
for z=minpos.z, maxpos.z do
|
|
for y=minpos.y, maxpos.y do
|
|
for x=minpos.x, maxpos.x do
|
|
local vindex = varea:index(x, y, z)
|
|
vdata[vindex] = content_id
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Apply the road node replacements that were calculated above
|
|
for r=1, #road_bulk_set do
|
|
local rdata = road_bulk_set[r]
|
|
vdata_bulk_set_node(vdata_main, varea, rdata[1], rdata[2], rdata[3])
|
|
end
|
|
|
|
-- Generate ground decorations (like grass)
|
|
for d=1, #state.decors_to_place do
|
|
-- We just iterate through the positions we have collected earlier
|
|
local decor_info = state.decors_to_place[d]
|
|
-- Check if this position is still valid for the decor. Prevents placing decorations
|
|
-- in non-air nodes and if the floor node has changed (e.g. dirt path).
|
|
if vdata_main[decor_info.index_decor] == minetest.CONTENT_AIR and vdata_main[decor_info.index_floor] == decor_info.content_floor then
|
|
vdata_main[decor_info.index_decor] = decor_info.content_decor
|
|
end
|
|
end
|
|
|
|
vmanip:set_data(vdata_main)
|
|
|
|
-- The main village generation is complete here
|
|
vmanip:write_to_map()
|
|
vmanip:update_liquids()
|
|
|
|
-- <<< END OF VOXELMANIP CHANGES >>>
|
|
|
|
-- Final step: set node metadata (stuff that cannot be done in VManip)
|
|
-- and perform other manipulations
|
|
if state.nodeupdates then
|
|
for u=1, #state.nodeupdates do
|
|
local chunktype = state.nodeupdates[u].chunktype
|
|
local upos = state.nodeupdates[u].pos
|
|
local upos2 = vector.add(upos, vector.new(VILLAGE_CHUNK_SIZE, VILLAGE_CHUNK_SIZE, VILLAGE_CHUNK_SIZE))
|
|
|
|
-- Replace random chests with locked chests
|
|
village_modify_lock_chests(upos, upos2, pr)
|
|
|
|
-- Maximum of 1 music player per village
|
|
village_modify_limit_music_players(upos, upos2, pr)
|
|
|
|
-- Village modifier: Abandoned village
|
|
if state.is_abandoned then
|
|
village_modify_abandoned_village(upos, upos2, pr, {path=dirt_path, path_slab=dirt_path_slab, ground_top=ground_top})
|
|
end
|
|
|
|
-- Colorize beds
|
|
village_modify_bed_colors(upos, upos2, pr)
|
|
|
|
-- Force on_construct to be called on all nodes
|
|
util.reconstruct(upos, upos2, pr)
|
|
|
|
-- Initialize doors (required by rp_doors mod)
|
|
village_modify_init_doors(upos, upos2, pr)
|
|
|
|
-- Fill containers with goodies
|
|
village_modify_populate_containers(upos, upos2, pr, {chunktype=chunktype})
|
|
|
|
-- Handle entity spawner nodes.
|
|
-- In abandoned villages, remove all spawners and don't spawn anything.
|
|
-- Otherwise, randomly spawn an entity at each
|
|
-- spawner (chance of 1:chunkdef.entity_chance), then
|
|
-- remove the spawner nodes.
|
|
local chunkdef = village.chunkdefs[chunktype]
|
|
if chunkdef ~= nil then
|
|
if chunkdef.entities ~= nil then
|
|
if state.is_abandoned or (chunkdef.entity_chance ~= nil and pr:next(1, chunkdef.entity_chance) == 1) then
|
|
-- Remove entity spawners
|
|
util.nodefunc(
|
|
upos, upos2,
|
|
"rp_village:entity_spawner",
|
|
function(pos)
|
|
minetest.remove_node(pos)
|
|
end)
|
|
else
|
|
local ent_spawns = {}
|
|
|
|
-- Collect entitiy spawners
|
|
util.nodefunc(
|
|
upos, upos2,
|
|
"rp_village:entity_spawner",
|
|
function(pos)
|
|
table.insert(ent_spawns, pos)
|
|
end, true)
|
|
|
|
-- Initialize entity spawners
|
|
if #ent_spawns > 0 then
|
|
for ent, amt in pairs(chunkdef.entities) do
|
|
local profession
|
|
if string.sub(ent, 1, 11) == "__villager_" then
|
|
profession = string.sub(ent, 12, -1)
|
|
ent = "rp_mobs_mobs:villager"
|
|
end
|
|
for j = 1, pr:next(1, amt) do
|
|
if #ent_spawns == 0 then
|
|
break
|
|
end
|
|
local spawn, index = util.choice_element(ent_spawns, pr)
|
|
if spawn ~= nil then
|
|
local meta = minetest.get_meta(spawn)
|
|
meta:set_string("entity", ent)
|
|
if profession then
|
|
meta:set_string("villager_profession", profession)
|
|
end
|
|
minetest.get_node_timer(spawn):start(1)
|
|
-- Prevent spawning on same tile
|
|
table.remove(ent_spawns, index)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
-- Remove unused entity spawners
|
|
for e=1, #ent_spawns do
|
|
minetest.remove_node(ent_spawns[e])
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
minetest.log("action", string.format("[rp_village] Generated village '%s' at %s in %.2fms", village_name, minetest.pos_to_string(pos), (os.clock() - t1) * 1000))
|
|
return true
|
|
end
|
|
|
|
function village.spawn_village(pos, pr, force_place_starter, ground, ground_top)
|
|
if not ground then
|
|
ground = "rp_default:dirt"
|
|
end
|
|
if not ground_top then
|
|
ground_top = "rp_default:dirt_with_grass"
|
|
end
|
|
|
|
-- Before we begin, make sure there is enough space for the first chunk
|
|
-- (unless force_place_starter is true)
|
|
local empty = force_place_starter or check_empty(pos)
|
|
if not empty then
|
|
-- Oops! Not enough space. Village generation fails.
|
|
minetest.log("action", "[rp_village] Village generation not done at "..minetest.pos_to_string(pos)..". Not enough space for the first village chunk")
|
|
return false
|
|
end
|
|
|
|
-- Village generation can start!
|
|
-- Set village init stuff
|
|
local village_name = village.name.generate(pr, village.name.used)
|
|
village.villages[village.get_id(village_name, pos)] = {
|
|
name = village_name,
|
|
pos = pos,
|
|
}
|
|
village.save_villages()
|
|
village.load_waypoints()
|
|
|
|
local spread = VILLAGE_CHUNK_SIZE * village.max_village_spread
|
|
local vspread = vector.new(spread, spread, spread)
|
|
local emerge_min = vector.add(pos, vector.new(-spread, -(HILL_H + HILL_EXTEND_BELOW + 1), -spread))
|
|
local emerge_max = vector.add(pos, vector.new(spread, VILLAGE_CHUNK_HEIGHT, spread))
|
|
-- chance for village to be abandoned
|
|
local is_abandoned = pr:next(1,ABANDONED_CHANCE) == 1
|
|
minetest.emerge_area(emerge_min, emerge_max, after_village_area_emerged, {
|
|
pos=pos,
|
|
pr=pr,
|
|
force_place_starter=force_place_starter,
|
|
ground=ground,ground_top=ground_top,
|
|
village_name=village_name,
|
|
emin=emerge_min,
|
|
emax=emerge_max,
|
|
is_abandoned=is_abandoned})
|
|
return true
|
|
end
|
|
|
|
minetest.register_on_mods_loaded(village.load_villages)
|