diff --git a/.luacheckrc b/.luacheckrc index 3c7ec693..c3df64be 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -7,11 +7,12 @@ read_globals = { "dump", "vector", "VoxelManip", "VoxelArea", - "PseudoRandom", "ItemStack", + "PseudoRandom", "PcgRandom", + "ItemStack", "Settings", "unpack", - -- Silence "accessing undefined field copy of global table". - table = { fields = { "copy" } } + -- Silence errors about custom table methods. + table = { fields = { "copy", "indexof" } } } -- Overwrites minetest.handle_node_drops diff --git a/game_api.txt b/game_api.txt index 1a0e252a..379e3f85 100644 --- a/game_api.txt +++ b/game_api.txt @@ -161,6 +161,38 @@ The doors mod allows modders to register custom doors and trapdoors. groups = {choppy = 2, oddly_breakable_by_hand = 2, flammable = 2}, sounds = default.node_sound_wood_defaults(), -- optional +Dungeon Loot API +---------------- + +The mod that places chests with loot in dungeons provides an API to register additional loot. + +`dungeon_loot.register(def)` + + * Registers one or more loot items + * `def` Can be a single [#Loot definition] or a list of them + +`dungeon_loot.registered_loot` + + * Table of all registered loot, not to be modified manually + +### Loot definition + + name = "item:name", + chance = 0.5, + -- ^ chance value from 0.0 to 1.0 that the item will appear in the chest when chosen + -- due to an extra step in the selection process, 0.5 does not(!) mean that + -- on average every second chest will have this item + count = {1, 4}, + -- ^ table with minimum and maximum amounts of this item + -- optional, defaults to always single item + y = {-32768, -512}, + -- ^ table with minimum and maximum heights this item can be found at + -- optional, defaults to no height restrictions + types = {"desert"}, + -- ^ table with types of dungeons this item can be found in + -- supported types: "normal" (the cobble/mossycobble one), "sandstone", "desert" + -- optional, defaults to no type restrictions + Fence API --------- diff --git a/mods/dungeon_loot/README.txt b/mods/dungeon_loot/README.txt new file mode 100644 index 00000000..c500d255 --- /dev/null +++ b/mods/dungeon_loot/README.txt @@ -0,0 +1,11 @@ +Minetest Game mod: dungeon_loot +=============================== +Adds randomly generated chests with some "loot" to generated dungeons, +an API to register additional loot is provided. +Only works if dungeons are actually enabled in mapgen flags. + +License information can be found in license.txt + +Authors of source code +---------------------- +Originally by sfan5 (MIT) diff --git a/mods/dungeon_loot/depends.txt b/mods/dungeon_loot/depends.txt new file mode 100644 index 00000000..4ad96d51 --- /dev/null +++ b/mods/dungeon_loot/depends.txt @@ -0,0 +1 @@ +default diff --git a/mods/dungeon_loot/init.lua b/mods/dungeon_loot/init.lua new file mode 100644 index 00000000..9d8ac52f --- /dev/null +++ b/mods/dungeon_loot/init.lua @@ -0,0 +1,8 @@ +dungeon_loot = {} + +dungeon_loot.CHESTS_MIN = 0 -- not necessarily in a single dungeon +dungeon_loot.CHESTS_MAX = 2 +dungeon_loot.STACKS_PER_CHEST_MAX = 8 + +dofile(minetest.get_modpath("dungeon_loot") .. "/loot.lua") +dofile(minetest.get_modpath("dungeon_loot") .. "/mapgen.lua") diff --git a/mods/dungeon_loot/license.txt b/mods/dungeon_loot/license.txt new file mode 100644 index 00000000..0af30a0c --- /dev/null +++ b/mods/dungeon_loot/license.txt @@ -0,0 +1,24 @@ +License of source code +---------------------- + +The MIT License (MIT) +Copyright (C) 2017 sfan5 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +For more details: +https://opensource.org/licenses/MIT diff --git a/mods/dungeon_loot/loot.lua b/mods/dungeon_loot/loot.lua new file mode 100644 index 00000000..3fe0bff6 --- /dev/null +++ b/mods/dungeon_loot/loot.lua @@ -0,0 +1,62 @@ +dungeon_loot.registered_loot = { + -- buckets + {name = "bucket:bucket_empty", chance = 0.55}, + -- water in deserts or above ground, lava otherwise + {name = "bucket:bucket_water", chance = 0.45, types = {"sandstone", "desert"}}, + {name = "bucket:bucket_water", chance = 0.45, y = {0, 32768}, types = {"normal"}}, + {name = "bucket:bucket_lava", chance = 0.45, y = {-32768, -1}, types = {"normal"}}, + + -- various items + {name = "default:stick", chance = 0.6, count = {3, 6}}, + {name = "default:flint", chance = 0.4, count = {1, 3}}, + {name = "vessels:glass_fragments", chance = 0.35, count = {1, 4}}, + {name = "carts:rail", chance = 0.35, count = {1, 6}}, + + -- farming / consumable + {name = "farming:string", chance = 0.5, count = {1, 8}}, + {name = "farming:wheat", chance = 0.5, count = {2, 5}}, + {name = "default:apple", chance = 0.4, count = {1, 4}}, + {name = "farming:seed_cotton", chance = 0.4, count = {1, 4}, types = {"normal"}}, + {name = "default:cactus", chance = 0.4, count = {1, 4}, types = {"sandstone", "desert"}}, + + -- minerals + {name = "default:coal_lump", chance = 0.9, count = {1, 12}}, + {name = "default:gold_ingot", chance = 0.5}, + {name = "default:steel_ingot", chance = 0.4, count = {1, 6}}, + {name = "default:mese_crystal", chance = 0.1, count = {2, 3}}, + + -- tools + {name = "default:sword_wood", chance = 0.6}, + {name = "default:pick_stone", chance = 0.3}, + {name = "default:axe_diamond", chance = 0.05}, + + -- natural materials + {name = "default:sand", chance = 0.8, count = {4, 32}, y = {-64, 32768}, types = {"normal"}}, + {name = "default:desert_sand", chance = 0.8, count = {4, 32}, y = {-64, 32768}, types = {"sandstone"}}, + {name = "default:desert_cobble", chance = 0.8, count = {4, 32}, types = {"desert"}}, + {name = "default:dirt", chance = 0.6, count = {2, 16}, y = {-64, 32768}}, + {name = "default:obsidian", chance = 0.25, count = {1, 3}, y = {-32768, -512}}, + {name = "default:mese", chance = 0.15, y = {-32768, -512}}, +} + +function dungeon_loot.register(t) + if t.name ~= nil then + t = {t} -- single entry + end + for _, loot in ipairs(t) do + table.insert(dungeon_loot.registered_loot, loot) + end +end + +function dungeon_loot._internal_get_loot(pos_y, dungeontype) + -- filter by y pos and type + local ret = {} + for _, l in ipairs(dungeon_loot.registered_loot) do + if l.y == nil or (pos_y >= l.y[1] and pos_y <= l.y[2]) then + if l.types == nil or table.indexof(l.types, dungeontype) ~= -1 then + table.insert(ret, l) + end + end + end + return ret +end diff --git a/mods/dungeon_loot/mapgen.lua b/mods/dungeon_loot/mapgen.lua new file mode 100644 index 00000000..9d42c530 --- /dev/null +++ b/mods/dungeon_loot/mapgen.lua @@ -0,0 +1,168 @@ +minetest.set_gen_notify({dungeon = true, temple = true}) + +local function noise3d_integer(noise, pos) + return math.abs(math.floor(noise:get3d(pos) * 0x7fffffff)) +end + +local function random_sample(rand, list, count) + local ret = {} + for n = 1, count do + local idx = rand:next(1, #list) + table.insert(ret, list[idx]) + table.remove(list, idx) + end + return ret +end + +local function find_walls(cpos) + local wall = minetest.registered_aliases["mapgen_cobble"] + local wall_alt = minetest.registered_aliases["mapgen_mossycobble"] + local wall_ss = minetest.registered_aliases["mapgen_sandstonebrick"] + local wall_ds = minetest.registered_aliases["mapgen_desert_stone"] + local is_wall = function(node) + return table.indexof({wall, wall_alt, wall_ss, wall_ds}, node.name) ~= -1 + end + + local dirs = {{x=1, z=0}, {x=-1, z=0}, {x=0, z=1}, {x=0, z=-1}} + local get_node = minetest.get_node + + local ret = {} + local mindist = {x=0, z=0} + local min = function(a, b) return a ~= 0 and math.min(a, b) or b end + local wallnode + for _, dir in ipairs(dirs) do + for i = 1, 9 do -- 9 = max room size / 2 + local pos = vector.add(cpos, {x=dir.x*i, y=0, z=dir.z*i}) + + -- continue in that direction until we find a wall-like node + local node = get_node(pos) + if is_wall(node) then + local front_below = vector.subtract(pos, {x=dir.x, y=1, z=dir.z}) + local above = vector.add(pos, {x=0, y=1, z=0}) + + -- check that it: + --- is at least 2 nodes high (not a staircase) + --- has a floor + if is_wall(get_node(front_below)) and is_wall(get_node(above)) then + table.insert(ret, {pos = pos, facing = {x=-dir.x, y=0, z=-dir.z}}) + if dir.z == 0 then + mindist.x = min(mindist.x, i-1) + else + mindist.z = min(mindist.z, i-1) + end + wallnode = node.name + end + -- abort even if it wasn't a wall cause something is in the way + break + end + end + end + + local mapping = { + [wall_ss] = "sandstone", + [wall_ds] = "desert" + } + return { + walls = ret, + size = {x=mindist.x*2, z=mindist.z*2}, + type = mapping[wallnode] or "normal" + } +end + +local function populate_chest(pos, rand, dungeontype) + --minetest.chat_send_all("chest placed at " .. minetest.pos_to_string(pos) .. " [" .. dungeontype .. "]") + --minetest.add_node(vector.add(pos, {x=0, y=1, z=0}), {name="default:torch", param2=1}) + + local item_list = dungeon_loot._internal_get_loot(pos.y, dungeontype) + -- take random (partial) sample of all possible items + assert(#item_list >= dungeon_loot.STACKS_PER_CHEST_MAX) + item_list = random_sample(rand, item_list, dungeon_loot.STACKS_PER_CHEST_MAX) + + -- apply chances / randomized amounts and collect resulting items + local items = {} + for _, loot in ipairs(item_list) do + if rand:next(0, 1000) / 1000 <= loot.chance then + local itemdef = minetest.registered_items[loot.name] + local amount = 1 + if loot.count ~= nil then + amount = rand:next(loot.count[1], loot.count[2]) + end + + if itemdef.tool_capabilities then + for n = 1, amount do + local wear = rand:next(0.20 * 65535, 0.75 * 65535) -- 20% to 75% wear + table.insert(items, ItemStack({name = loot.name, wear = wear})) + end + elseif itemdef.stack_max == 1 then + -- not stackable, add separately + for n = 1, amount do + table.insert(items, loot.name) + end + else + table.insert(items, ItemStack({name = loot.name, count = amount})) + end + end + end + + -- place items at random places in chest + local inv = minetest.get_meta(pos):get_inventory() + local listsz = inv:get_size("main") + assert(listsz >= #items) + for _, item in ipairs(items) do + local index = rand:next(1, listsz) + if inv:get_stack("main", index):is_empty() then + inv:set_stack("main", index, item) + else + inv:add_item("main", item) -- space occupied, just put it anywhere + end + end +end + + +minetest.register_on_generated(function(minp, maxp, blockseed) + local gennotify = minetest.get_mapgen_object("gennotify") + local poslist = gennotify["dungeon"] or {} + for _, entry in ipairs(gennotify["temple"] or {}) do + table.insert(poslist, entry) + end + if #poslist == 0 then return end + + local noise = minetest.get_perlin(10115, 4, 0.5, 1) + local rand = PcgRandom(noise3d_integer(noise, poslist[1])) + + local candidates = {} + -- process at most 16 rooms to keep runtime of this predictable + local num_process = math.min(#poslist, 16) + for i = 1, num_process do + local room = find_walls(poslist[i]) + -- skip small rooms and everything that doesn't at least have 3 walls + if math.min(room.size.x, room.size.z) >= 4 and #room.walls >= 3 then + table.insert(candidates, room) + end + end + + local num_chests = rand:next(dungeon_loot.CHESTS_MIN, dungeon_loot.CHESTS_MAX) + num_chests = math.min(#candidates, num_chests) + local rooms = random_sample(rand, candidates, num_chests) + + for _, room in ipairs(rooms) do + -- choose place somewhere in front of any of the walls + local wall = room.walls[rand:next(1, #room.walls)] + local v, vi -- vector / axis that runs alongside the wall + if wall.facing.x ~= 0 then + v, vi = {x=0, y=0, z=1}, "z" + else + v, vi = {x=1, y=0, z=0}, "x" + end + local chestpos = vector.add(wall.pos, wall.facing) + local off = rand:next(-room.size[vi]/2 + 1, room.size[vi]/2 - 1) + chestpos = vector.add(chestpos, vector.multiply(v, off)) + + if minetest.get_node(chestpos).name == "air" then + -- make it face inwards to the room + local facedir = minetest.dir_to_facedir(vector.multiply(wall.facing, -1)) + minetest.add_node(chestpos, {name = "default:chest", param2 = facedir}) + populate_chest(chestpos, PcgRandom(noise3d_integer(noise, chestpos)), room.type) + end + end +end)