sorcery  Diff

Differences From Artifact [d9df5661ad]:

To Artifact [324c6534ec]:

  • File writing.lua — part of check-in [72eebac4bc] at 2020-09-26 18:49:51 on branch trunk — add writing stand for editing codexes; add scissors, ink, erasure fluid, pens; touch up codex UI; add many recipe notes; add craft divination type for crafttools; defuckulate fucktarded crafttool impl; enhance table library with missing features like lua's table.unpack; many bug fixes and enhancements; blood for the blood god (user: lexi, size: 16181) [annotate] [blame] [check-ins using]

     3      3   -- making them appear as by an "unknown author", by smudging out
     4      4   -- the byline with black dye. it also allows written books to be
     5      5   -- soaked in a bucket of water to wash out the ink and return
     6      6   -- them to a virginal, unwritten state. finally, it makes it so
     7      7   -- that when a book (or any owned item, for that matter) is
     8      8   -- copied, the owner of the new copy is set to the user who
     9      9   -- copied it, allowing users to collaborate on books.
           10  +
           11  +local constants = {
           12  +	ops_per_pen_level = 15;
           13  +	ops_per_ink_bottle = 8;
           14  +	cuts_per_scissor_hardness = 10;
           15  +	op_cost_retitle = 1;
           16  +	op_cost_copy_rec = 2;
           17  +	op_cost_insert_rec = 2;
           18  +};
    10     19   
    11     20   local paperburn = function(item,value)
    12     21   	minetest.register_craft { type = 'fuel', recipe = item, burntime = 3 * value }
    13     22   	minetest.register_craft {
    14     23   		type = 'cooking';
    15     24   		recipe = item;
    16     25   		output = 'sorcery:ash ' .. tostring(value);
................................................................................
    59     68   			meta:set_string('owner','unknown author')
    60     69   			meta:set_string('description','"'..meta:get_string('title')..'"')
    61     70   			pinv:set_stack('craft',book_idx,ItemStack())
    62     71   		end
    63     72   	end
    64     73   	return itemstack
    65     74   end)
           75  +
           76  +do
           77  +	local penbase = sorcery.lib.image('sorcery_pen.png')
           78  +	local pennib = sorcery.lib.image('sorcery_pen_nib.png')
           79  +	for name,metal in pairs(sorcery.data.metals) do
           80  +		if not metal.no_tools then
           81  +			local nib if name ~= 'steel' then
           82  +				nib = pennib:multiply(sorcery.lib.color(metal.tone):brighten(1.3))
           83  +			else nib = pennib end
           84  +			local cc if metal.hardness < 4 then cc = 3
           85  +				elseif metal.hardness < 7 then cc = 4
           86  +				else cc = 5 end
           87  +			local id = 'sorcery:pen_' .. name
           88  +			minetest.register_tool(id, {
           89  +				description = sorcery.lib.str.capitalize(name) .. ' Pen';
           90  +				inventory_image = penbase:blit(nib):render();
           91  +				groups = { pen = 1; sorcery_pen = 1; crafttool = cc };
           92  +				_sorcery = {
           93  +					material = {
           94  +						metal = true, grindvalue = 1;
           95  +						id = name, data = metal;
           96  +					};
           97  +				};
           98  +			})
           99  +			minetest.register_craft {
          100  +				output = id;
          101  +				recipe = {
          102  +					{'sorcery:fragment_gold','sorcery:fragment_copper'};
          103  +					{'default:stick',''};
          104  +					{metal.parts.fragment,''};
          105  +				};
          106  +			}
          107  +		end
          108  +	end
          109  +end
          110  +
          111  +for _,name in pairs {
          112  +	'bronze', 'steel', 'aluminum', 'tungsten', 'iridium'
          113  +} do -- :/
          114  +	local metal = sorcery.data.metals[name]
          115  +	local id = 'sorcery:scissors_' .. name
          116  +	minetest.register_tool(id, {
          117  +		description = sorcery.lib.str.capitalize(name) .. ' Scissors';
          118  +		inventory_image = 'sorcery_scissors_' .. name .. '.png';
          119  +		groups = { crafttool = metal.hardness * 15; scissors = 1 };
          120  +		tool_capabilities = {
          121  +			full_punch_interval = 1.5;
          122  +			max_drop_level = metal.maxlevel or metal.level;
          123  +			groupcaps = {
          124  +				snappy = {
          125  +					uses = math.floor(metal.durability * 0.10);
          126  +					leveldiff = 1;
          127  +					maxlevel = metal.maxlevel or metal.level;
          128  +					times = {
          129  +						[1] = 3 / metal.speed;
          130  +						[2] = 2 / metal.speed;
          131  +						[3] = 1 / metal.speed;
          132  +					};
          133  +				};
          134  +			};
          135  +			damage_groups = { fleshy = 1; };
          136  +		};
          137  +		_sorcery = {
          138  +			material = {
          139  +				metal = true, data = metal, name = name;
          140  +				grindvalue = 3;
          141  +			};
          142  +			recipe = {
          143  +				note = "An editor's best friend";
          144  +			};
          145  +		};
          146  +	})
          147  +	local frag = metal.parts.fragment;
          148  +	local screw = metal.parts.screw;
          149  +	local ingot = metal.parts.ingot;
          150  +	minetest.register_craft {
          151  +		output = id;
          152  +		recipe = {
          153  +			{frag,'screwdriver:screwdriver',frag};
          154  +			{'basic_materials:plastic_strip',screw,'basic_materials:plastic_strip'};
          155  +			{ingot,'',ingot};
          156  +		};
          157  +		replacements = {
          158  +			{'screwdriver:screwdriver', 'screwdriver:screwdriver'};
          159  +		};
          160  +	}
          161  +end
          162  +
          163  +-- the writing stand node allows books to be modified
          164  +-- in more complex ways than just inserting pages
          165  +
          166  +local ws_props = function(pos)
          167  +	local meta = minetest.get_meta(pos)
          168  +	local inv = meta:get_inventory()
          169  +	local reservoir = meta:get_int('inkwell')
          170  +	local inkstack = inv:get_stack('ink',1)
          171  +	local inkbottles = 0
          172  +	if minetest.get_item_group(inkstack:get_name(),'ink') ~= 0 then
          173  +		inkbottles = inkstack:get_count()
          174  +	end
          175  +	local penstack = inv:get_stack('pen',1)
          176  +	local penstr if not penstack:is_empty() then
          177  +		penstr = penstack:get_definition()._sorcery.material.data.hardness * constants.ops_per_pen_level
          178  +	end
          179  +	local operstack = inv:get_stack('operand',1)
          180  +	local mode if not operstack:is_empty() then
          181  +		if operstack:get_name() == 'sorcery:erasure_fluid' then
          182  +			mode = 'delete';
          183  +		elseif operstack:get_name() == 'sorcery:recipe' then
          184  +			mode = 'insert';
          185  +		elseif operstack:get_name() == 'default:paper' then
          186  +			mode = 'copy';
          187  +		elseif minetest.get_item_group(operstack:get_name(), 'scissors') ~= 0 then
          188  +			mode = 'cut';
          189  +		end
          190  +	end
          191  +	return {
          192  +		meta = meta;
          193  +		inv = inv;
          194  +		haspen = not penstack:is_empty();
          195  +		hasink = not inkstack:is_empty();
          196  +		inkwell = reservoir, penstr = penstr;
          197  +		mode = mode;
          198  +		inkstack = inkstack, penstack = penstack;
          199  +		operstack = operstack;
          200  +		totalink = reservoir + inkbottles * constants.ops_per_ink_bottle;
          201  +	}
          202  +end
          203  +local ws_formspec = function(pos)
          204  +	local props = ws_props(pos)
          205  +	local meta = props.meta
          206  +	local inv = props.inv
          207  +	local base = [[
          208  +		formspec_version[3] size[10.25,10.5]
          209  +		real_coordinates[true]
          210  +
          211  +		list[context;subject;0.25,0.25;1,1;]
          212  +		list[context;pen;0.25,1.50;1,1;]
          213  +		list[context;ink;0.25,2.75;1,1;]
          214  +
          215  +		list[context;operand;9,0.25;1,1;]
          216  +		image[9,1.50;1,1;sorcery_ui_inkwell.png]
          217  +		list[context;output;9,2.75;1,1;]
          218  +
          219  +		list[current_player;main;0.25,5.5;8,4;]
          220  +
          221  +		listring[context;output]
          222  +		listring[current_player;main] listring[context;subject]
          223  +		listring[current_player;main] listring[context;pen]
          224  +		listring[current_player;main] listring[context;ink]
          225  +		listring[current_player;main] listring[context;operand]
          226  +		listring[current_player;main]
          227  +	]]
          228  +	local mkbtn = function(x,y,tex,name,presstex)
          229  +		tex = 'sorcery_ui_' .. tex
          230  +		if presstex then
          231  +			presstex = 'sorcery_ui_' .. presstex
          232  +		else presstex = tex end
          233  +		return string.format('image_button[' ..
          234  +			'%f,%f;' ..
          235  +			'1,1;' ..
          236  +			'%s;%s;;' ..
          237  +			'false;false;%s' ..
          238  +		']',x,y,tex,name,presstex)
          239  +	end
          240  +
          241  +	local form = base
          242  +	local subj = inv:get_stack('subject',1)
          243  +
          244  +	if subj:is_empty() then
          245  +		form = form .. 'image[0.25,0.25;1,1;default_bookshelf_slot.png]'
          246  +	end
          247  +	if props.penstack:is_empty() then
          248  +		form = form .. 'image[0.25,1.50;1,1;sorcery_ui_ghost_pen.png]'
          249  +	end
          250  +	if props.inkstack:is_empty() then
          251  +		form = form .. 'image[0.25,2.75;1,1;sorcery_ui_ghost_ink_bottle.png]'
          252  +	end
          253  +
          254  +	if props.inkwell > 0 then
          255  +		form = form .. string.format(
          256  +			'image[8.8,1.50;0.5,0.5;sorcery_ui_inkwell_bar.png^[lowpart:%u:sorcery_ui_inkwell_bar_black.png]', 
          257  +
          258  +			math.min(100,math.ceil((props.inkwell / constants.ops_per_ink_bottle) * 100))
          259  +		)
          260  +	end
          261  +	
          262  +	if subj:get_name() == 'sorcery:cookbook' then
          263  +		local book = sorcery.cookbook.get(subj)
          264  +		local bm = subj:get_meta()
          265  +		local rpp = sorcery.cookbook.constants.recipes_per_cookbook_page
          266  +		local page = bm:contains("pagenr") and bm:get_int("pagenr") or 1
          267  +		local lastpage = math.ceil(#book.pages / rpp)
          268  +
          269  +		if page > 1 then form = form .. mkbtn(0.25,4,'pg_next.png^[transformFX','prevpage') end
          270  +		if page < lastpage then form = form .. mkbtn(9.00,4,'pg_next.png','nextpage'); end
          271  +
          272  +		local desctext = minetest.formspec_escape(bm:get_string('description'))
          273  +		if props.haspen and props.totalink >= constants.op_cost_retitle then
          274  +			form = form .. string.format([[
          275  +				field[1.75,0.25;7,1;title;;%s]
          276  +				field_close_on_enter[title;false]
          277  +			]], desctext)
          278  +		else
          279  +			form = form .. string.format([[
          280  +				label[1.75,0.75;%s]
          281  +			]], desctext)
          282  +		end
          283  +
          284  +		local mode = props.mode
          285  +		local modecolors = {
          286  +			delete = {255,127,127};
          287  +			cut    = {255,127,64};
          288  +			insert = {127,255,127};
          289  +			copy   = {127,127,255};
          290  +		}
          291  +
          292  +		for i=1,rpp do
          293  +			local chap = rpp * (page-1) + i
          294  +			local rec = book.pages[chap]
          295  +			if rec == nil then break end
          296  +			local text = sorcery.cookbook.recfn(rec.kind,'title')(rec.name)
          297  +			local class = sorcery.cookbook.classes[rec.kind]
          298  +
          299  +			local id = 'recipe_' .. tostring(i)
          300  +			local height = 1.50 + 1.25*(i-1)
          301  +			local img if class.icon then
          302  +				img = class.icon(rec.name)
          303  +			end
          304  +			local mcolor = sorcery.lib.color(modecolors[mode] or {255,255,255})
          305  +			form = form .. string.format([[
          306  +				style[%s;border=%s;textcolor=%s]
          307  +				style[%s:hovered;textcolor=#ffffff]
          308  +				style[%s:pressed;content_offset=0,0;textcolor=%s]
          309  +				button[1.75,%f;7,1;%s;%u. %s]
          310  +				%s[1.80,%f;0.90,0.90;%s]
          311  +			]], id, mode and 'true' or 'false',
          312  +				mode and mcolor:brighten(1.2):hex() or mcolor:hex(),
          313  +				id, id, mcolor:hex(),
          314  +				height, id, chap,
          315  +				minetest.formspec_escape(text),
          316  +				img and 'image' or 'item_image',
          317  +				height + 0.05, img or rec.name
          318  +			)
          319  +		end
          320  +	end
          321  +		
          322  +	meta:set_string('formspec',form)
          323  +end
          324  +
          325  +local wsbox = {
          326  +	type = 'fixed';
          327  +	fixed = {
          328  +		-0.5, -0.5, -0.43;
          329  +		 0.5,  0.05, 0.43;
          330  +	};
          331  +}
          332  +
          333  +minetest.register_node('sorcery:writing_stand', {
          334  +	description = 'Writing Stand';
          335  +	drawtype = 'mesh';
          336  +	mesh = 'sorcery-writing-stand.obj';
          337  +	sunlight_propagates = true;
          338  +	paramtype = 'light';
          339  +	paramtype2 = 'facedir';
          340  +	collision_box = wsbox;
          341  +	selection_box = wsbox;
          342  +	after_dig_node = sorcery.lib.node.purge_container;
          343  +	tiles = {
          344  +		'default_obsidian.png';
          345  +		'default_wood.png';
          346  +		'default_gold_block.png';
          347  +		'default_steel_block.png';
          348  +		'default_desert_stone.png';
          349  +		'default_cloud.png';
          350  +	};
          351  +	groups = {
          352  +		choppy = 2, oddly_breakable_by_hand = 2;
          353  +		sorcery_tech = 1;
          354  +		flammable = 2;
          355  +	};
          356  +	on_construct = function(pos)
          357  +		local meta = minetest.get_meta(pos)
          358  +		local inv = meta:get_inventory()
          359  +		
          360  +		meta:set_string('infotext','Writing Stand')
          361  +		inv:set_size('subject',1)
          362  +		inv:set_size('ink',1)
          363  +		inv:set_size('pen',1)
          364  +		inv:set_size('operand',1)
          365  +		inv:set_size('output',1)
          366  +
          367  +		ws_formspec(pos)
          368  +	end;
          369  +
          370  +	on_metadata_inventory_put = ws_formspec;
          371  +	on_metadata_inventory_take = ws_formspec;
          372  +	on_metadata_inventory_move = ws_formspec;
          373  +
          374  +	on_receive_fields = function(pos,fname,fields,user)
          375  +		local p = ws_props(pos)
          376  +		local subj = p.inv:get_stack('subject',1)
          377  +		local changed = false
          378  +		local charge_ink = function(val)
          379  +			if p.totalink < val then return false end
          380  +			if p.inkwell >= val then
          381  +				p.inkwell = p.inkwell - val
          382  +			else
          383  +				local bottles = math.ceil(val / constants.ops_per_ink_bottle)
          384  +				local inkbtls = p.inkstack:get_count()
          385  +				p.inkstack:take_item(bottles)
          386  +				local empties = ItemStack {
          387  +					name = 'vessels:glass_bottle';
          388  +					count = bottles;
          389  +				}
          390  +				if p.inkstack:is_empty() then
          391  +					p.inkstack = empties
          392  +				else
          393  +					local r = user:get_inventory():add_item('main',empties)
          394  +					if not r:is_empty() then
          395  +						minetest.add_item(pos,r)
          396  +					end
          397  +				end
          398  +				p.inkwell = (p.inkwell + bottles*constants.ops_per_ink_bottle) - val
          399  +				p.totalink = p.inkwell + (inkbtls-bottles)*constants.ops_per_ink_bottle
          400  +			end
          401  +			p.penstack:add_wear(math.ceil((65535 / p.penstr) * val))
          402  +			changed = true
          403  +			return true
          404  +		end
          405  +		if subj:is_empty() then return nil end
          406  +		if subj:get_name() == 'sorcery:cookbook' then
          407  +			local bm = subj:get_meta()
          408  +			local book = sorcery.cookbook.get(subj)
          409  +
          410  +			-- handle page change request
          411  +			if fields.nextpage or fields.prevpage then
          412  +				local page = math.max(1,bm:get_int('pagenr'))
          413  +				if     fields.nextpage then page = page + 1
          414  +				elseif fields.prevpage then page = page - 1 end
          415  +				bm:set_int('pagenr',math.max(1,page))
          416  +				changed = true
          417  +			end
          418  +
          419  +			-- handle retitle request
          420  +			if fields.title then
          421  +				if fields.title ~= bm:get_string('description') then
          422  +					if charge_ink(constants.op_cost_retitle) then
          423  +						bm:set_string('description',fields.title)
          424  +					end
          425  +				end
          426  +			end
          427  +
          428  +			-- handle editing request
          429  +			local rpp = sorcery.cookbook.constants.recipes_per_cookbook_page 
          430  +			if p.mode then for idx=1,rpp do
          431  +				if fields['recipe_' .. tostring(idx)] then
          432  +					local recnr = (bm:get_int('pagenr')-1)*rpp + idx
          433  +					local rec = book.pages[recnr]
          434  +					local bookchanged = false 
          435  +					if p.mode == 'delete' then
          436  +						p.operstack:take_item(1)
          437  +						table.remove(book.pages,recnr)
          438  +						bookchanged = true
          439  +					elseif p.mode == 'copy' and
          440  +						charge_ink(constants.op_cost_copy_rec) then
          441  +						local recipe = ItemStack('sorcery:recipe')
          442  +						sorcery.cookbook.setrecipe(recipe,rec.kind,rec.name)
          443  +						if p.operstack:get_count() == 1 then
          444  +							p.operstack = recipe
          445  +						else
          446  +							p.operstack:take_item(1)
          447  +							minetest.add_item(pos, user:get_inventory():add_item('main',recipe))
          448  +						end
          449  +						changed = true
          450  +					elseif p.mode == 'insert' then
          451  +						local rm = p.operstack:get_meta()
          452  +						if not rm:contains('recipe_kind') then
          453  +							sorcery.cookbook.setrecipe(p.operstack)
          454  +						end
          455  +						table.insert(book.pages,recnr,{
          456  +							kind = rm:get_string('recipe_kind');
          457  +							name = rm:get_string('recipe_name');
          458  +						})
          459  +						-- insertion can operate in one of two modes: copying
          460  +						-- the recipe into the book, or inserting the paper
          461  +						-- directly. if there is no ink available, we use the
          462  +						-- latter mode. in effect, this means deleting the
          463  +						-- recipe item is insufficient ink is available.
          464  +						if charge_ink(constants.op_cost_insert_rec) == false then
          465  +							p.operstack = ItemStack(nil)
          466  +						end
          467  +						bookchanged = true
          468  +					elseif p.mode == 'cut' then
          469  +						local spr = p.operstack:get_definition()._sorcery
          470  +						local sch = (spr and spr.material and spr.material.data.hardness) or 2
          471  +						local suses = sch * constants.cuts_per_scissor_hardness
          472  +						local dmg = 65535 / suses
          473  +
          474  +						local cutrec = ItemStack('sorcery:recipe')
          475  +						sorcery.cookbook.setrecipe(cutrec,rec.kind,rec.name)
          476  +						table.remove(book.pages,recnr)
          477  +						minetest.add_item(pos, user:get_inventory():add_item('main', p.inv:add_item('output', cutrec)))
          478  +
          479  +						p.operstack:add_wear(dmg)
          480  +						bookchanged = true
          481  +					end
          482  +					if bookchanged then
          483  +						sorcery.cookbook.set(subj, book)
          484  +						changed = true
          485  +					end
          486  +					break
          487  +				end
          488  +			end end
          489  +		end
          490  +
          491  +		if changed then
          492  +			p.inv:set_stack('subject',1,subj)
          493  +			p.inv:set_stack('ink',1,p.inkstack)
          494  +			p.inv:set_stack('pen',1,p.penstack)
          495  +			p.inv:set_stack('operand',1,p.operstack)
          496  +			p.meta:set_int('inkwell',p.inkwell)
          497  +			ws_formspec(pos)
          498  +		end
          499  +	end;
          500  +
          501  +	allow_metadata_inventory_put = function(pos,list,index,stack,user)
          502  +		local matchgrp = function(grp)
          503  +			return minetest.get_item_group(stack:get_name(), grp) ~= 0
          504  +		end
          505  +		local allowgroups = function(groups)
          506  +			for g,c in pairs(groups) do
          507  +				if matchgrp(g) then
          508  +					if c==0 then return stack:get_count()
          509  +						else return c end
          510  +				end
          511  +			end
          512  +			return 0
          513  +		end
          514  +		if list == 'subject' then
          515  +			return allowgroups{book=1,paper=1}
          516  +		elseif list == 'ink' then
          517  +			return allowgroups{ink=0}
          518  +		elseif list == 'pen' then
          519  +			return allowgroups{sorcery_pen=1}
          520  +		elseif list == 'operand' then
          521  +			-- TODO restrict to meaningful operands
          522  +			return stack:get_count()
          523  +			-- return allowgroups{book=0,paper=0,ink=0,...}
          524  +		end
          525  +		return 0
          526  +	end;
          527  +	allow_metadata_inventory_move = function(pos,fl,fi,tl,ti,ct,user)
          528  +		-- TODO this is lazy, defuckulate
          529  +		if tl == 'operand' then return ct end
          530  +		return 0
          531  +	end;
          532  +
          533  +	_sorcery = {
          534  +		recipe = {
          535  +			note = 'Edit manuals, copy their chapters, splice them together, or cut them apart';
          536  +		};
          537  +	};
          538  +})