@@ -6,8 +6,17 @@ -- 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 { @@ -62,4 +71,468 @@ 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'; + }; + }; +})