mirror of
https://git.minetest.land/VoxeLibre/VoxeLibre.git
synced 2024-12-02 23:41:06 +01:00
0b27b6bec3
DO NOT USE IN PRODUCTION, DO NOT START OLD WORLDS WITHOUT A BACKUP These are the first steps of the new mob API. The game does actually start, but mobs do not work yet. You will also get some warnings about mob spawners, but don't worry about that. This is really just some 'first impression' of how the mob API is gonna look like. Some things are already complete, like the agression system. AI and attacking have not been worked on yet. mobs_mc and mcl_mobs have actually been merged into one piece but I will probably change that again in the future actually, and split the different mobs into different mods. There are also a few usefull things like the universal mount API and a more general purpose smoke API, but all of this is still far from complete. I'll put some work into the API this week but probably not next week, then I'll see but don't expect this to be done before 2022. I'll work on it, but I'll do it slowly and progressively to not get burned out again and to still have enough time to graduate from school in the meantime.
419 lines
15 KiB
Lua
419 lines
15 KiB
Lua
-- FIXME: Chests may appear at openings
|
|
|
|
mcl_dungeons = {}
|
|
|
|
local mg_name = minetest.get_mapgen_setting("mg_name")
|
|
|
|
-- Are dungeons disabled?
|
|
if mcl_vars.mg_dungeons == false or mg_name == "singlenode" then
|
|
return
|
|
end
|
|
|
|
--lua locals
|
|
--minetest
|
|
local registered_nodes = minetest.registered_nodes
|
|
local swap_node = minetest.swap_node
|
|
local set_node = minetest.set_node
|
|
local dir_to_facedir = minetest.dir_to_facedir
|
|
local get_meta = minetest.get_meta
|
|
local emerge_area = minetest.emerge_area
|
|
|
|
--vector
|
|
local vector_add = vector.add
|
|
local vector_subtract = vector.subtract
|
|
|
|
--table
|
|
local table_insert = table.insert
|
|
local table_sort = table.sort
|
|
|
|
--math
|
|
local math_min = math.min
|
|
local math_max = math.max
|
|
local math_ceil = math.ceil
|
|
|
|
--custom mcl_vars
|
|
local get_node = mcl_vars.get_node
|
|
|
|
|
|
local min_y = math_max(mcl_vars.mg_overworld_min, mcl_vars.mg_bedrock_overworld_max) + 1
|
|
local max_y = mcl_vars.mg_overworld_max - 1
|
|
-- Calculate the number of dungeon spawn attempts
|
|
-- In Minecraft, there 8 dungeon spawn attempts Minecraft chunk (16*256*16 = 65536 blocks).
|
|
-- Minetest chunks don't have this size, so scale the number accordingly.
|
|
local attempts = math_ceil(((mcl_vars.chunksize * mcl_vars.MAP_BLOCKSIZE) ^ 3) / 8192) -- 63 = 80*80*80/8192
|
|
|
|
local dungeonsizes = {
|
|
{ x=5, y=4, z=5},
|
|
{ x=5, y=4, z=7},
|
|
{ x=7, y=4, z=5},
|
|
{ x=7, y=4, z=7},
|
|
}
|
|
|
|
--[[local dirs = {
|
|
{ x= 1, y=0, z= 0 },
|
|
{ x= 0, y=0, z= 1 },
|
|
{ x=-1, y=0, z= 0 },
|
|
{ x= 0, y=0, z=-1 },
|
|
}]]
|
|
|
|
local surround_vectors = {
|
|
{ x=-1, y=0, z=0 },
|
|
{ x=1, y=0, z=0 },
|
|
{ x=0, y=0, z=-1 },
|
|
{ x=0, y=0, z=1 },
|
|
}
|
|
|
|
local function ecb_spawn_dungeon(blockpos, action, calls_remaining, param)
|
|
if calls_remaining >= 1 then return end
|
|
|
|
local p1, _, dim, pr = param.p1, param.p2, param.dim, param.pr
|
|
local x, y, z = p1.x, p1.y, p1.z
|
|
local check = not (param.dontcheck or false)
|
|
|
|
-- Check floor and ceiling: Must be *completely* solid
|
|
local y_floor = y
|
|
local y_ceiling = y + dim.y + 1
|
|
if check then for tx = x+1, x+dim.x do for tz = z+1, z+dim.z do
|
|
if not registered_nodes[get_node({x = tx, y = y_floor , z = tz}).name].walkable
|
|
or not registered_nodes[get_node({x = tx, y = y_ceiling, z = tz}).name].walkable then return false end
|
|
end end end
|
|
|
|
-- Check for air openings (2 stacked air at ground level) in wall positions
|
|
local openings_counter = 0
|
|
-- Store positions of openings; walls will not be generated here
|
|
local openings = {}
|
|
-- Corners are stored because a corner-only opening needs to be increased,
|
|
-- so entities can get through.
|
|
local corners = {}
|
|
|
|
local x2,z2 = x+dim.x+1, z+dim.z+1
|
|
|
|
if get_node({x=x, y=y+1, z=z}).name == "air" and get_node({x=x, y=y+2, z=z}).name == "air" then
|
|
openings_counter = openings_counter + 1
|
|
if not openings[x] then openings[x]={} end
|
|
openings[x][z] = true
|
|
table_insert(corners, {x=x, z=z})
|
|
end
|
|
if get_node({x=x2, y=y+1, z=z}).name == "air" and get_node({x=x2, y=y+2, z=z}).name == "air" then
|
|
openings_counter = openings_counter + 1
|
|
if not openings[x2] then openings[x2]={} end
|
|
openings[x2][z] = true
|
|
table_insert(corners, {x=x2, z=z})
|
|
end
|
|
if get_node({x=x, y=y+1, z=z2}).name == "air" and get_node({x=x, y=y+2, z=z2}).name == "air" then
|
|
openings_counter = openings_counter + 1
|
|
if not openings[x] then openings[x]={} end
|
|
openings[x][z2] = true
|
|
table_insert(corners, {x=x, z=z2})
|
|
end
|
|
if get_node({x=x2, y=y+1, z=z2}).name == "air" and get_node({x=x2, y=y+2, z=z2}).name == "air" then
|
|
openings_counter = openings_counter + 1
|
|
if not openings[x2] then openings[x2]={} end
|
|
openings[x2][z2] = true
|
|
table_insert(corners, {x=x2, z=z2})
|
|
end
|
|
|
|
for wx = x+1, x+dim.x do
|
|
if get_node({x=wx, y=y+1, z=z}).name == "air" and get_node({x=wx, y=y+2, z=z}).name == "air" then
|
|
openings_counter = openings_counter + 1
|
|
if check and openings_counter > 5 then return end
|
|
if not openings[wx] then openings[wx]={} end
|
|
openings[wx][z] = true
|
|
end
|
|
if get_node({x=wx, y=y+1, z=z2}).name == "air" and get_node({x=wx, y=y+2, z=z2}).name == "air" then
|
|
openings_counter = openings_counter + 1
|
|
if check and openings_counter > 5 then return end
|
|
if not openings[wx] then openings[wx]={} end
|
|
openings[wx][z2] = true
|
|
end
|
|
end
|
|
for wz = z+1, z+dim.z do
|
|
if get_node({x=x, y=y+1, z=wz}).name == "air" and get_node({x=x, y=y+2, z=wz}).name == "air" then
|
|
openings_counter = openings_counter + 1
|
|
if check and openings_counter > 5 then return end
|
|
if not openings[x] then openings[x]={} end
|
|
openings[x][wz] = true
|
|
end
|
|
if get_node({x=x2, y=y+1, z=wz}).name == "air" and get_node({x=x2, y=y+2, z=wz}).name == "air" then
|
|
openings_counter = openings_counter + 1
|
|
if check and openings_counter > 5 then return end
|
|
if not openings[x2] then openings[x2]={} end
|
|
openings[x2][wz] = true
|
|
end
|
|
end
|
|
|
|
-- If all openings are only at corners, the dungeon can't be accessed yet.
|
|
-- This code extends the openings of corners so they can be entered.
|
|
if openings_counter >= 1 and openings_counter == #corners then
|
|
for c=1, #corners do
|
|
-- Prevent creating too many openings because this would lead to dungeon rejection
|
|
if openings_counter >= 5 then
|
|
break
|
|
end
|
|
-- A corner is widened by adding openings to both neighbors
|
|
local cx, cz = corners[c].x, corners[c].z
|
|
local cxn, czn = cx, cz
|
|
if x == cx then
|
|
cxn = cxn + 1
|
|
else
|
|
cxn = cxn - 1
|
|
end
|
|
if z == cz then
|
|
czn = czn + 1
|
|
else
|
|
czn = czn - 1
|
|
end
|
|
openings[cx][czn] = true
|
|
openings_counter = openings_counter + 1
|
|
if openings_counter < 5 then
|
|
if not openings[cxn] then openings[cxn]={} end
|
|
openings[cxn][cz] = true
|
|
openings_counter = openings_counter + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Check conditions. If okay, start generating
|
|
if check and (openings_counter < 1 or openings_counter > 5) then return end
|
|
|
|
minetest.log("action","[mcl_dungeons] Placing new dungeon at "..minetest.pos_to_string({x=x,y=y,z=z}))
|
|
-- Okay! Spawning starts!
|
|
|
|
-- Remember spawner chest positions to set metadata later
|
|
local chests = {}
|
|
local spawner_posses = {}
|
|
|
|
-- First prepare random chest positions.
|
|
-- Chests spawn at wall
|
|
|
|
-- We assign each position at the wall a number and each chest gets one of these numbers randomly
|
|
local totalChests = 2 -- this code strongly relies on this number being 2
|
|
local totalChestSlots = (dim.x + dim.z - 2) * 2
|
|
local chestSlots = {}
|
|
-- There is a small chance that both chests have the same slot.
|
|
-- In that case, we give a 2nd chance for the 2nd chest to get spawned.
|
|
-- If it failed again, tough luck! We stick with only 1 chest spawned.
|
|
local lastRandom
|
|
local secondChance = true -- second chance is still available
|
|
for i=1, totalChests do
|
|
local r = pr:next(1, totalChestSlots)
|
|
if r == lastRandom and secondChance then
|
|
-- Oops! Same slot selected. Try again.
|
|
r = pr:next(1, totalChestSlots)
|
|
secondChance = false
|
|
end
|
|
lastRandom = r
|
|
table_insert(chestSlots, r)
|
|
end
|
|
table_sort(chestSlots)
|
|
local currentChest = 1
|
|
|
|
-- Calculate the mob spawner position, to be re-used for later
|
|
local sp = {x = x + math_ceil(dim.x/2), y = y+1, z = z + math_ceil(dim.z/2)}
|
|
local rn = registered_nodes[get_node(sp).name]
|
|
if rn and rn.is_ground_content then
|
|
table_insert(spawner_posses, sp)
|
|
end
|
|
|
|
-- Generate walls and floor
|
|
local maxx, maxy, maxz = x+dim.x+1, y+dim.y, z+dim.z+1
|
|
local chestSlotCounter = 1
|
|
for tx = x, maxx do
|
|
for tz = z, maxz do
|
|
for ty = y, maxy do
|
|
local p = {x = tx, y=ty, z=tz}
|
|
|
|
-- Do not overwrite nodes with is_ground_content == false (e.g. bedrock)
|
|
-- Exceptions: cobblestone and mossy cobblestone so neighborings dungeons nicely connect to each other
|
|
local name = get_node(p).name
|
|
if registered_nodes[name].is_ground_content or name == "mcl_core:cobble" or name == "mcl_core:mossycobble" then
|
|
-- Floor
|
|
if ty == y then
|
|
if pr:next(1,4) == 1 then
|
|
swap_node(p, {name = "mcl_core:cobble"})
|
|
else
|
|
swap_node(p, {name = "mcl_core:mossycobble"})
|
|
end
|
|
|
|
-- Generate walls
|
|
--[[ Note: No additional cobblestone ceiling is generated. This is intentional.
|
|
The solid blocks above the dungeon are considered as the “ceiling”.
|
|
It is possible (but rare) for a dungeon to generate below sand or gravel. ]]
|
|
|
|
elseif tx == x or tz == z or tx == maxx or tz == maxz then
|
|
-- Check if it's an opening first
|
|
if (ty == maxy) or (not (openings[tx] and openings[tx][tz])) then
|
|
-- Place wall or ceiling
|
|
swap_node(p, {name = "mcl_core:cobble"})
|
|
elseif ty < maxy - 1 then
|
|
-- Normally the openings are already clear, but not if it is a corner
|
|
-- widening. Make sure to clear at least the bottom 2 nodes of an opening.
|
|
if name ~= "air" then swap_node(p, {name = "air"}) end
|
|
elseif name ~= "air" then
|
|
-- This allows for variation between 2-node and 3-node high openings.
|
|
swap_node(p, {name = "mcl_core:cobble"})
|
|
end
|
|
-- If it was an opening, the lower 3 blocks are not touched at all
|
|
|
|
-- Room interiour
|
|
else
|
|
if (ty==y+1) and (tx==x+1 or tx==maxx-1 or tz==z+1 or tz==maxz-1) and (currentChest < totalChests + 1) and (chestSlots[currentChest] == chestSlotCounter) then
|
|
currentChest = currentChest + 1
|
|
table_insert(chests, {x=tx, y=ty, z=tz})
|
|
else
|
|
swap_node(p, {name = "air"})
|
|
end
|
|
|
|
local forChest = ty==y+1 and (tx==x+1 or tx==maxx-1 or tz==z+1 or tz==maxz-1)
|
|
|
|
-- Place next chest at the wall (if it was its chosen wall slot)
|
|
if forChest and (currentChest < totalChests + 1) and (chestSlots[currentChest] == chestSlotCounter) then
|
|
currentChest = currentChest + 1
|
|
table_insert(chests, {x=tx, y=ty, z=tz})
|
|
-- else
|
|
--swap_node(p, {name = "air"})
|
|
end
|
|
if forChest then
|
|
chestSlotCounter = chestSlotCounter + 1
|
|
end
|
|
end
|
|
end
|
|
end end end
|
|
|
|
for c=#chests, 1, -1 do
|
|
local pos = chests[c]
|
|
|
|
local surroundings = {}
|
|
for s=1, #surround_vectors do
|
|
-- Detect the 4 horizontal neighbors
|
|
local spos = vector_add(pos, surround_vectors[s])
|
|
local wpos = vector_subtract(pos, surround_vectors[s])
|
|
local nodename = get_node(spos).name
|
|
local nodename2 = get_node(wpos).name
|
|
local nodedef = registered_nodes[nodename]
|
|
local nodedef2 = registered_nodes[nodename2]
|
|
-- The chest needs an open space in front of it and a walkable node (except chest) behind it
|
|
if nodedef and nodedef.walkable == false and nodedef2 and nodedef2.walkable == true and nodename2 ~= "mcl_chests:chest" then
|
|
table_insert(surroundings, spos)
|
|
end
|
|
end
|
|
-- Set param2 (=facedir) of this chest
|
|
local facedir
|
|
if #surroundings <= 0 then
|
|
-- Fallback if chest ended up in the middle of a room for some reason
|
|
facedir = pr:next(0, 0)
|
|
else
|
|
-- 1 or multiple possible open directions: Choose random facedir
|
|
local face_to = surroundings[pr:next(1, #surroundings)]
|
|
facedir = dir_to_facedir(vector_subtract(pos, face_to))
|
|
end
|
|
|
|
set_node(pos, {name="mcl_chests:chest", param2=facedir})
|
|
local meta = get_meta(pos)
|
|
|
|
local loottable =
|
|
{
|
|
{
|
|
stacks_min = 1,
|
|
stacks_max = 3,
|
|
items = {
|
|
{ itemstring = "mcl_mobitems:nametag", weight = 20 },
|
|
{ itemstring = "mcl_mobitems:saddle", weight = 20 },
|
|
{ itemstring = "mcl_jukebox:record_1", weight = 15 },
|
|
{ itemstring = "mcl_jukebox:record_4", weight = 15 },
|
|
{ itemstring = "mcl_mobitems:iron_horse_armor", weight = 15 },
|
|
{ itemstring = "mcl_core:apple_gold", weight = 15 },
|
|
{ itemstack = mcl_enchanting.get_uniform_randomly_enchanted_book({"soul_speed"}, pr), weight = 10 },
|
|
{ itemstring = "mcl_mobitems:gold_horse_armor", weight = 10 },
|
|
{ itemstring = "mcl_mobitems:diamond_horse_armor", weight = 5 },
|
|
{ itemstring = "mcl_core:apple_gold_enchanted", weight = 2 },
|
|
}
|
|
},
|
|
{
|
|
stacks_min = 1,
|
|
stacks_max = 4,
|
|
items = {
|
|
{ itemstring = "mcl_farming:wheat_item", weight = 20, amount_min = 1, amount_max = 4 },
|
|
{ itemstring = "mcl_farming:bread", weight = 20 },
|
|
{ itemstring = "mcl_core:coal_lump", weight = 15, amount_min = 1, amount_max = 4 },
|
|
{ itemstring = "mesecons:redstone", weight = 15, amount_min = 1, amount_max = 4 },
|
|
{ itemstring = "mcl_farming:beetroot_seeds", weight = 10, amount_min = 2, amount_max = 4 },
|
|
{ itemstring = "mcl_farming:melon_seeds", weight = 10, amount_min = 2, amount_max = 4 },
|
|
{ itemstring = "mcl_farming:pumpkin_seeds", weight = 10, amount_min = 2, amount_max = 4 },
|
|
{ itemstring = "mcl_core:iron_ingot", weight = 10, amount_min = 1, amount_max = 4 },
|
|
{ itemstring = "mcl_buckets:bucket_empty", weight = 10 },
|
|
{ itemstring = "mcl_core:gold_ingot", weight = 5, amount_min = 1, amount_max = 4 },
|
|
},
|
|
},
|
|
{
|
|
stacks_min = 3,
|
|
stacks_max = 3,
|
|
items = {
|
|
{ itemstring = "mcl_mobitems:bone", weight = 10, amount_min = 1, amount_max = 8 },
|
|
{ itemstring = "mcl_mobitems:gunpowder", weight = 10, amount_min = 1, amount_max = 8 },
|
|
{ itemstring = "mcl_mobitems:rotten_flesh", weight = 10, amount_min = 1, amount_max = 8 },
|
|
{ itemstring = "mcl_mobitems:string", weight = 10, amount_min = 1, amount_max = 8 },
|
|
},
|
|
}
|
|
}
|
|
|
|
-- Bonus loot for v6 mapgen: Otherwise unobtainable saplings.
|
|
if mg_name == "v6" then
|
|
table_insert(loottable, {
|
|
stacks_min = 1,
|
|
stacks_max = 3,
|
|
items = {
|
|
{ itemstring = "mcl_core:birchsapling", weight = 1, amount_min = 1, amount_max = 2 },
|
|
{ itemstring = "mcl_core:acaciasapling", weight = 1, amount_min = 1, amount_max = 2 },
|
|
{ itemstring = "", weight = 6 },
|
|
},
|
|
})
|
|
end
|
|
|
|
minetest.log("action", "[mcl_dungeons] Filling chest " .. tostring(c) .. " at " .. minetest.pos_to_string(pos))
|
|
mcl_loot.fill_inventory(meta:get_inventory(), "main", mcl_loot.get_multi_loot(loottable, pr), pr)
|
|
end
|
|
|
|
-- Mob spawners are placed seperately, too
|
|
-- We don't want to destroy non-ground nodes
|
|
for s=#spawner_posses, 1, -1 do
|
|
local sp = spawner_posses[s]
|
|
-- ... and place it and select a random mob
|
|
set_node(sp, {name = "mcl_mobspawners:spawner"})
|
|
local mobs = {
|
|
"mcl_mobs:zombie",
|
|
"mcl_mobs:zombie",
|
|
"mcl_mobs:spider",
|
|
"mcl_mobs:skeleton",
|
|
}
|
|
local spawner_mob = mobs[pr:next(1, #mobs)]
|
|
|
|
mcl_mobspawners.setup_spawner(sp, spawner_mob, 0, 7)
|
|
end
|
|
end
|
|
|
|
local function dungeons_nodes(minp, maxp, blockseed)
|
|
local ymin, ymax = math_max(min_y, minp.y), math_min(max_y, maxp.y)
|
|
if ymax < ymin then return false end
|
|
local pr = PseudoRandom(blockseed)
|
|
for a=1, attempts do
|
|
local dim = dungeonsizes[pr:next(1, #dungeonsizes)]
|
|
local x = pr:next(minp.x, maxp.x-dim.x-1)
|
|
local y = pr:next(ymin , ymax -dim.y-1)
|
|
local z = pr:next(minp.z, maxp.z-dim.z-1)
|
|
local p1 = {x=x,y=y,z=z}
|
|
local p2 = {x = x+dim.x+1, y = y+dim.y+1, z = z+dim.z+1}
|
|
minetest.log("verbose","[mcl_dungeons] size=" ..minetest.pos_to_string(dim) .. ", emerge from "..minetest.pos_to_string(p1) .. " to " .. minetest.pos_to_string(p2))
|
|
emerge_area(p1, p2, ecb_spawn_dungeon, {p1=p1, p2=p2, dim=dim, pr=pr})
|
|
end
|
|
end
|
|
|
|
function mcl_dungeons.spawn_dungeon(p1, _, pr)
|
|
if not p1 or not pr or not p1.x or not p1.y or not p1.z then return end
|
|
local dim = dungeonsizes[pr:next(1, #dungeonsizes)]
|
|
local p2 = {x = p1.x+dim.x+1, y = p1.y+dim.y+1, z = p1.z+dim.z+1}
|
|
minetest.log("verbose","[mcl_dungeons] size=" ..minetest.pos_to_string(dim) .. ", emerge from "..minetest.pos_to_string(p1) .. " to " .. minetest.pos_to_string(p2))
|
|
emerge_area(p1, p2, ecb_spawn_dungeon, {p1=p1, p2=p2, dim=dim, pr=pr, dontcheck=true})
|
|
end
|
|
|
|
mcl_mapgen_core.register_generator("dungeons", nil, dungeons_nodes, 999999)
|