Differences From
Artifact [d9df5661ad]:
- 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 +})