Add loot to dungeons (#1921)
authorsfan5 <sfan5@live.de>
Fri, 27 Oct 2017 11:34:41 +0000 (13:34 +0200)
committerGitHub <noreply@github.com>
Fri, 27 Oct 2017 11:34:41 +0000 (13:34 +0200)
.luacheckrc
game_api.txt
mods/dungeon_loot/README.txt [new file with mode: 0644]
mods/dungeon_loot/depends.txt [new file with mode: 0644]
mods/dungeon_loot/init.lua [new file with mode: 0644]
mods/dungeon_loot/license.txt [new file with mode: 0644]
mods/dungeon_loot/loot.lua [new file with mode: 0644]
mods/dungeon_loot/mapgen.lua [new file with mode: 0644]

index 3c7ec693569c305163c7f3938ed902a78b3b4d63..c3df64be4954f481d90a63fd0f6616ccd896fff2 100644 (file)
@@ -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
index 1a0e252a652ae3607defe7b87648c91a67c7d54d..379e3f853cab624097943ace1638a5e5cced6f3b 100644 (file)
@@ -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 (file)
index 0000000..c500d25
--- /dev/null
@@ -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 (file)
index 0000000..4ad96d5
--- /dev/null
@@ -0,0 +1 @@
+default
diff --git a/mods/dungeon_loot/init.lua b/mods/dungeon_loot/init.lua
new file mode 100644 (file)
index 0000000..9d8ac52
--- /dev/null
@@ -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 (file)
index 0000000..0af30a0
--- /dev/null
@@ -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 (file)
index 0000000..3fe0bff
--- /dev/null
@@ -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 (file)
index 0000000..9d42c53
--- /dev/null
@@ -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)