Browse Source

Add loot to dungeons (#1921)

sfan5 6 years ago
parent
commit
49cc4c7c63

+ 4 - 3
.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

+ 32 - 0
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
 ---------
 

+ 11 - 0
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)

+ 1 - 0
mods/dungeon_loot/depends.txt

@@ -0,0 +1 @@
+default

+ 8 - 0
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")

+ 24 - 0
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

+ 62 - 0
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

+ 168 - 0
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)