sorcery  writing.lua at [72eebac4bc]

File writing.lua artifact 324c6534ec part of check-in 72eebac4bc


-- 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"};
	output = "default:book";
	replacements = {
		{"bucket:bucket_water", "bucket:bucket_empty"}
	}
}

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

for _,name in pairs {
	'bronze', 'steel', 'aluminum', 'tungsten', 'iridium'
} do -- :/
	local metal = sorcery.data.metals[name]
	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 = 3;
			};
			recipe = {
				note = "An editor's best friend";
			};
		};
	})
	local frag = metal.parts.fragment;
	local screw = metal.parts.screw;
	local ingot = metal.parts.ingot;
	minetest.register_craft {
		output = id;
		recipe = {
			{frag,'screwdriver:screwdriver',frag};
			{'basic_materials:plastic_strip',screw,'basic_materials:plastic_strip'};
			{ingot,'',ingot};
		};
		replacements = {
			{'screwdriver:screwdriver', 'screwdriver:screwdriver'};
		};
	}
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_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';
		};
	};
})