-- this file contains a few enhancements to the normal book and
-- paper functionality. it allows authors to disavow their books,
-- making them appear as by an "unknown author", by smudging out
-- the byline with black dye. it also allows written books to be
-- soaked in a bucket of water to wash out the ink and return
-- them to a virginal, unwritten state. finally, it makes it so
-- that when a book (or any owned item, for that matter) is
-- copied, the owner of the new copy is set to the user who
-- copied it, allowing users to collaborate on books.
local constants = {
ops_per_pen_level = 15;
ops_per_ink_bottle = 8;
cuts_per_scissor_hardness = 10;
op_cost_retitle = 1;
op_cost_copy_rec = 2;
op_cost_insert_rec = 2;
};
local paperburn = function(item,value)
minetest.register_craft { type = 'fuel', recipe = item, burntime = 3 * value }
minetest.register_craft {
type = 'cooking';
recipe = item;
output = 'sorcery:ash ' .. tostring(value);
cooktime = 3 * value;
}
end
paperburn('default:paper',1) paperburn('sorcery:recipe',1)
paperburn('default:book',3) paperburn('sorcery:cookbook',3)
paperburn('default:book_written',3)
-- minetest.register_craft {
-- type = "shapeless";
-- recipe = {
-- "default:book_written";
-- "bucket:bucket_water", 'sorcery:erasure_fluid';
-- };
-- output = "default:book";
-- replacements = {
-- {"bucket:bucket_water", "bucket:bucket_empty"};
-- {'sorcery:erasure_fluid', 'vessels:glass_bottle'};
-- }
-- }
minetest.register_craft {
type = 'shapeless';
recipe = {"default:book_written", "dye:black"};
output = 'default:book_written';
}
minetest.register_on_craft(function(itemstack,player,recipe,pinv)
local meta = itemstack:get_meta()
if not meta:contains('owner') then return nil end
local pname = player:get_player_name()
if meta:get_string('owner') ~= pname then
meta:set_string('owner', pname)
end
if itemstack:get_name() == 'default:book_written' then
local found_book, found_dye, book_idx = false, false, 0
for i,v in pairs(recipe) do
if v:get_name() == 'dye:black' and not found_dye
then found_dye = true
elseif v:get_name() == 'default:book_written' and not found_book
then found_book = v book_idx = i
elseif not v:is_empty() then found_book = false break end
end
if found_book and found_dye then
meta:from_table(found_book:get_meta():to_table())
meta:set_string('owner','unknown author')
meta:set_string('description','"'..meta:get_string('title')..'"')
pinv:set_stack('craft',book_idx,ItemStack())
end
end
return itemstack
end)
do
local penbase = sorcery.lib.image('sorcery_pen.png')
local pennib = sorcery.lib.image('sorcery_pen_nib.png')
for name,metal in pairs(sorcery.data.metals) do
if not metal.no_tools then
local nib if name ~= 'steel' then
nib = pennib:multiply(sorcery.lib.color(metal.tone):brighten(1.3))
else nib = pennib end
local cc if metal.hardness < 4 then cc = 3
elseif metal.hardness < 7 then cc = 4
else cc = 5 end
local id = 'sorcery:pen_' .. name
minetest.register_tool(id, {
description = sorcery.lib.str.capitalize(name) .. ' Pen';
inventory_image = penbase:blit(nib):render();
groups = { pen = 1; sorcery_pen = 1; crafttool = cc };
_sorcery = {
material = {
metal = true, grindvalue = 1;
id = name, data = metal;
};
};
})
minetest.register_craft {
output = id;
recipe = {
{'sorcery:fragment_gold','sorcery:fragment_copper'};
{'default:stick',''};
{metal.parts.fragment,''};
};
}
end
end
end
sorcery.register.metals.foreach('sorcery:mkscissors',{'sorcery:generate'},function(name,metal)
local oklist = { -- :/
bronze=true, steel=true, aluminum=true;
tungsten=true, iridium=true
} -- TODO: moar art
if not oklist[name] then return end
local id = 'sorcery:scissors_' .. name
minetest.register_tool(id, {
description = sorcery.lib.str.capitalize(name) .. ' Scissors';
inventory_image = 'sorcery_scissors_' .. name .. '.png';
groups = { crafttool = metal.hardness * 15; scissors = 1 };
tool_capabilities = {
full_punch_interval = 1.5;
max_drop_level = metal.maxlevel or metal.level;
groupcaps = {
snappy = {
uses = math.floor(metal.durability * 0.10);
leveldiff = 1;
maxlevel = metal.maxlevel or metal.level;
times = {
[1] = 3 / metal.speed;
[2] = 2 / metal.speed;
[3] = 1 / metal.speed;
};
};
};
damage_groups = { fleshy = 1; };
};
_sorcery = {
material = {
metal = true, data = metal, name = name;
grindvalue = 4;
};
recipe = {
note = "An editor's best friend";
};
};
})
local frag = metal.parts.fragment;
local screw = metal.parts.screw;
minetest.register_craft {
output = id;
recipe = {
{frag,'screwdriver:screwdriver',frag};
{'basic_materials:plastic_strip',screw,'basic_materials:plastic_strip'};
{frag,'',frag};
};
replacements = {
{'screwdriver:screwdriver', 'screwdriver:screwdriver'};
};
}
end)
sorcery.register.metals.foreach('create cutting recipes',{'sorcery:mkscissors'}, function(name,metal)
local sc = 'sorcery:scissors_' .. name
local mkcut = function(item,out)
minetest.register_craft {
output = out, type = 'shapeless';
recipe = {item,sc}, replacements = {{sc,sc}};
}
end
if minetest.registered_items[sc] then
mkcut('default:paper','sorcery:punchcard_blank 2')
mkcut('default:book','default:paper 3')
end
end)
-- the writing stand node allows books to be modified
-- in more complex ways than just inserting pages
local ws_props = function(pos)
local meta = minetest.get_meta(pos)
local inv = meta:get_inventory()
local reservoir = meta:get_int('inkwell')
local inkstack = inv:get_stack('ink',1)
local inkbottles = 0
if minetest.get_item_group(inkstack:get_name(),'ink') ~= 0 then
inkbottles = inkstack:get_count()
end
local penstack = inv:get_stack('pen',1)
local penstr if not penstack:is_empty() then
penstr = penstack:get_definition()._sorcery.material.data.hardness * constants.ops_per_pen_level
end
local operstack = inv:get_stack('operand',1)
local mode if not operstack:is_empty() then
if operstack:get_name() == 'sorcery:erasure_fluid' then
mode = 'delete';
elseif operstack:get_name() == 'sorcery:recipe' then
mode = 'insert';
elseif operstack:get_name() == 'default:paper' then
mode = 'copy';
elseif minetest.get_item_group(operstack:get_name(), 'scissors') ~= 0 then
mode = 'cut';
end
end
return {
meta = meta;
inv = inv;
haspen = not penstack:is_empty();
hasink = not inkstack:is_empty();
inkwell = reservoir, penstr = penstr;
mode = mode;
inkstack = inkstack, penstack = penstack;
operstack = operstack;
totalink = reservoir + inkbottles * constants.ops_per_ink_bottle;
}
end
local ws_formspec = function(pos)
local props = ws_props(pos)
local meta = props.meta
local inv = props.inv
local base = [[
formspec_version[3] size[10.25,10.5]
real_coordinates[true]
list[context;subject;0.25,0.25;1,1;]
list[context;pen;0.25,1.50;1,1;]
list[context;ink;0.25,2.75;1,1;]
list[context;operand;9,0.25;1,1;]
image[9,1.50;1,1;sorcery_ui_inkwell.png]
list[context;output;9,2.75;1,1;]
list[current_player;main;0.25,5.5;8,4;]
listring[context;output]
listring[current_player;main] listring[context;subject]
listring[current_player;main] listring[context;pen]
listring[current_player;main] listring[context;ink]
listring[current_player;main] listring[context;operand]
listring[current_player;main]
]]
local mkbtn = function(x,y,tex,name,presstex)
tex = 'sorcery_ui_' .. tex
if presstex then
presstex = 'sorcery_ui_' .. presstex
else presstex = tex end
return string.format('image_button[' ..
'%f,%f;' ..
'1,1;' ..
'%s;%s;;' ..
'false;false;%s' ..
']',x,y,tex,name,presstex)
end
local form = base
local subj = inv:get_stack('subject',1)
if subj:is_empty() then
form = form .. 'image[0.25,0.25;1,1;default_bookshelf_slot.png]'
end
if props.penstack:is_empty() then
form = form .. 'image[0.25,1.50;1,1;sorcery_ui_ghost_pen.png]'
end
if props.inkstack:is_empty() then
form = form .. 'image[0.25,2.75;1,1;sorcery_ui_ghost_ink_bottle.png]'
end
if props.inkwell > 0 then
form = form .. string.format(
'image[8.8,1.50;0.5,0.5;sorcery_ui_inkwell_bar.png^[lowpart:%u:sorcery_ui_inkwell_bar_black.png]',
math.min(100,math.ceil((props.inkwell / constants.ops_per_ink_bottle) * 100))
)
end
if subj:get_name() == 'sorcery:cookbook' then
local book = sorcery.cookbook.get(subj)
local bm = subj:get_meta()
local rpp = sorcery.cookbook.constants.recipes_per_cookbook_page
local page = bm:contains("pagenr") and bm:get_int("pagenr") or 1
local lastpage = math.ceil(#book.pages / rpp)
if page > 1 then form = form .. mkbtn(0.25,4,'pg_next.png^[transformFX','prevpage') end
if page < lastpage then form = form .. mkbtn(9.00,4,'pg_next.png','nextpage'); end
local desctext = minetest.formspec_escape(bm:get_string('description'))
if props.haspen and props.totalink >= constants.op_cost_retitle then
form = form .. string.format([[
field[1.75,0.25;7,1;title;;%s]
field_close_on_enter[title;false]
]], desctext)
else
form = form .. string.format([[
label[1.75,0.75;%s]
]], desctext)
end
local mode = props.mode
local modecolors = {
delete = {255,127,127};
cut = {255,127,64};
insert = {127,255,127};
copy = {127,127,255};
}
for i=1,rpp do
local chap = rpp * (page-1) + i
local rec = book.pages[chap]
if rec == nil then break end
local text = sorcery.cookbook.recfn(rec.kind,'title')(rec.name)
local class = sorcery.cookbook.classes[rec.kind]
local id = 'recipe_' .. tostring(i)
local height = 1.50 + 1.25*(i-1)
local img if class.icon then
img = class.icon(rec.name)
end
local mcolor = sorcery.lib.color(modecolors[mode] or {255,255,255})
form = form .. string.format([[
style[%s;border=%s;textcolor=%s]
style[%s:hovered;textcolor=#ffffff]
style[%s:pressed;content_offset=0,0;textcolor=%s]
button[1.75,%f;7,1;%s;%u. %s]
%s[1.80,%f;0.90,0.90;%s]
]], id, mode and 'true' or 'false',
mode and mcolor:brighten(1.2):hex() or mcolor:hex(),
id, id, mcolor:hex(),
height, id, chap,
minetest.formspec_escape(text),
img and 'image' or 'item_image',
height + 0.05, img or rec.name
)
end
end
meta:set_string('formspec',form)
end
local wsbox = {
type = 'fixed';
fixed = {
-0.5, -0.5, -0.43;
0.5, 0.05, 0.43;
};
}
minetest.register_craftitem('sorcery:pulp', {
description = 'Pulp';
inventory_image = 'sorcery_pulp.png';
group = {flammable = 1};
})
minetest.register_craftitem('sorcery:pulp_inky', {
description = 'Inky Pulp';
inventory_image = 'sorcery_pulp_inky.png';
group = {flammable = 1};
})
minetest.register_craft {
output = 'sorcery:pulp 6';
type = 'shapeless';
recipe = {
'bucket:bucket_water', 'sorcery:erasure_fluid';
'sorcery:pulp_inky'; 'sorcery:pulp_inky'; 'sorcery:pulp_inky';
'sorcery:pulp_inky'; 'sorcery:pulp_inky'; 'sorcery:pulp_inky';
};
replacements = {
{'sorcery:erasure_fluid', 'vessels:glass_bottle'};
{'bucket:bucket_water', 'bucket:bucket_empty'};
};
}
minetest.register_craft {
output = 'sorcery:punchcard_blank';
type = 'cooking';
recipe = 'sorcery:pulp';
cooktime = 6;
}
minetest.register_craftitem('sorcery:pulp_sheet', {
description = 'Pulp Sheet';
inventory_image = 'sorcery_pulp_sheet.png';
groups = {flammable = 1};
})
minetest.register_craft {
output = 'default:paper';
type = 'cooking';
recipe = 'sorcery:pulp_sheet';
cooktime = 8;
}
minetest.register_craft {
output = 'sorcery:pulp_sheet 2';
recipe = {
{'sorcery:pulp', 'sorcery:pulp'};
{'sorcery:pulp', 'sorcery:pulp'};
};
}
minetest.register_node('sorcery:writing_stand', {
description = 'Writing Stand';
drawtype = 'mesh';
mesh = 'sorcery-writing-stand.obj';
sunlight_propagates = true;
paramtype = 'light';
paramtype2 = 'facedir';
collision_box = wsbox;
selection_box = wsbox;
after_dig_node = sorcery.lib.node.purge_container;
tiles = {
'default_obsidian.png';
'default_wood.png';
'default_gold_block.png';
'default_steel_block.png';
'default_desert_stone.png';
'default_cloud.png';
};
groups = {
choppy = 2, oddly_breakable_by_hand = 2;
sorcery_tech = 1;
flammable = 2;
};
on_construct = function(pos)
local meta = minetest.get_meta(pos)
local inv = meta:get_inventory()
meta:set_string('infotext','Writing Stand')
inv:set_size('subject',1)
inv:set_size('ink',1)
inv:set_size('pen',1)
inv:set_size('operand',1)
inv:set_size('output',1)
ws_formspec(pos)
end;
on_metadata_inventory_put = ws_formspec;
on_metadata_inventory_take = ws_formspec;
on_metadata_inventory_move = ws_formspec;
on_receive_fields = function(pos,fname,fields,user)
local p = ws_props(pos)
local subj = p.inv:get_stack('subject',1)
local changed = false
local charge_ink = function(val)
if p.totalink < val then return false end
if p.inkwell >= val then
p.inkwell = p.inkwell - val
else
local bottles = math.ceil(val / constants.ops_per_ink_bottle)
local inkbtls = p.inkstack:get_count()
p.inkstack:take_item(bottles)
local empties = ItemStack {
name = 'vessels:glass_bottle';
count = bottles;
}
if p.inkstack:is_empty() then
p.inkstack = empties
else
local r = user:get_inventory():add_item('main',empties)
if not r:is_empty() then
minetest.add_item(pos,r)
end
end
p.inkwell = (p.inkwell + bottles*constants.ops_per_ink_bottle) - val
p.totalink = p.inkwell + (inkbtls-bottles)*constants.ops_per_ink_bottle
end
p.penstack:add_wear(math.ceil((65535 / p.penstr) * val))
changed = true
return true
end
if subj:is_empty() then return nil end
if subj:get_name() == 'sorcery:cookbook' then
local bm = subj:get_meta()
local book = sorcery.cookbook.get(subj)
-- handle page change request
if fields.nextpage or fields.prevpage then
local page = math.max(1,bm:get_int('pagenr'))
if fields.nextpage then page = page + 1
elseif fields.prevpage then page = page - 1 end
bm:set_int('pagenr',math.max(1,page))
changed = true
end
-- handle retitle request
if fields.title then
if fields.title ~= bm:get_string('description') then
if charge_ink(constants.op_cost_retitle) then
bm:set_string('description',fields.title)
end
end
end
-- handle editing request
local rpp = sorcery.cookbook.constants.recipes_per_cookbook_page
if p.mode then for idx=1,rpp do
if fields['recipe_' .. tostring(idx)] then
local recnr = (bm:get_int('pagenr')-1)*rpp + idx
local rec = book.pages[recnr]
local bookchanged = false
if p.mode == 'delete' then
p.operstack:take_item(1)
table.remove(book.pages,recnr)
bookchanged = true
elseif p.mode == 'copy' and
charge_ink(constants.op_cost_copy_rec) then
local recipe = ItemStack('sorcery:recipe')
sorcery.cookbook.setrecipe(recipe,rec.kind,rec.name)
if p.operstack:get_count() == 1 then
p.operstack = recipe
else
p.operstack:take_item(1)
minetest.add_item(pos, user:get_inventory():add_item('main',recipe))
end
changed = true
elseif p.mode == 'insert' then
local rm = p.operstack:get_meta()
if not rm:contains('recipe_kind') then
sorcery.cookbook.setrecipe(p.operstack)
end
table.insert(book.pages,recnr,{
kind = rm:get_string('recipe_kind');
name = rm:get_string('recipe_name');
})
-- insertion can operate in one of two modes: copying
-- the recipe into the book, or inserting the paper
-- directly. if there is no ink available, we use the
-- latter mode. in effect, this means deleting the
-- recipe item is insufficient ink is available.
if charge_ink(constants.op_cost_insert_rec) == false then
p.operstack = ItemStack(nil)
end
bookchanged = true
elseif p.mode == 'cut' then
local spr = p.operstack:get_definition()._sorcery
local sch = (spr and spr.material and spr.material.data.hardness) or 2
local suses = sch * constants.cuts_per_scissor_hardness
local dmg = 65535 / suses
local cutrec = ItemStack('sorcery:recipe')
sorcery.cookbook.setrecipe(cutrec,rec.kind,rec.name)
table.remove(book.pages,recnr)
minetest.add_item(pos, user:get_inventory():add_item('main', p.inv:add_item('output', cutrec)))
p.operstack:add_wear(dmg)
bookchanged = true
end
if bookchanged then
sorcery.cookbook.set(subj, book)
changed = true
end
break
end
end end
end
if changed then
p.inv:set_stack('subject',1,subj)
p.inv:set_stack('ink',1,p.inkstack)
p.inv:set_stack('pen',1,p.penstack)
p.inv:set_stack('operand',1,p.operstack)
p.meta:set_int('inkwell',p.inkwell)
ws_formspec(pos)
end
end;
allow_metadata_inventory_put = function(pos,list,index,stack,user)
local matchgrp = function(grp)
return minetest.get_item_group(stack:get_name(), grp) ~= 0
end
local allowgroups = function(groups)
for g,c in pairs(groups) do
if matchgrp(g) then
if c==0 then return stack:get_count()
else return c end
end
end
return 0
end
if list == 'subject' then
return allowgroups{book=1,paper=1}
elseif list == 'ink' then
return allowgroups{ink=0}
elseif list == 'pen' then
return allowgroups{sorcery_pen=1}
elseif list == 'operand' then
-- TODO restrict to meaningful operands
return stack:get_count()
-- return allowgroups{book=0,paper=0,ink=0,...}
end
return 0
end;
allow_metadata_inventory_move = function(pos,fl,fi,tl,ti,ct,user)
-- TODO this is lazy, defuckulate
if tl == 'operand' then return ct end
return 0
end;
_sorcery = {
recipe = {
note = 'Edit manuals, copy their chapters, splice them together, or cut them apart';
};
};
})