Skip to content

Commit 49cc4c7

Browse files
authoredOct 27, 2017
Add loot to dungeons (#1921)
1 parent 36df80f commit 49cc4c7

File tree

8 files changed

+310
-3
lines changed

8 files changed

+310
-3
lines changed
 

Diff for: ‎.luacheckrc

+4-3
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ read_globals = {
77
"dump",
88
"vector",
99
"VoxelManip", "VoxelArea",
10-
"PseudoRandom", "ItemStack",
10+
"PseudoRandom", "PcgRandom",
11+
"ItemStack",
1112
"Settings",
1213
"unpack",
13-
-- Silence "accessing undefined field copy of global table".
14-
table = { fields = { "copy" } }
14+
-- Silence errors about custom table methods.
15+
table = { fields = { "copy", "indexof" } }
1516
}
1617

1718
-- Overwrites minetest.handle_node_drops

Diff for: ‎game_api.txt

+32
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,38 @@ The doors mod allows modders to register custom doors and trapdoors.
161161
groups = {choppy = 2, oddly_breakable_by_hand = 2, flammable = 2},
162162
sounds = default.node_sound_wood_defaults(), -- optional
163163

164+
Dungeon Loot API
165+
----------------
166+
167+
The mod that places chests with loot in dungeons provides an API to register additional loot.
168+
169+
`dungeon_loot.register(def)`
170+
171+
* Registers one or more loot items
172+
* `def` Can be a single [#Loot definition] or a list of them
173+
174+
`dungeon_loot.registered_loot`
175+
176+
* Table of all registered loot, not to be modified manually
177+
178+
### Loot definition
179+
180+
name = "item:name",
181+
chance = 0.5,
182+
-- ^ chance value from 0.0 to 1.0 that the item will appear in the chest when chosen
183+
-- due to an extra step in the selection process, 0.5 does not(!) mean that
184+
-- on average every second chest will have this item
185+
count = {1, 4},
186+
-- ^ table with minimum and maximum amounts of this item
187+
-- optional, defaults to always single item
188+
y = {-32768, -512},
189+
-- ^ table with minimum and maximum heights this item can be found at
190+
-- optional, defaults to no height restrictions
191+
types = {"desert"},
192+
-- ^ table with types of dungeons this item can be found in
193+
-- supported types: "normal" (the cobble/mossycobble one), "sandstone", "desert"
194+
-- optional, defaults to no type restrictions
195+
164196
Fence API
165197
---------
166198

Diff for: ‎mods/dungeon_loot/README.txt

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Minetest Game mod: dungeon_loot
2+
===============================
3+
Adds randomly generated chests with some "loot" to generated dungeons,
4+
an API to register additional loot is provided.
5+
Only works if dungeons are actually enabled in mapgen flags.
6+
7+
License information can be found in license.txt
8+
9+
Authors of source code
10+
----------------------
11+
Originally by sfan5 (MIT)

Diff for: ‎mods/dungeon_loot/depends.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
default

Diff for: ‎mods/dungeon_loot/init.lua

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
dungeon_loot = {}
2+
3+
dungeon_loot.CHESTS_MIN = 0 -- not necessarily in a single dungeon
4+
dungeon_loot.CHESTS_MAX = 2
5+
dungeon_loot.STACKS_PER_CHEST_MAX = 8
6+
7+
dofile(minetest.get_modpath("dungeon_loot") .. "/loot.lua")
8+
dofile(minetest.get_modpath("dungeon_loot") .. "/mapgen.lua")

Diff for: ‎mods/dungeon_loot/license.txt

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
License of source code
2+
----------------------
3+
4+
The MIT License (MIT)
5+
Copyright (C) 2017 sfan5
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy of this
8+
software and associated documentation files (the "Software"), to deal in the Software
9+
without restriction, including without limitation the rights to use, copy, modify, merge,
10+
publish, distribute, sublicense, and/or sell copies of the Software, and to permit
11+
persons to whom the Software is furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all copies or
14+
substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
17+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
18+
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
19+
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
20+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21+
DEALINGS IN THE SOFTWARE.
22+
23+
For more details:
24+
https://opensource.org/licenses/MIT

Diff for: ‎mods/dungeon_loot/loot.lua

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
dungeon_loot.registered_loot = {
2+
-- buckets
3+
{name = "bucket:bucket_empty", chance = 0.55},
4+
-- water in deserts or above ground, lava otherwise
5+
{name = "bucket:bucket_water", chance = 0.45, types = {"sandstone", "desert"}},
6+
{name = "bucket:bucket_water", chance = 0.45, y = {0, 32768}, types = {"normal"}},
7+
{name = "bucket:bucket_lava", chance = 0.45, y = {-32768, -1}, types = {"normal"}},
8+
9+
-- various items
10+
{name = "default:stick", chance = 0.6, count = {3, 6}},
11+
{name = "default:flint", chance = 0.4, count = {1, 3}},
12+
{name = "vessels:glass_fragments", chance = 0.35, count = {1, 4}},
13+
{name = "carts:rail", chance = 0.35, count = {1, 6}},
14+
15+
-- farming / consumable
16+
{name = "farming:string", chance = 0.5, count = {1, 8}},
17+
{name = "farming:wheat", chance = 0.5, count = {2, 5}},
18+
{name = "default:apple", chance = 0.4, count = {1, 4}},
19+
{name = "farming:seed_cotton", chance = 0.4, count = {1, 4}, types = {"normal"}},
20+
{name = "default:cactus", chance = 0.4, count = {1, 4}, types = {"sandstone", "desert"}},
21+
22+
-- minerals
23+
{name = "default:coal_lump", chance = 0.9, count = {1, 12}},
24+
{name = "default:gold_ingot", chance = 0.5},
25+
{name = "default:steel_ingot", chance = 0.4, count = {1, 6}},
26+
{name = "default:mese_crystal", chance = 0.1, count = {2, 3}},
27+
28+
-- tools
29+
{name = "default:sword_wood", chance = 0.6},
30+
{name = "default:pick_stone", chance = 0.3},
31+
{name = "default:axe_diamond", chance = 0.05},
32+
33+
-- natural materials
34+
{name = "default:sand", chance = 0.8, count = {4, 32}, y = {-64, 32768}, types = {"normal"}},
35+
{name = "default:desert_sand", chance = 0.8, count = {4, 32}, y = {-64, 32768}, types = {"sandstone"}},
36+
{name = "default:desert_cobble", chance = 0.8, count = {4, 32}, types = {"desert"}},
37+
{name = "default:dirt", chance = 0.6, count = {2, 16}, y = {-64, 32768}},
38+
{name = "default:obsidian", chance = 0.25, count = {1, 3}, y = {-32768, -512}},
39+
{name = "default:mese", chance = 0.15, y = {-32768, -512}},
40+
}
41+
42+
function dungeon_loot.register(t)
43+
if t.name ~= nil then
44+
t = {t} -- single entry
45+
end
46+
for _, loot in ipairs(t) do
47+
table.insert(dungeon_loot.registered_loot, loot)
48+
end
49+
end
50+
51+
function dungeon_loot._internal_get_loot(pos_y, dungeontype)
52+
-- filter by y pos and type
53+
local ret = {}
54+
for _, l in ipairs(dungeon_loot.registered_loot) do
55+
if l.y == nil or (pos_y >= l.y[1] and pos_y <= l.y[2]) then
56+
if l.types == nil or table.indexof(l.types, dungeontype) ~= -1 then
57+
table.insert(ret, l)
58+
end
59+
end
60+
end
61+
return ret
62+
end

Diff for: ‎mods/dungeon_loot/mapgen.lua

+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
minetest.set_gen_notify({dungeon = true, temple = true})
2+
3+
local function noise3d_integer(noise, pos)
4+
return math.abs(math.floor(noise:get3d(pos) * 0x7fffffff))
5+
end
6+
7+
local function random_sample(rand, list, count)
8+
local ret = {}
9+
for n = 1, count do
10+
local idx = rand:next(1, #list)
11+
table.insert(ret, list[idx])
12+
table.remove(list, idx)
13+
end
14+
return ret
15+
end
16+
17+
local function find_walls(cpos)
18+
local wall = minetest.registered_aliases["mapgen_cobble"]
19+
local wall_alt = minetest.registered_aliases["mapgen_mossycobble"]
20+
local wall_ss = minetest.registered_aliases["mapgen_sandstonebrick"]
21+
local wall_ds = minetest.registered_aliases["mapgen_desert_stone"]
22+
local is_wall = function(node)
23+
return table.indexof({wall, wall_alt, wall_ss, wall_ds}, node.name) ~= -1
24+
end
25+
26+
local dirs = {{x=1, z=0}, {x=-1, z=0}, {x=0, z=1}, {x=0, z=-1}}
27+
local get_node = minetest.get_node
28+
29+
local ret = {}
30+
local mindist = {x=0, z=0}
31+
local min = function(a, b) return a ~= 0 and math.min(a, b) or b end
32+
local wallnode
33+
for _, dir in ipairs(dirs) do
34+
for i = 1, 9 do -- 9 = max room size / 2
35+
local pos = vector.add(cpos, {x=dir.x*i, y=0, z=dir.z*i})
36+
37+
-- continue in that direction until we find a wall-like node
38+
local node = get_node(pos)
39+
if is_wall(node) then
40+
local front_below = vector.subtract(pos, {x=dir.x, y=1, z=dir.z})
41+
local above = vector.add(pos, {x=0, y=1, z=0})
42+
43+
-- check that it:
44+
--- is at least 2 nodes high (not a staircase)
45+
--- has a floor
46+
if is_wall(get_node(front_below)) and is_wall(get_node(above)) then
47+
table.insert(ret, {pos = pos, facing = {x=-dir.x, y=0, z=-dir.z}})
48+
if dir.z == 0 then
49+
mindist.x = min(mindist.x, i-1)
50+
else
51+
mindist.z = min(mindist.z, i-1)
52+
end
53+
wallnode = node.name
54+
end
55+
-- abort even if it wasn't a wall cause something is in the way
56+
break
57+
end
58+
end
59+
end
60+
61+
local mapping = {
62+
[wall_ss] = "sandstone",
63+
[wall_ds] = "desert"
64+
}
65+
return {
66+
walls = ret,
67+
size = {x=mindist.x*2, z=mindist.z*2},
68+
type = mapping[wallnode] or "normal"
69+
}
70+
end
71+
72+
local function populate_chest(pos, rand, dungeontype)
73+
--minetest.chat_send_all("chest placed at " .. minetest.pos_to_string(pos) .. " [" .. dungeontype .. "]")
74+
--minetest.add_node(vector.add(pos, {x=0, y=1, z=0}), {name="default:torch", param2=1})
75+
76+
local item_list = dungeon_loot._internal_get_loot(pos.y, dungeontype)
77+
-- take random (partial) sample of all possible items
78+
assert(#item_list >= dungeon_loot.STACKS_PER_CHEST_MAX)
79+
item_list = random_sample(rand, item_list, dungeon_loot.STACKS_PER_CHEST_MAX)
80+
81+
-- apply chances / randomized amounts and collect resulting items
82+
local items = {}
83+
for _, loot in ipairs(item_list) do
84+
if rand:next(0, 1000) / 1000 <= loot.chance then
85+
local itemdef = minetest.registered_items[loot.name]
86+
local amount = 1
87+
if loot.count ~= nil then
88+
amount = rand:next(loot.count[1], loot.count[2])
89+
end
90+
91+
if itemdef.tool_capabilities then
92+
for n = 1, amount do
93+
local wear = rand:next(0.20 * 65535, 0.75 * 65535) -- 20% to 75% wear
94+
table.insert(items, ItemStack({name = loot.name, wear = wear}))
95+
end
96+
elseif itemdef.stack_max == 1 then
97+
-- not stackable, add separately
98+
for n = 1, amount do
99+
table.insert(items, loot.name)
100+
end
101+
else
102+
table.insert(items, ItemStack({name = loot.name, count = amount}))
103+
end
104+
end
105+
end
106+
107+
-- place items at random places in chest
108+
local inv = minetest.get_meta(pos):get_inventory()
109+
local listsz = inv:get_size("main")
110+
assert(listsz >= #items)
111+
for _, item in ipairs(items) do
112+
local index = rand:next(1, listsz)
113+
if inv:get_stack("main", index):is_empty() then
114+
inv:set_stack("main", index, item)
115+
else
116+
inv:add_item("main", item) -- space occupied, just put it anywhere
117+
end
118+
end
119+
end
120+
121+
122+
minetest.register_on_generated(function(minp, maxp, blockseed)
123+
local gennotify = minetest.get_mapgen_object("gennotify")
124+
local poslist = gennotify["dungeon"] or {}
125+
for _, entry in ipairs(gennotify["temple"] or {}) do
126+
table.insert(poslist, entry)
127+
end
128+
if #poslist == 0 then return end
129+
130+
local noise = minetest.get_perlin(10115, 4, 0.5, 1)
131+
local rand = PcgRandom(noise3d_integer(noise, poslist[1]))
132+
133+
local candidates = {}
134+
-- process at most 16 rooms to keep runtime of this predictable
135+
local num_process = math.min(#poslist, 16)
136+
for i = 1, num_process do
137+
local room = find_walls(poslist[i])
138+
-- skip small rooms and everything that doesn't at least have 3 walls
139+
if math.min(room.size.x, room.size.z) >= 4 and #room.walls >= 3 then
140+
table.insert(candidates, room)
141+
end
142+
end
143+
144+
local num_chests = rand:next(dungeon_loot.CHESTS_MIN, dungeon_loot.CHESTS_MAX)
145+
num_chests = math.min(#candidates, num_chests)
146+
local rooms = random_sample(rand, candidates, num_chests)
147+
148+
for _, room in ipairs(rooms) do
149+
-- choose place somewhere in front of any of the walls
150+
local wall = room.walls[rand:next(1, #room.walls)]
151+
local v, vi -- vector / axis that runs alongside the wall
152+
if wall.facing.x ~= 0 then
153+
v, vi = {x=0, y=0, z=1}, "z"
154+
else
155+
v, vi = {x=1, y=0, z=0}, "x"
156+
end
157+
local chestpos = vector.add(wall.pos, wall.facing)
158+
local off = rand:next(-room.size[vi]/2 + 1, room.size[vi]/2 - 1)
159+
chestpos = vector.add(chestpos, vector.multiply(v, off))
160+
161+
if minetest.get_node(chestpos).name == "air" then
162+
-- make it face inwards to the room
163+
local facedir = minetest.dir_to_facedir(vector.multiply(wall.facing, -1))
164+
minetest.add_node(chestpos, {name = "default:chest", param2 = facedir})
165+
populate_chest(chestpos, PcgRandom(noise3d_integer(noise, chestpos)), room.type)
166+
end
167+
end
168+
end)

1 commit comments

Comments
 (1)

Curtico commented on Oct 29, 2017

@Curtico

Hey that's awesome I'm gonna love getting that loot!

Please sign in to comment.