Rename "Monster Spawner" to "Mob Spawner"
[MineClone/MineClone2.git] / mods / MAPGEN / mcl_dungeons / init.lua
blobbd081619caa1b238979b7c3c35233d2f7c26bec0
1 -- FIXME: Chests may appear at openings
3 local mg_name = minetest.get_mapgen_setting("mg_name")
4 local pr = PseudoRandom(os.time())
6 -- Get loot for dungeon chests
7 local get_loot = function()
8 local loottable = {
10 stacks_min = 1,
11 stacks_max = 3,
12 items = {
13 { itemstring = "mobs:nametag", weight = 20 },
14 { itemstring = "mcl_mobitems:saddle", weight = 20 },
15 { itemstring = "mcl_jukebox:record_1", weight = 15 },
16 { itemstring = "mcl_jukebox:record_4", weight = 15 },
17 { itemstring = "mobs_mc:iron_horse_armor", weight = 15 },
18 { itemstring = "mcl_core:apple_gold", weight = 15 },
19 -- TODO: Enchanted Book
20 { itemstring = "mcl_books:book", weight = 10 },
21 { itemstring = "mobs_mc:gold_horse_armor", weight = 10 },
22 { itemstring = "mobs_mc:diamond_horse_armor", weight = 5 },
23 -- TODO: Enchanted Golden Apple
24 { itemstring = "mcl_core:apple_gold", weight = 2 },
28 stacks_min = 1,
29 stacks_max = 4,
30 items = {
31 { itemstring = "mcl_farming:wheat_item", weight = 20, amount_min = 1, amount_max = 4 },
32 { itemstring = "mcl_farming:bread", weight = 20 },
33 { itemstring = "mcl_core:coal_lump", weight = 15, amount_min = 1, amount_max = 4 },
34 { itemstring = "mesecons:redstone", weight = 15, amount_min = 1, amount_max = 4 },
35 { itemstring = "mcl_farming:beetroot_seeds", weight = 10, amount_min = 2, amount_max = 4 },
36 { itemstring = "mcl_farming:melon_seeds", weight = 10, amount_min = 2, amount_max = 4 },
37 { itemstring = "mcl_farming:pumpkin_seeds", weight = 10, amount_min = 2, amount_max = 4 },
38 { itemstring = "mcl_core:iron_ingot", weight = 10, amount_min = 1, amount_max = 4 },
39 { itemstring = "mcl_buckets:bucket_empty", weight = 10 },
40 { itemstring = "mcl_core:gold_ingot", weight = 5, amount_min = 1, amount_max = 4 },
44 stacks_min = 3,
45 stacks_max = 3,
46 items = {
47 { itemstring = "mcl_mobitems:bone", weight = 10, amount_min = 1, amount_max = 8 },
48 { itemstring = "mcl_mobitems:gunpowder", weight = 10, amount_min = 1, amount_max = 8 },
49 { itemstring = "mcl_mobitems:rotten_flesh", weight = 10, amount_min = 1, amount_max = 8 },
50 { itemstring = "mcl_mobitems:string", weight = 10, amount_min = 1, amount_max = 8 },
55 -- Bonus loot for v6 mapgen: Otherwise unobtainable saplings.
56 if mg_name == "v6" then
57 table.insert(loottable, {
58 stacks_min = 1,
59 stacks_max = 1,
60 items = {
61 { itemstring = "mcl_core:birchsapling", weight = 1, amount_min = 1, amount_max = 2 },
62 { itemstring = "mcl_core:acaciasapling", weight = 1, amount_min = 1, amount_max = 2 },
63 { itemstring = "", weight = 11 },
66 end
67 local items = mcl_loot.get_multi_loot(loottable, pr)
69 return items
70 end
73 -- Buffer for LuaVoxelManip
74 local lvm_buffer = {}
76 -- Below the bedrock, generate air/void
77 minetest.register_on_generated(function(minp, maxp)
78 if maxp.y < mcl_vars.mg_overworld_min or minp.y > mcl_vars.mg_overworld_max then
79 return
80 end
82 local vm, emin, emax = minetest.get_mapgen_object("voxelmanip")
83 local data = vm:get_data(lvm_buffer)
84 local area = VoxelArea:new({MinEdge=emin, MaxEdge=emax})
85 local lvm_used = false
87 local c_air = minetest.get_content_id("air")
88 local c_cobble = minetest.get_content_id("mcl_core:cobble")
89 local c_mossycobble = minetest.get_content_id("mcl_core:mossycobble")
91 -- Remember spawner chest positions to set metadata later
92 local chest_posses = {}
93 local spawner_posses = {}
95 -- Calculate the number of dungeon spawn attempts
96 local sizevector = vector.subtract(maxp, minp)
97 sizevector = vector.add(sizevector, 1)
98 local chunksize = sizevector.x * sizevector.y * sizevector.z
100 -- In Minecraft, there 8 dungeon spawn attempts Minecraft chunk (16*256*16 = 65536 blocks).
101 -- Minetest chunks don't have this size, so scale the number accordingly.
102 local attempts = math.ceil(chunksize / 65536 * 8)
104 for a=1, attempts do
105 local x, y, z
106 local b = 7 -- buffer
107 x = math.random(minp.x+b, maxp.x-b)
109 local ymin = math.min(mcl_vars.mg_overworld_max, math.max(minp.y, mcl_vars.mg_bedrock_overworld_max) + 7)
110 local ymax = math.min(mcl_vars.mg_overworld_max, math.max(maxp.y, mcl_vars.mg_bedrock_overworld_max) - 4)
112 y = math.random(ymin, ymax)
113 z = math.random(minp.z+b, maxp.z-b)
115 local dungeonsizes = {
116 { x=5, y=4, z=5},
117 { x=5, y=4, z=7},
118 { x=7, y=4, z=5},
119 { x=7, y=4, z=7},
121 local dim = dungeonsizes[math.random(1, #dungeonsizes)]
123 -- Check floor and ceiling: Must be *completely* solid
124 local ceilingfloor_ok = true
125 for tx = x, x+dim.x do
126 for tz = z, z+dim.z do
127 local floor = minetest.get_name_from_content_id(data[area:index(tx, y, tz)])
128 local ceiling = minetest.get_name_from_content_id(data[area:index(tx, y+dim.y+1, tz)])
129 if (not minetest.registered_nodes[floor].walkable) or (not minetest.registered_nodes[ceiling].walkable) then
130 ceilingfloor_ok = false
131 break
134 if not ceilingfloor_ok then break end
137 -- Check for air openings (2 stacked air at ground level) in wall positions
138 local openings_counter = 0
139 -- Store positions of openings; walls will not be generated here
140 local openings = {}
141 -- Corners are stored because a corner-only opening needs to be increased,
142 -- so entities can get through.
143 local corners = {}
144 if ceilingfloor_ok then
146 local walls = {
147 -- walls along x axis (contain corners)
148 { x, x+dim.x+1, "x", "z", z },
149 { x, x+dim.x+1, "x", "z", z+dim.z+1 },
150 -- walls along z axis (exclude corners)
151 { z+1, z+dim.z, "z", "x", x },
152 { z+1, z+dim.z, "z", "x", x+dim.x+1 },
155 for w=1, #walls do
156 local wall = walls[w]
157 for iter = wall[1], wall[2] do
158 local pos = {}
159 pos[wall[3]] = iter
160 pos[wall[4]] = wall[5]
161 pos.y = y+1
163 if openings[pos.x] == nil then openings[pos.x] = {} end
165 local door1 = area:index(pos.x, pos.y, pos.z)
166 pos.y = y+2
167 local door2 = area:index(pos.x, pos.y, pos.z)
168 local doorname1 = minetest.get_name_from_content_id(data[door1])
169 local doorname2 = minetest.get_name_from_content_id(data[door2])
170 if doorname1 == "air" and doorname2 == "air" then
171 openings_counter = openings_counter + 1
172 openings[pos.x][pos.z] = true
174 -- Record corners
175 if wall[3] == "x" and (iter == wall[1] or iter == wall[2]) then
176 table.insert(corners, {x=pos.x, z=pos.z})
184 -- If all openings are only at corners, the dungeon can't be accessed yet.
185 -- This code extends the openings of corners so they can be entered.
186 if openings_counter >= 1 and openings_counter == #corners then
187 for c=1, #corners do
188 -- Prevent creating too many openings because this would lead to dungeon rejection
189 if openings_counter >= 5 then
190 break
192 -- A corner is widened by adding openings to both neighbors
193 local cx, cz = corners[c].x, corners[c].z
194 local cxn, czn = cx, cz
195 if x == cx then
196 cxn = cxn + 1
197 else
198 cxn = cxn - 1
200 if z == cz then
201 czn = czn + 1
202 else
203 czn = czn - 1
205 openings[cx][czn] = true
206 openings_counter = openings_counter + 1
207 if openings_counter < 5 then
208 openings[cxn][cz] = true
209 openings_counter = openings_counter + 1
214 -- Check conditions. If okay, start generating
215 if ceilingfloor_ok and openings_counter >= 1 and openings_counter <= 5 then
216 -- Okay! Spawning starts!
218 -- First prepare random chest positions.
219 -- Chests spawn at wall
221 -- We assign each position at the wall a number and each chest gets one of these numbers randomly
222 local totalChests = 2 -- this code strongly relies on this number being 2
223 local totalChestSlots = (dim.x-1) * (dim.z-1)
224 local chestSlots = {}
225 -- There is a small chance that both chests have the same slot.
226 -- In that case, we give a 2nd chance for the 2nd chest to get spawned.
227 -- If it failed again, tough luck! We stick with only 1 chest spawned.
228 local lastRandom
229 local secondChance = true -- second chance is still available
230 for i=1, totalChests do
231 local r = math.random(1, totalChestSlots)
232 if r == lastRandom and secondChance then
233 -- Oops! Same slot selected. Try again.
234 r = math.random(1, totalChestSlots)
235 secondChance = false
237 lastRandom = r
238 table.insert(chestSlots, r)
240 table.sort(chestSlots)
241 local currentChest = 1
243 -- Calculate the mob spawner position, to be re-used for later
244 local spawner_pos = {x = x + math.ceil(dim.x/2), y = y+1, z = z + math.ceil(dim.z/2)}
245 table.insert(spawner_posses, spawner_pos)
247 -- Generate walls and floor
248 local maxx, maxy, maxz = x+dim.x+1, y+dim.y, z+dim.z+1
249 local chestSlotCounter = 1
250 for tx = x, maxx do
251 for tz = z, maxz do
252 for ty = y, maxy do
253 local p_pos = area:index(tx, ty, tz)
255 -- Do not overwrite nodes with is_ground_content == false (e.g. bedrock)
256 -- Exceptions: cobblestone and moss stone so neighborings dungeons nicely connect to each other
257 local name = minetest.get_name_from_content_id(data[p_pos])
258 if name == "mcl_core:cobble" or name == "mcl_core:mossycobble" or minetest.registered_nodes[name].is_ground_content then
259 -- Floor
260 if ty == y then
261 if math.random(1,4) == 1 then
262 data[p_pos] = c_cobble
263 else
264 data[p_pos] = c_mossycobble
267 -- Generate walls
268 --[[ Note: No additional cobblestone ceiling is generated. This is intentional.
269 The solid blocks above the dungeon are considered as the “ceiling”.
270 It is possible (but rare) for a dungeon to generate below sand or gravel. ]]
272 elseif ty > y and (tx == x or tx == maxx or (tz == z or tz == maxz)) then
273 -- Check if it's an opening first
274 if (not openings[tx][tz]) or ty == maxy then
275 -- Place wall or ceiling
276 data[p_pos] = c_cobble
277 elseif ty < maxy - 1 then
278 -- Normally the openings are already clear, but not if it is a corner
279 -- widening. Make sure to clear at least the bottom 2 nodes of an opening.
280 data[p_pos] = c_air
281 elseif ty == maxy - 1 and data[p_pos] ~= c_air then
282 -- This allows for variation between 2-node and 3-node high openings.
283 data[p_pos] = c_cobble
285 -- If it was an opening, the lower 3 blocks are not touched at all
287 -- Room interiour
288 else
289 local forChest = ty==y+1 and (tx==x+1 or tx==maxx-1 or tz==z+1 or tz==maxz-1)
291 -- Place next chest at the wall (if it was its chosen wall slot)
292 if forChest and (currentChest < totalChests + 1) and (chestSlots[currentChest] == chestSlotCounter) then
293 table.insert(chest_posses, {x=tx, y=ty, z=tz})
294 currentChest = currentChest + 1
295 else
296 data[p_pos] = c_air
298 if forChest then
299 chestSlotCounter = chestSlotCounter + 1
309 lvm_used = true
312 if lvm_used then
313 local chest_param2 = {}
314 -- Determine correct chest rotation (must pointi outwards)
315 for c=1, #chest_posses do
316 local cpos = chest_posses[c]
318 -- Check surroundings of chest to determine correct rotation
319 local surround_vectors = {
320 { x=-1, y=0, z=0 },
321 { x=1, y=0, z=0 },
322 { x=0, y=0, z=-1 },
323 { x=0, y=0, z=1 },
325 local surroundings = {}
327 for s=1, #surround_vectors do
328 -- Detect the 4 horizontal neighbors
329 local spos = vector.add(cpos, surround_vectors[s])
330 local wpos = vector.subtract(cpos, surround_vectors[s])
331 local p_pos = area:index(spos.x, spos.y, spos.z)
332 local p_pos2 = area:index(wpos.x, wpos.y, wpos.z)
334 local nodename = minetest.get_name_from_content_id(data[p_pos])
335 local nodename2 = minetest.get_name_from_content_id(data[p_pos2])
336 local nodedef = minetest.registered_nodes[nodename]
337 local nodedef2 = minetest.registered_nodes[nodename2]
338 -- The chest needs an open space in front of it and a walkable node (except chest) behind it
339 if nodedef and nodedef.walkable == false and nodedef2 and nodedef2.walkable == true and nodename2 ~= "mcl_chests:chest" then
340 table.insert(surroundings, spos)
343 -- Set param2 (=facedir) of this chest
344 local facedir
345 if #surroundings <= 0 then
346 -- Fallback if chest ended up in the middle of a room for some reason
347 facedir = math.random(0, 0)
348 else
349 -- 1 or multiple possible open directions: Choose random facedir
350 local face_to = surroundings[math.random(1, #surroundings)]
351 facedir = minetest.dir_to_facedir(vector.subtract(cpos, face_to))
353 chest_param2[c] = facedir
356 -- Finally generate the dungeons all at once (except the chests and the spawners)
357 vm:set_data(data)
358 vm:calc_lighting()
359 vm:update_liquids()
360 vm:write_to_map()
362 -- Chests are placed seperately
363 for c=1, #chest_posses do
364 local cpos = chest_posses[c]
365 minetest.set_node(cpos, {name="mcl_chests:chest", param2=chest_param2[c]})
366 local meta = minetest.get_meta(cpos)
367 local inv = meta:get_inventory()
368 local items = get_loot()
369 for i=1, math.min(#items, inv:get_size("main")) do
370 inv:set_stack("main", i, ItemStack(items[i]))
374 -- Mob spawners are placed seperately, too
375 -- We don't want to destroy non-ground nodes
376 for s=1, #spawner_posses do
377 local sp = spawner_posses[s]
378 local n = minetest.get_name_from_content_id(data[area:index(sp.x,sp.y,sp.z)])
379 if minetest.registered_nodes[n].is_ground_content then
381 -- ... and place it and select a random mob
382 minetest.set_node(sp, {name = "mcl_mobspawners:spawner"})
383 local mobs = {
384 "mobs_mc:zombie",
385 "mobs_mc:zombie",
386 "mobs_mc:spider",
387 "mobs_mc:skeleton",
389 local spawner_mob = mobs[math.random(1, #mobs)]
391 mcl_mobspawners.setup_spawner(sp, spawner_mob)
397 end)