Add loot to dungeons (#1921)

This commit is contained in:
sfan5 2017-10-27 13:34:41 +02:00 committed by GitHub
parent 36df80fc45
commit 49cc4c7c63
8 changed files with 310 additions and 3 deletions

@ -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

@ -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
---------

@ -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)

@ -0,0 +1 @@
default

@ -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")

@ -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

@ -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

@ -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)