-- by use of the enchanter, it is possible to reveal a random
-- recipe and enscribe it on a sheet of paper. these sheets of
-- paper can then bound together into books, combining like
-- recipes
sorcery.cookbook = {}
local constants = {
-- do not show recipes for items in these groups
exclude_groups = {
};
exclude_names = {
'stairs';
'slab';
'slope';
};
-- do not show recipes from this namespace
blacklist_mods = {
'group'; -- WHY IS THIS NECESSARY
'moreblocks'; -- too much noise
};
recipes_per_cookbook_page = 3;
group_ids = {
wood = { caption = 'Any Wood', cnitem = 'default:wood' };
tree = { caption = 'Any Tree', cnitem = 'default:tree' };
leaves = { caption = 'Any Leaves', cnitem = 'default:leaves' };
stone = { caption = 'Any Stone', cnitem = 'default:stone' };
dye = { caption = 'Any Dye', cnitem = 'dye:black' };
bone = { caption = 'Any Bone', cnitem = 'bonemeal:bone' };
vessel = { caption = 'Any Bottle', cnitem = 'vessels:glass_bottle' };
flower = { caption = 'Any Flower', cnitem = 'flowers:rose' };
mushroom = { caption = 'Any Mushroom', cnitem = 'flowers:mushroom_brown' };
water_bucket = { caption = 'Water Bucket', cnitem = 'bucket:bucket_water' };
sorcery_ley_cable = { caption = 'Cable', cnitem = 'sorcery:cable_vidrium' };
};
}
sorcery.cookbook.constants = constants
local slot3x3 = {
{0,0}, {1,0}, {2,0};
{0,1}, {1,1}, {2,1};
{0,2}, {1,2}, {2,2};
}
local props_builtin = function(item)
local props = minetest.registered_items[item]._sorcery
if props and props.recipe then
return props.recipe
end
return {}
end
local modofname = function(id)
local sep = string.find(id,':')
if sep == nil then return nil end -- uh oh
return string.sub(id, 1, sep - 1)
end
local pick_builtin = function(kind) return function(restrict)
-- ow ow ow ow ow ow ow
local names = {}
for k in pairs(minetest.registered_items) do
local rec = minetest.get_craft_recipe(k)
if rec.items ~= nil and (rec.method == kind or (rec.method == 'shapeless' and kind == 'normal')) then -- is this last bit necessary?
local excluded = false
for _,n in pairs(constants.exclude_names) do
if string.find(k,n) ~= nil then
excluded = true break end
end
if not excluded then for _,g in pairs(constants.exclude_groups) do
if minetest.get_item_group(k, g) > 0 then
excluded = true break end
end end
local props = minetest.registered_items[k]._sorcery
local module = modofname(k)
if not (excluded
or sorcery.lib.tbl.has(constants.blacklist_mods,module)
or (props and props.recipe and props.recipe.secret)
or (restrict and (
(restrict.mod and module ~= restrict.mod)
or (restrict.group and (minetest.get_item_group(k, restrict.group) == 0))
))) then names[#names + 1] = k end
end
end
return names[math.random(#names)]
end end
local find_builtin = function(method,kind)
return function(out)
local rec = {}
local crec = sorcery.lib.tbl.walk(minetest.registered_items[out],{'_sorcery','recipe','canonical',kind})
local w=0, lst
if crec then
lst = {}
for i,v in pairs(crec) do
if #v > w then w = #v end
for j,n in pairs(v) do
lst[#lst+1] = n
end
end
else
-- WHY IS THIS INTERFACE SO CLUMSY
local all,i = minetest.get_all_craft_recipes(out), nil
for _,r in pairs(all) do
if r.method == method and r.items and #r.items>0 then
i = r break
end
end
if i == nil or i.items == nil or #i.items == 0 then return nil end
w = (i.width == 0) and 3 or i.width
lst = i.items
end
-- for j=1,#i.items do
for j,item in pairs(lst) do
local row = math.floor((j-1) / w)
local col = (j-1) % w
if item then
rec[1 + (row * 3) + col] = item
end
end
return rec
end
end
local function group_eval(i)
if string.sub(i,1,6) == 'group:' then
local g = string.sub(i,7)
if constants.group_ids[g] then
return constants.group_ids[g].cnitem,
constants.group_ids[g].caption
end
for i,v in pairs(minetest.registered_items) do
if minetest.get_item_group(i, g) > 0 then
return i, v.description
end
end
return i
end
return i
end
local function desc_builtin(i)
local desc
i, desc = group_eval(i)
-- print('describing ',i,dump(minetest.registered_items[i]))
local s = ItemStack(i)
if not minetest.registered_items[s:get_name()] then
minetest.log('WARNING: unknown item in recipe ' .. i)
return 'Unknown Item'
end
if not desc then desc = minetest.registered_items[s:get_name()].description end
if not desc then return 'Peculiar Item' end
local eol = string.find(desc,'\n')
if eol then desc = string.sub(desc,1,eol-1) end
if s:get_count() > 1 then
desc = string.format("%s (%u)",desc,s:get_count())
end
return desc
end;
local bookadjs = { -- sets are in reverse order!
{'Celestial', 'Divine', 'Inspired', 'Heavenly';
'Mystic', 'Diabolic', 'Luminous', 'Forsaken'};
{'Dark', 'Perfected', 'Flawless', 'Unthinkable';
'Impossible', 'Worrisome', 'Unimpeachable'};
{'Splendid', 'Magnificent', 'Sublime', 'Grand';
'Beneficent', 'Mysterious', 'Peculiar', 'Eerie';
'Fulsome', 'Fearsome', 'Curious', 'Fascinating';
'Notorious', 'Infamous'};
}
local cache = {
populate_grindables = function(cache)
if not cache.grindables then
cache.grindables = {}
for k,v in pairs(minetest.registered_items) do
if sorcery.itemclass.get(k, 'grindable') then
cache.grindables[#cache.grindables+1] = k
end
end
end
end;
}
sorcery.cookbook.classes = {
craft = {
name = 'Crafting Guide';
node = 'xdecor:workbench';
booksuf = 'Codex';
w = 3, h = 3;
chance = 2;
slots = slot3x3;
pick = pick_builtin('normal');
find = find_builtin('normal','craft');
props = props_builtin;
apply_exclusions = true;
};
-- smelt = {
-- w = 3, h = 3;
-- slots = slot3x3;
-- };
cook = {
name = 'Cooking Recipe';
node = 'default:furnace';
booksuf = 'Cookbook';
w = 1, h = 1;
chance = 3;
slots = {{-0.2,0}};
pick = pick_builtin('cooking');
find = find_builtin('cooking','cook');
props = props_builtin;
apply_exclusions = true;
};
infuse = {
name = 'Infusion Recipe';
node = 'sorcery:infuser';
booksuf = 'Pharmacopeia';
w = 1, h = 2;
chance = 4;
slots = {
{0,0};
{0,1};
};
pick = function(restrict)
-- TODO make sure affinity restrictions match
return sorcery.data.infusions[math.random(#sorcery.data.infusions)].output
end;
title = function(output)
for _,i in pairs(sorcery.data.infusions) do
if i.output == output then
if i._proto and i._proto.name
then return i._proto.name
else break end
end
end
return 'Mysterious Potion'
end;
find = function(out)
for _,i in pairs(sorcery.data.infusions) do
if i.output == out then
return { i.infuse, i.into }
end
end
end;
props = function(out)
for _,i in pairs(sorcery.data.infusions) do
if i.output == out then
if i.recipe then return i.recipe else return {} end
end
end
end;
};
grind = {
name = 'Milling Guide';
node = 'sorcery:mill';
booksuf = 'Manual';
chance = 1;
w = 1, h = 2;
pick = function(restrict)
cache:populate_grindables()
local i = cache.grindables[math.random(#cache.grindables)]
local pd = sorcery.itemclass.get(i, 'grindable')
return pd.powder
end;
props = props_builtin;
slots = {
{0,1},
{0,0};
};
find = function(out)
cache:populate_grindables()
for _,v in pairs(cache.grindables) do
local g = sorcery.itemclass.get(v,'grindable')
if g.powder == out then
if g.grindcost then
v = v .. ' ' .. tostring(g.grindcost)
end
local mbh = sorcery.lib.tbl.keys(sorcery.data.metals)
table.sort(mbh, function(a,b)
return sorcery.data.metals[a].hardness < sorcery.data.metals[b].hardness
end)
for _,metal in pairs(mbh) do
local md = sorcery.data.metals[metal]
if ((not md.no_tools) or md.grindhead) and md.hardness >= g.hardness then
return {v, 'sorcery:mill_grindhead_' .. metal}
end
end
return {v,''} -- !!
end
end
end;
};
-- wand = {
-- booksuf = 'Grimoire';
-- }
enchant = {
name = 'Enchantment Matrix';
node = 'sorcery:enchanter';
booksuf = 'Grimoire';
drawslots = false;
chance = 6;
w = 2, h = 2;
pick = function(restrict)
-- TODO make sure affinity restrictions match
local names = {}
for k,v in pairs(sorcery.data.enchants) do
if v.recipe then names[#names+1] = k end
end
return names[math.random(#names)]
end;
icon = function(name)
return 'sorcery_enchant_' .. name .. '.png'
end;
find = function(name)
local rec = {}
local en = sorcery.data.enchants[name]
if not en then return nil end
en = en.recipe
for i,e in pairs(en) do
if e.lens then
rec[i] = 'sorcery:lens_' .. e.lens .. '_' .. e.gem
end
end
return rec
end;
props = function(name)
return sorcery.data.enchants[name].info or {}
end;
slots = {
{0.5,0};
{0,1}, {1,1}
};
title = function(name) return sorcery.data.enchants[name].name end;
outdesc = function(name,suffix)
local e = sorcery.data.enchants[name]
return sorcery.lib.ui.tooltip {
title = e.name;
desc = sorcery.lib.str.capitalize(e.desc);
color = sorcery.lib.color(e.tone):readable();
}
end;
};
-- spells = {
-- booksuf = 'Spellbook';
-- slots = {
-- {0,0}, {1,0};
-- {0,1}, {1,1};
-- };
-- };
}
local recipe_kinds = sorcery.cookbook.classes
local namebook = function(kind,author)
local name
if kind then name = recipe_kinds[kind].booksuf
else name = 'Cyclopedia' end
for _,set in pairs(bookadjs) do
if math.random(3) == 1 then
name = set[math.random(#set)] .. ' ' .. name
end
end
return sorcery.lib.str.capitalize(author) .. "'s " .. name
end
sorcery.cookbook.pickrecipe = function(kind,restrict)
if kind == nil then
for k,v in pairs(recipe_kinds) do
if math.random(v.chance) == 1 then
kind = k break
end
end
if kind == nil then -- oh well, we tried
local rks = sorcery.lib.tbl.keys(recipe_kinds)
kind = rks[math.random(#rks)]
end
end
return recipe_kinds[kind].pick(restrict), kind
end
local render_recipe = function(kind,ingredients,result,notes_right)
local k = recipe_kinds[kind]
local t = ''
local props = k.props(result)
for i=1,#k.slots do
local ing = ingredients[i]
local x, y = k.slots[i][1], k.slots[i][2]
if ing and ing ~= '' then
local tt
if k.indesc then tt = k.indesc(ing) else tt = desc_builtin(ing) end
t = t .. string.format([[
item_image[%f,%f;1,1;%s]
tooltip[%f,%f;1,1;%s]
]], x,y, minetest.formspec_escape(group_eval(ing)),
x,y, minetest.formspec_escape(tt))
else
if k.drawslots == nil or k.drawslots then
t = string.format('box[%f,%f;0.1,0.1;#00000060]',x+0.45,y+0.45) .. t
end
end
end
local img, ot
if props.note then
local nx, ny, nw, nh
if notes_right then
nx = 5.25 ny = 0
nw = 4 nh = 3
else
nx = 0 ny = 3
nw = 4 nh = 1
end
t = t .. string.format([[
hypertext[%f,%f;%f,%f;note;<global valign=middle halign=justify size=20>%s]
]], nx,ny,nw,nh, minetest.formspec_escape(props.note))
end
if k.icon then img = k.icon(result) end
if k.outdesc then ot = k.outdesc(result) else ot = desc_builtin(result) end
-- image[%f,%f;1,1;gui_furnace_arrow_bg.png^[transformR270]
return t .. string.format([[
item_image[%f,%f;1,1;%s]tooltip[%f,%f;1,1;%s]
]] --[[box[%f,%f;1,1;#850083A0]] .. [[
%s[%f,%f;1,1;%s]
tooltip[%f,%f;1,1;%s]
]], k.w, k.h/2 - 0.5, k.node,
k.w, k.h/2 - 0.5, minetest.formspec_escape(minetest.registered_nodes[k.node].description),
-- k.w+1, k.h/2 - 0.5,
img and 'image' or 'item_image',
k.w+1.1, k.h/2 - 0.5, minetest.formspec_escape(img or result),
k.w+1.1, k.h/2 - 0.5, minetest.formspec_escape(ot))
end;
local retrieve_recipe = function(kind,out,notes_right)
local rec = recipe_kinds[kind]
local ing = rec.find(out)
return render_recipe(kind,ing,out,notes_right), rec.w, rec.h
end
sorcery.cookbook.setrecipe = function(stack,k,r,restrict)
local meta = stack:get_meta()
if not r then r,k = sorcery.cookbook.pickrecipe(k,restrict) end
local t = recipe_kinds[k]
meta:set_string('recipe_kind', k)
meta:set_string('recipe_name', r)
meta:set_string('description',
(t.title and t.title(r) or desc_builtin(r)) .. ' ' .. t.name)
end
minetest.register_craftitem('sorcery:recipe', {
description = 'Recipe';
inventory_image = 'default_paper.png'; -- fixme
groups = { flammable = 1; book = 1; paper = 1; };
stack_max = 1;
on_use = function(itemstack, user, target)
local meta = itemstack:get_meta()
if not meta:contains('recipe_kind') then sorcery.cookbook.setrecipe(itemstack) end
local kind = meta:get_string('recipe_kind')
local spec, w, h = retrieve_recipe(kind,meta:get_string('recipe_name'))
minetest.show_formspec(user:get_player_name(), 'sorcery:recipe', string.format([[
size[%f,%f]
container[1,1] %s container_end[]
]], w + 4,h + 2,
spec))
return itemstack
end;
})
dungeon_loot.register {
name = 'sorcery:recipe';
chance = 0.9;
count = {1,7};
}
default.register_craft_metadata_copy('default:paper','sorcery:recipe')
-- this seems bugged; it doesn't like it when its item shows up in another
-- recipe. so we'll do it manually :/
-- default.register_craft_metadata_copy('default:book','sorcery:cookbook')
for i=1,8 do
local rcp = {}
for i=1,i do rcp[i] = 'sorcery:recipe' end
rcp[#rcp+1]='default:book' minetest.register_craft {
type = 'shapeless', recipe = rcp, output = 'sorcery:cookbook';
}
rcp[#rcp]='sorcery:cookbook' minetest.register_craft {
type = 'shapeless', recipe = rcp, output = 'sorcery:cookbook';
}
end
minetest.register_craft {
type = 'shapeless';
recipe = {
'sorcery:cookbook';
'default:book';
};
output = 'sorcery:cookbook';
};
-- erase cookbooks in the usual way
minetest.register_craft {
type = 'shapeless';
recipe = {
'sorcery:cookbook';
'bucket:bucket_water';
};
output = 'default:book';
replacements = {
{'bucket:bucket_water','bucket:bucket_empty'};
};
};
local m = sorcery.lib.marshal
local encbook, decbook = m.transcoder {
pages = m.g.array(8, m.g.struct {
kind = m.t.str;
name = m.t.str;
})
}
local bookprops = function(stack)
local meta = stack:get_meta()
if meta:contains('cookbook') then
return decbook(sorcery.lib.str.meta_dearmor(meta:get_string('cookbook'),true))
else return {pages={}} end
end
sorcery.cookbook.get = bookprops
sorcery.cookbook.set = function(stack,props)
local meta = stack:get_meta()
meta:set_string('cookbook', sorcery.lib.str.meta_armor(encbook(props),true))
end
sorcery.cookbook.defaults = {
indesc = desc_builtin;
outdesc = desc_builtin;
title = desc_builtin;
props = props_builtin;
pick = pick_builtin;
find = find_builtin;
}
sorcery.cookbook.recfn = function(class,fn)
local c = sorcery.cookbook.classes[class]
if c[fn] then return c[fn] end
return sorcery.cookbook.defaults[fn]
end
local bookform_ctx = {}
local bookform = function(stack,user)
bookform_ctx[user:get_player_name()] = user:get_wield_index()
local meta = stack:get_meta()
local book = bookprops(stack)
local pagect = math.ceil(#book.pages / constants.recipes_per_cookbook_page)
local curpage = meta:contains("pagenr") and meta:get_int("pagenr") or 1
local pgofs = (curpage - 1) * constants.recipes_per_cookbook_page
local form = string.format([[
formspec_version[3]
size[10,12]real_coordinates[true]
box[0,0;10,1;#580C39FF]label[0.4,0.5;%s]
style[pgind;border=false]
style[pgind:hovered;content_offset=0,0]
button[3,11;4,1;pgind;Page %u/%u]
]], minetest.formspec_escape(meta:get_string('description')),
curpage, pagect)
-- using an extremely dishonorable trick to fake centered text
if curpage > 1 then form = form .. 'button[0,11;3,1;pageback;< Back]' end
if curpage < pagect then form = form .. 'button[7,11;3,1;pagenext;Next >]' end
local coords = {
{0.85,1.3};
{0.85,4.5};
{0.85,7.7};
-- {0,1.3}, {4, 1.3};
-- {0,4.7}, {4, 4.7};
-- {0,8.1}, {4, 8.1};
}
for i=pgofs,(pgofs + constants.recipes_per_cookbook_page-1) do
local maxw, maxh = 3, 2
if not book.pages[i+1] then break end
local nr = 1+(i - pgofs)
local x,y = coords[nr][1], coords[nr][2]
local k = recipe_kinds[book.pages[i+1].kind]
local ox,oy = maxw - k.w, maxh - k.h
form = form .. string.format('container[%f,%f]%scontainer_end[]',(x+ox)-0.5,y,
retrieve_recipe(book.pages[i+1].kind, book.pages[i+1].name, true))
end
minetest.show_formspec(user:get_player_name(), 'sorcery:cookbook', form)
end
minetest.register_craftitem('sorcery:cookbook', {
description = 'Catalog';
inventory_image = 'default_book_written.png';
groups = { book = 1; flammable = 3 };
on_use = function(stack,user)
local book = bookprops(stack)
if #book.pages == 0 then return nil end
bookform(stack,user)
end;
})
minetest.register_on_player_receive_fields(function(user,form,fields)
if form ~= 'sorcery:cookbook' then return false end
if fields.quit then
bookform_ctx[user:get_player_name()] = nil
return true
end
local idx = bookform_ctx[user:get_player_name()]
if not idx then return true end
local uinv = user:get_inventory()
local stack = uinv:get_stack('main', idx)
local book = bookprops(stack)
local meta = stack:get_meta()
local curpage = meta:contains("pagenr") and meta:get_int("pagenr") or 1
local pagect = math.ceil(#book.pages / constants.recipes_per_cookbook_page)
if curpage > 1 and fields.pageback then
meta:set_int('pagenr', curpage - 1)
elseif curpage < pagect and fields.pagenext then
meta:set_int('pagenr', curpage + 1)
end
uinv:set_stack('main',idx,stack)
bookform(stack,user)
end)
minetest.register_on_craft(function(stack,player,grid,inv)
-- god this is messy. i'm sorry. minetest made me do it
if stack:get_name() ~= 'sorcery:cookbook' then return nil end
local oldbook
local topic, onetopic = nil, true
local recipes = {}
local copybook = false
local obindex
for i,s in pairs(grid) do
if s:get_name() == 'sorcery:recipe' then
recipes[#recipes+1] = s
elseif s:get_name() == 'default:book' then copybook = true
elseif s:get_name() == 'sorcery:cookbook' then oldbook = s obindex = i end
end
if #recipes == 0 and copybook and oldbook then
inv:set_stack('craft',obindex,oldbook)
local newmeta = stack:get_meta()
local copy = function(field)
newmeta:set_string(field,oldbook:get_meta():get_string(field))
end
copy('cookbook') copy('description')
newmeta:set_string('owner',player:get_player_name())
return stack
end
oldbook = oldbook or stack
local bookmeta = oldbook:get_meta()
local newbook = not bookmeta:contains('cookbook')
local book = bookprops(oldbook)
for _,s in pairs(recipes) do
local meta = s:get_meta()
local kind, item
if meta:contains('recipe_kind') then
kind = meta:get_string('recipe_kind')
item = meta:get_string('recipe_name')
else item, kind = sorcery.cookbook.pickrecipe(kind) end
book.pages[#book.pages + 1] = { name = item, kind = kind }
if topic then
if topic ~= kind then onetopic = false end
else topic = kind end
end
if topic and newbook then
if not onetopic then topic = nil end
bookmeta:set_string('description',namebook(topic,player:get_player_name()))
bookmeta:set_string('owner',player:get_player_name())
end
bookmeta:set_string('cookbook', sorcery.lib.str.meta_armor(encbook(book),true))
return oldbook
end)
if minetest.get_modpath('books') then
-- make our own placeable cookbook somehow
end