sorcery  Check-in [c71731cf58]

Overview
Comment:many bug fixers, some minor refactoring, allow non-drinkable potions to be empowered in various ways, allow gods to be petitioned for recipes (next up: cookbooks!)
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: c71731cf588f3c6b783992fc2455d64564ae464caf8c79e06f449dc772309e88
User & Date: lexi on 2021-07-03 02:25:42
Other Links: manifest | tags
Context
2021-07-04
20:27
add cool and informative visuals for taps, add more capacity to rune forge, many bug fixes, fixed some bugs, and fixed some bugs check-in: 94064fe5c9 user: lexi tags: trunk
2021-07-03
02:25
many bug fixers, some minor refactoring, allow non-drinkable potions to be empowered in various ways, allow gods to be petitioned for recipes (next up: cookbooks!) check-in: c71731cf58 user: lexi tags: trunk
2021-06-28
18:31
defuckulate markdown check-in: fa442a5d6a user: lexi tags: trunk
Changes

Modified altar.lua from [a7a1197203] to [080406f8d2].

     1      1   local altar_item_offset = {
     2      2   	x = 0, y = -0.3, z = 0
     3      3   }
     4         -local log = function(...) sorcery.log('altar',...) end
            4  +local log = sorcery.logger('altar')
     5      5   
     6      6   local range = function(min, max)
     7      7   	local span = max - min
     8      8   	local val = math.random() * span
     9      9   	return val + min
    10     10   end
    11     11   
................................................................................
    73     73   
    74     74   		after_place_node = function(pos, placer, stack, pointat)
    75     75   			local meta = minetest.get_meta(pos)
    76     76   			local stackmeta = stack:get_meta()
    77     77   			meta:set_int('favor', stackmeta:get_int('favor'))
    78     78   			meta:set_string('last_sacrifice', stackmeta:get_string('last_sacrifice'))
    79     79   
    80         -			minetest.get_node_timer(pos):start(60)
           80  +			minetest.get_node_timer(pos):start(1)
    81     81   		end;
    82     82   
    83     83   		drop = {
    84     84   			-- for some idiot reason this is necessary for
    85     85   			-- preserve_metadata to work right
    86     86   			max_items = 1;
    87     87   			items = {
................................................................................
   167    167   					-- we pick a random gift and roll against its rarity
   168    168   					-- to determine if the god is feeling generous
   169    169   					local gift = sorcery.lib.tbl.pick(god.gifts)
   170    170   					local data = god.gifts[gift]
   171    171   					local value, rarity = data[1], data[2]
   172    172   					if value <= divine_favor and math.random(rarity) == 1 then
   173    173   						bestow(gift)
   174         -						log(god.name .. ' has produced ' .. gift .. ' upon an altar as a gift')
          174  +						log.act(god.name .. ' has produced ' .. gift .. ' upon an altar as a gift')
   175    175   						if math.random(god.generosity) == 1 then
   176    176   							-- unappreciated gifts may incur divine
   177    177   							-- irritation
   178    178   							divine_favor = divine_favor - 1
   179    179   						end
   180    180   					end
   181    181   				end
................................................................................
   215    215   						goto refresh
   216    216   					end
   217    217   				end
   218    218   
   219    219   				-- loop through the list of things this god will consecrate and
   220    220   				-- check whether the item on the altar is any of them
   221    221   				for s, cons in pairs(god.consecrate) do
   222         -					local cost, tx = cons[1], cons[2]
   223         -					if type(tx) == "table" then
   224         -						tx = tx[math.random(#tx)]
   225         -					end
   226         -					-- preserve wear
   227         -					local gift = ItemStack(tx)
   228         -					local wear = stack:get_wear()
   229         -					if wear > 0 then
   230         -						gift:set_wear(wear)
   231         -					end
   232         -					-- preserve meta
   233         -					gift:get_meta():from_table(stack:get_meta():to_table())
   234         -					-- reflash enchantments to ensure label is accurate
   235         -					local ench = sorcery.enchant.get(gift)
   236         -					if #ench.spells > 0 then
   237         -						-- add a bit of energy as a gift?
   238         -						if math.random(math.ceil(god.stinginess * 0.5)) == 1 then
   239         -							local max = 0.05 * god.generosity
   240         -							ench.energy = ench.energy * range(0.7*max,max)
          222  +					if itemname == s then
          223  +						local cost, tx
          224  +						if type(cons) == "table" then
          225  +							cost, tx = cons[1], cons[2]
          226  +							tx = tx[math.random(#tx)]
          227  +						elseif type(cons) == 'function' then
          228  +							cost, tx = cons {
          229  +								favor = divine_favor;
          230  +								pos = pos;
          231  +								altar = altarmeta;
          232  +								idol = idolmeta;
          233  +								god = god;
          234  +							}
          235  +						end
          236  +						-- preserve wear
          237  +						local gift
          238  +						if type(tx) == 'string' then
          239  +							gift = ItemStack(tx)
          240  +						else gift = tx end
          241  +						local wear = stack:get_wear()
          242  +						if wear > 0 then
          243  +							gift:set_wear(wear)
          244  +						end
          245  +						-- preserve meta
          246  +						local gm = gift:get_meta()
          247  +						gm:from_table(
          248  +							sorcery.lib.tbl.merge(
          249  +								stack:get_meta():to_table(),
          250  +								gm:to_table()
          251  +							)
          252  +						) -- oof
          253  +						-- reflash enchantments to ensure label is accurate
          254  +						local ench = sorcery.enchant.get(gift)
          255  +						if #ench.spells > 0 then
          256  +							-- add a bit of energy as a gift?
          257  +							if math.random(math.ceil(god.stinginess * 0.5)) == 1 then
          258  +								local max = 0.05 * god.generosity
          259  +								ench.energy = ench.energy * range(0.7*max,max)
          260  +							end
          261  +							sorcery.enchant.set(gift,ench)
   241    262   						end
   242         -						sorcery.enchant.set(gift,ench)
   243         -					end
   244         -					if itemname == s then
          263  +
   245    264   						if divine_favor >= cost then
   246    265   							bestow(gift)
   247    266   							divine_favor = divine_favor - cost
   248         -							print(god.name..'has consecrated ' ..s.. ' into ' ..tx.. ', for the cost of ' ..cost.. ' points of divine favor')
          267  +							log.act(god.name, 'has consecrated', s, 'into', tx, 'for the cost of', cost, 'points of divine favor')
   249    268   							goto refresh
   250    269   						end
   251    270   					end
   252    271   				end
   253    272   			end
   254    273   
   255    274   			::refresh::

Modified astrolabe.lua from [a9316d6c59] to [cc031237ac].

     4      4   	local r = {}
     5      5   	for i,v in ipairs(k) do
     6      6   		r[i] = {
     7      7   			id = k[i];
     8      8   			name = sorcery.data.calendar.styles[v].name;
     9      9   		}
    10     10   	end
    11         -	print(dump(r))
    12     11   	return r
    13     12   end
    14     13   local astrolabe_formspec = function(pos)
    15     14   	local m = minetest.get_meta(pos)
    16     15   	local i = m:get_inventory()
    17     16   	local datestamp = minetest.get_day_count()
    18     17   	local date = sorcery.calendar.date(datestamp)

Modified cookbook.lua from [e643a56c3c] to [aae72d93b8].

     1      1   -- by use of the enchanter, it is possible to reveal a random
     2      2   -- recipe and enscribe it on a sheet of paper. these sheets of
     3      3   -- paper can then bound together into books, combining like
     4      4   -- recipes
     5      5   
     6      6   sorcery.cookbook = {}
            7  +local log = sorcery.logger('cookbook')
            8  +
     7      9   local constants = {
     8     10   	-- do not show recipes for items in these groups
     9     11   	exclude_groups = {
    10     12   	};
    11     13   	exclude_names = {
    12     14   		'stairs';
    13     15   		'slab';
................................................................................
    51     53   	return {}
    52     54   end
    53     55   local modofname = function(id)
    54     56   	local sep = string.find(id,':')
    55     57   	if sep == nil then return nil end -- uh oh
    56     58   	return string.sub(id, 1, sep - 1)
    57     59   end
           60  +local item_restrict_eval = function(name, restrict)
           61  +	for _,n in pairs(constants.exclude_names) do
           62  +		if string.find(name,n) ~= nil then
           63  +			return false
           64  +		end
           65  +	end
           66  +	for _,g in pairs(constants.exclude_groups) do
           67  +		if minetest.get_item_group(name, g) > 0 then
           68  +			return false
           69  +		end
           70  +	end
           71  +
           72  +	local props = minetest.registered_items[name]._sorcery
           73  +	local module = modofname(name)
           74  +
           75  +	return not (excluded
           76  +		or sorcery.lib.tbl.has(constants.blacklist_mods,module)
           77  +		or (props and props.recipe and props.recipe.secret)
           78  +		or (restrict and (
           79  +			(restrict.pred and restrict.pred {
           80  +				mod = module, item = name, props = props
           81  +			} ~= true)
           82  +		 or (restrict.mod   and module ~= restrict.mod)
           83  +		 or (restrict.group and (minetest.get_item_group(name, restrict.group) == 0))
           84  +	))) 
           85  +end
           86  +
    58     87   local pick_builtin = function(kind) return function(restrict)
    59     88   	-- ow ow ow ow ow ow ow
    60     89   	local names = {}
    61     90   	for k in pairs(minetest.registered_items) do
    62     91   		local rec = minetest.get_craft_recipe(k)
    63     92   		if rec.items ~= nil and (rec.method == kind or (rec.method == 'shapeless' and kind == 'normal')) then -- is this last bit necessary?
    64         -			local excluded = false
    65         -			for _,n in pairs(constants.exclude_names) do
    66         -				if string.find(k,n) ~= nil then
    67         -					excluded = true break end
    68         -			end
    69         -			if not excluded then for _,g in pairs(constants.exclude_groups) do
    70         -				if minetest.get_item_group(k, g) > 0 then
    71         -					excluded = true break end
    72         -			end end
    73         -			local props = minetest.registered_items[k]._sorcery
    74         -			local module = modofname(k)
    75         -			if not (excluded
    76         -				or sorcery.lib.tbl.has(constants.blacklist_mods,module)
    77         -				or (props and props.recipe and props.recipe.secret)
    78         -				or (restrict and (
    79         -				    (restrict.mod   and module ~= restrict.mod)
    80         -				 or (restrict.group and (minetest.get_item_group(k, restrict.group) == 0))
    81         -			))) then names[#names + 1] = k end
           93  +			if item_restrict_eval(k, restrict) then names[#names + 1] = k end
    82     94   		end
    83     95   	end
    84     96   	return names[math.random(#names)]
    85     97   end end
    86     98   local find_builtin = function(method,kind)
    87     99   	return function(out)
    88    100   		local rec = {}
................................................................................
   219    231   		chance = 4;
   220    232   		slots = {
   221    233   			{0,0};
   222    234   			{0,1};
   223    235   		};
   224    236   		pick = function(restrict)
   225    237   			-- TODO make sure affinity restrictions match
   226         -			return sorcery.register.infusions.db[math.random(#sorcery.register.infusions.db)].output
          238  +			if restrict then
          239  +				local t = {}
          240  +				for _, i in pairs(sorcery.register.infusions.db) do
          241  +					if item_restrict_eval(i.output, restrict) and not (
          242  +						-- conditions which cause failure of restriction test
          243  +						(restrict.ipred and restrict.ipred {
          244  +							mod = module;
          245  +							infusion = i;
          246  +							output = i.output;
          247  +						} ~= true)
          248  +					) then t[#t+1] = i.output end
          249  +				end
          250  +				return select(2, sorcery.lib.tbl.pick(t))
          251  +			else
          252  +				return sorcery.register.infusions.db[math.random(#sorcery.register.infusions.db)].output
          253  +			end
   227    254   		end;
   228    255   		title = function(output)
   229    256   			for _,i in pairs(sorcery.register.infusions.db) do
   230    257   				if i.output == output then
   231    258   					if i._proto and i._proto.name
   232    259   						then return i._proto.name
   233    260   						else break end
................................................................................
   254    281   		name = 'Milling Guide';
   255    282   		node = 'sorcery:mill';
   256    283   		booksuf = 'Manual';
   257    284   		chance = 1;
   258    285   		w = 1, h = 2;
   259    286   		pick = function(restrict)
   260    287   			cache:populate_grindables()
   261         -			local i = cache.grindables[math.random(#cache.grindables)]
   262         -			local pd = sorcery.itemclass.get(i, 'grindable')
   263         -			return pd.powder
          288  +			if restrict then
          289  +				local t = {}
          290  +				for _, i in pairs(cache.grindables) do
          291  +					local pd = sorcery.itemclass.get(i, 'grindable')
          292  +					if item_restrict_eval(pd.powder, restrict) then
          293  +						t[#t+1] = pd.powder
          294  +					end
          295  +				end
          296  +				return select(2, sorcery.lib.tbl.pick(t))
          297  +			else
          298  +				local gd = cache.grindables[math.random(#cache.grindables)]
          299  +				local pd = sorcery.itemclass.get(gd, 'grindable')
          300  +				return pd.powder
          301  +			end
   264    302   		end;
   265    303   		props = props_builtin;
   266    304   		slots = {
   267    305   			{0,1},
   268    306   			{0,0};
   269    307   		};
   270    308   		find = function(out)
................................................................................
   400    438   		end
   401    439   		if kind == nil then -- oh well, we tried
   402    440   			local rks = sorcery.lib.tbl.keys(recipe_kinds)
   403    441   			kind = rks[math.random(#rks)]
   404    442   		end
   405    443   	end
   406    444   
          445  +	if not recipe_kinds[kind] then
          446  +		log.fatalf('attempted to pick recipe of unknown kind "%s"', kind)
          447  +	end
   407    448   	return recipe_kinds[kind].pick(restrict), kind
   408    449   end
   409    450   
   410    451   local render_recipe = function(kind,ingredients,result,notes_right)
   411    452   	local k = recipe_kinds[kind]
   412    453   	local t = ''
   413    454   	local props = k.props(result)
................................................................................
   464    505   	local ing = rec.find(out)
   465    506   	return render_recipe(kind,ing,out,notes_right), rec.w, rec.h
   466    507   end
   467    508   
   468    509   sorcery.cookbook.setrecipe = function(stack,k,r,restrict)
   469    510   	local meta = stack:get_meta()
   470    511   	if not r then r,k = sorcery.cookbook.pickrecipe(k,restrict) end
          512  +	if not r then return false end
   471    513   	local t = recipe_kinds[k]
   472    514   	meta:set_string('recipe_kind', k)
   473    515   	meta:set_string('recipe_name', r)
   474    516   	meta:set_string('description',
   475    517   		(t.title and t.title(r) or desc_builtin(r)) .. ' ' .. t.name)
   476    518   end
   477    519   

Modified data/draughts.lua from [ab3b6e4e1b] to [0a84465667].

     6      6   		style = 'sparkle';
     7      7   		desc = "A potion that amps up your body's natural\nhealing abilities, causing you to heal rapidly\neven if you're starving";
     8      8   		infusion = 'sorcery:blood';
     9      9   		basis = 'sorcery:potion_luminous';
    10     10   		duration = function(self,meta)
    11     11   			return 10 + meta:get_int('duration')*2
    12     12   		end;
           13  +		quals = { force = true, duration = true };
    13     14   		effect = function(self, user, proto)
    14     15   			local meta = self:get_meta()
    15     16   			local force = 1 + meta:get_int('force')
    16     17   			late.new_effect(user, {
    17     18   				duration = proto:duration(meta);
    18     19   				raise = 4; fall = 4;
    19     20   				impacts = {
................................................................................
    27     28   		color = {79,228,243}; style = 'sparkle';
    28     29   		basis = 'sorcery:potion_luminous';
    29     30   		desc = "Conserve your precious supply of oxygen when diving down into the ocean's depths";
    30     31   		infusion = 'sorcery:extract_kelp';
    31     32   		duration = function(self,meta)
    32     33   			return 20 + meta:get_int('duration')*30
    33     34   		end;
           35  +		quals = { force = true, duration = true };
    34     36   		effect = function(self,user,proto)
    35     37   			local meta = self:get_meta()
    36     38   			local force = 1 + 2 * (meta:get_int('force'))
    37     39   			late.new_effect(user, {
    38     40   				duration = proto:duration(meta);
    39     41   				raise = 2; fall = 5;
    40     42   				impacts = {
................................................................................
    43     45   			})
    44     46   		end;
    45     47   	};
    46     48   	heal = {
    47     49   		name = 'Healing';
    48     50   		color = {243,44,58};
    49     51   		style = 'sparkle';
    50         -		no_duration = true;
           52  +		quals = { force = true };
    51     53   		desc = 'This blood-red liquid glitters with an enchantment that rapidly knits torn flesh and broken bones';
    52     54   		infusion = 'sorcery:oil_sanguine';
    53     55   		basis = 'sorcery:potion_luminous';
    54     56   		effect = function(self, user)
    55     57   			local meta = self:get_meta()
    56     58   			user:set_hp(user:get_hp() + (2 * (2 + meta:get_int('force'))))
    57     59   		end;
................................................................................
    61     63   
    62     64   	stealth = {
    63     65   		name = 'Stealth';
    64     66   		color = {184,106,224}; style = 'sparkle';
    65     67   		infusion = 'default:coal_lump';
    66     68   		basis = 'sorcery:potion_soft';
    67     69   		desc = 'Drinking this dark, swirling draught will shelter you from the power of mortal perception for a time, even rendering you entirely invisible at full strength.';
           70  +		quals = { force = true, duration = true };
    68     71   		duration = function(self,meta)
    69     72   			return 30 + meta:get_int('duration')*30
    70     73   		end;
    71     74   		effect = function(self,user,proto)
    72     75   			local meta = self:get_meta()
    73     76   			local force = 1 + 1 * (meta:get_int('force'))
    74     77   			local opacity = 1.0 - (1.0 * (force / 4)) 
................................................................................
    85     88   	nightsight = {
    86     89   		name = 'Nightsight';
    87     90   		color = {91,0,200}; style = 'sparkle';
    88     91   		desc = 'While this potion flows through your veins, your vision will be strengthened against the darkness of the night';
    89     92   		maxforce = 3;
    90     93   		infusion = 'sorcery:oil_dawn';
    91     94   		basis = 'sorcery:potion_soft';
           95  +		quals = { force = true, duration = true };
    92     96   		duration = function(self,meta)
    93     97   			return 50 + meta:get_int('duration')*70
    94     98   		end;
    95     99   		effect = function(self,user,proto)
    96    100   			--TODO ensure it can only be drunk at night
    97    101   			--TODO ensure it can't last more than one night
    98    102   			local meta = self:get_meta()
................................................................................
   108    112   	};
   109    113   	antigravity = {
   110    114   		name = 'Antigravity';
   111    115   		color = {240,59,255}; style = 'sparkle';
   112    116   		desc = 'Loosen the crushing grip of the earth upon your tender mortal form with a few sips from this glittering phial';
   113    117   		infusion = 'sorcery:oil_stone';
   114    118   		basis = 'sorcery:potion_soft';
          119  +		quals = { force = true, duration = true };
   115    120   		duration = function(self,meta)
   116    121   			return 20 + meta:get_int('duration')*25
   117    122   		end;
   118    123   		effect = function(self,user,proto)
   119    124   			local meta = self:get_meta()
   120    125   			local force = 1 - 0.3 * (meta:get_int('force') + 1)
   121    126   			late.new_effect(user, {
................................................................................
   129    134   	};
   130    135   	gale = {
   131    136   		name = 'Gale';
   132    137   		color = {187,176,203};
   133    138   		desc = 'Move and strike with the speed of a hurricane as this enchanted fluid courses through your veins';
   134    139   		infusion = 'sorcery:grease_storm';
   135    140   		basis = 'sorcery:potion_soft';
          141  +		quals = { force = true, duration = true };
   136    142   		duration = function(self,meta)
   137    143   			return 10 + meta:get_int('duration')*15
   138    144   		end;
   139    145   		effect = function(self,user,proto)
   140    146   			local meta = self:get_meta()
   141    147   			local force = 2 + 0.7 * (meta:get_int('force'))
   142    148   			late.new_effect(user, {
................................................................................
   151    157   	obsidian = {
   152    158   		name = 'Obsidian';
   153    159   		infusion = 'default:obsidian_shard';
   154    160   		color = {76,0,121}; style = 'sparkle';
   155    161   		desc = 'Walk untroubled through volleys of arrows and maelstroms of swinging blades, for all will batter uselessly against skin protected by spellwork mightier than the doughtiest armor';
   156    162   		infusion = 'default:obsidian_shard';
   157    163   		basis = 'sorcery:potion_luminous';
   158         -		no_force = true;
          164  +		quals = { duration = true };
   159    165   		duration = function(self,meta)
   160    166   			return 5 + meta:get_int('duration')*7
   161    167   		end;
   162    168   	};
   163    169   	lavabreathing = {
   164    170   		name = 'Lavabreathing';
   165    171   		color = {243,118,79}; style = 'sparkle'; glow = 12;
   166    172   		basis = 'sorcery:potion_soft';
   167    173   		desc = "Wade through seas of roiling lava as easily as though it were but a babbling brook";
          174  +		quals = { duration = true };
   168    175   	};
   169    176   	-- mighty = {
   170    177   	-- 	name = 'Mighty';
   171    178   	-- 	color = {255,0,119}; style = 'sparkle'; glow = 5;
   172    179   	-- 	infusion = 'sorcery:grease_war';
   173    180   	-- 	basis = 'sorcery:potion_soft';
   174    181   	-- 	desc = 'Amplify the power of your blows and crack steel armor with the force of your bare hands';
   175    182   	-- };
   176    183   	resilient = {
   177    184   		name = 'Resilient';
   178    185   		color = {124,124,124}; style = 'dull';
   179    186   		basis = 'sorcery:potion_soft';
   180    187   		desc = 'Withstand greater damage and hold your ground even in face of tremendous force';
          188  +		quals = { force = true, duration = true };
   181    189   	};
   182    190   	hover = {
   183    191   		name = 'Hover';
   184    192   		color = {164,252,55}; style = 'sparkle';
   185    193   		desc = 'Rise into the air for a time and stay there until the potion wears off';
   186    194   		basis = 'sorcery:potion_soft';
          195  +		quals = { force = true, duration = true };
   187    196   	}; 
   188    197   	flight = {
   189    198   		name = 'Flight';
   190    199   		color = {143,35,255}; style = 'sparkle';
   191    200   		desc = 'Free yourself totally from the shackles of gravity and soar through the air however you should will';
   192    201   		basis = 'sorcery:potion_soft';
   193    202   		infusion = 'sorcery:grease_lift';
   194         -		no_force = true;
          203  +		quals = { duration = true };
   195    204   		duration = function(self,meta)
   196    205   			return 40 + meta:get_int('duration')*55
   197    206   		end;
   198    207   		effect = function(self,user,proto)
   199    208   			late.new_effect(user, {
   200    209   				duration = proto:duration(self:get_meta());
   201    210   				impacts = {
................................................................................
   206    215   	};
   207    216   	leap = {
   208    217   		name = 'Leap';
   209    218   		color = {164,252,55};
   210    219   		desc = 'Soar high into the air each time you jump (but may risk damage if used without a Feather Potion)';
   211    220   		infusion = 'sorcery:oil_wind';
   212    221   		basis = 'sorcery:potion_soft';
          222  +		quals = { force = true, duration = true };
   213    223   		duration = function(self,meta)
   214    224   			 return 5 + meta:get_int('duration')*7
   215    225   		end;
   216    226   		effect = function(self,user,proto)
   217    227   			local meta = self:get_meta()
   218    228   			local force = 2 + (0.5 * meta:get_int('force'))
   219    229   			late.new_effect(user, {

Modified data/elixirs.lua from [b08a2041a5] to [ba37716134].

            1  +local inc = function(prop, val)
            2  +	return function(potion, kind)
            3  +		local meta = potion:get_meta()
            4  +		meta:set_int(prop, meta:get_int(prop) + (val or 1))
            5  +	end
            6  +end
            7  +
     1      8   return {
     2      9   	Force = {
     3         -		color = {255,165,85}; flag = 'force';
     4         -		apply = function(potion, kind)
     5         -			local meta = potion:get_meta()
     6         -			meta:set_int('force', meta:get_int('force') + 1)
     7         -		end;
           10  +		color = {255,165,85}; qual = 'force';
           11  +		apply = inc('force');
     8     12   		describe = function(potion)
     9     13   			return 'good', 'empowered', "The strength of this potion's effect has been alchemically amplified"
    10     14   		end;
    11     15   		infusion = 'sorcery:grease_thunder';
    12     16   	};
    13     17   	Longevity = {
    14         -		color = {255,85,216}; flag = 'duration';
    15         -		apply = function(potion, kind)
    16         -			local meta = potion:get_meta()
    17         -			meta:set_int('duration', meta:get_int('duration') + 1)
    18         -		end;
           18  +		color = {255,85,216}; qual = 'duration';
           19  +		apply = inc('duration');
    19     20   		describe = function(potion)
    20     21   			return 'good', 'prolonged', 'The effects of this potion will last longer than normal'
    21     22   		end;
    22     23   		infusion = 'sorcery:grease_pine';
    23     24   	};
           25  +	Rapidity = {
           26  +		color = {183,28,238}; qual = 'speed';
           27  +		apply = inc('speed');
           28  +		describe = function(potion)
           29  +			return 'good', 'Quickened', 'This potion will take effect more quiclkly and easily'
           30  +		end;
           31  +		infusion = 'sorcery:liquid_sap_acacia_bottle';
           32  +	};
           33  +	Purity = {
           34  +		color = {244,255,255}; qual = 'purity';
           35  +		apply = inc('purity');
           36  +		describe = function(potion)
           37  +			return 'good', 'purified', 'This potion\'s impurities and undesirable side effects are diminished or eliminated'
           38  +		end;
           39  +		infusion = 'sorcery:oil_purifying';
           40  +	};
           41  +	Beauty = {
           42  +		color = {255,20,226}; qual = 'beauty';
           43  +		apply = inc('beauty');
           44  +		describe = function(potion)
           45  +			return 'good', 'beautified', 'The effects of this potion will be more vivid and spectacular than normal'
           46  +		end;
           47  +		infusion = 'sorcery:liquid_sap_apple_bottle';
           48  +	};
           49  +	-- Glory?
           50  +	-- Clarity?
    24     51   }

Modified data/gods.lua from [4ff6f034b6] to [fc9658990d].

            1  +local L = sorcery.lib
     1      2   return {
     2      3   	harvest = {
     3      4   		name = "Irix Irimentari" --[[
     4      5   			an old Elerian harvest goddess, Irix Irimentari has watched vigilantly over the fields of her worshippers since before the Second Age. she favors alcoholic beverages as tribute, and has been known to perform blessings for sorcerers when sufficiently inebriated. she harbors a particular hatred of daemons and those who spill the blood of farmers.
     5      6   
     6      7   			legend says that a barbarian lord high on opium once wandered into a temple of Irix and left the severed head of a local shepherd on the her altar. this desecration so enraged the goddess that the barbarian's entire tribe soon starved horribly to death, their crops refusing to take root, and their stolen breads turning to ash in their mouths.
            8  +
            9  +			in the mystic arts, she is the patron of Alchemy. it is said that Irix
           10  +			Irimentari herself invented alchemy when she brewed the first mead.
     7     11   		]];
     8     12   		laziness = 5;
     9     13   		stinginess = 3;
    10     14   		generosity = 10;
    11     15   		color = {214, 255, 146};
    12     16   		idol = {
    13     17   			desc = "Harvest Idol";
................................................................................
    33     37   				"flowerpot:flowers_chrysanthemum_green";
    34     38   				"flowerpot:flowers_dandelion_white";
    35     39   				"flowerpot:flowers_dandelion_yellow";
    36     40   			}};
    37     41   			["sorcery:dagger"] = {17, "sorcery:dagger_consecrated"};
    38     42   			["sorcery:oil_mystic"] = {9, "sorcery:oil_purifying"};
    39     43   			["sorcery:potion_water"] = {4, "sorcery:holy_water"};
           44  +			["default:paper"] = function(ctx)
           45  +				local stack = ItemStack('sorcery:recipe')
           46  +				local mode = select(2,L.tbl.pick{'cook','craft','infuse','grind'})
           47  +				sorcery.cookbook.setrecipe(stack, mode, nil, {
           48  +					pred = function(c)
           49  +						local me = ctx.god
           50  +						if c.mod == 'farming' or
           51  +							minetest.get_item_group(c.item, 'sorcery_potion') ~= 0 or
           52  +							minetest.get_item_group(c.item, 'sorcery_oil') ~= 0 or
           53  +							minetest.get_item_group(c.item, 'sorcery_grease') ~= 0 or
           54  +							minetest.get_item_group(c.item, 'sorcery_extract') ~= 0 or
           55  +							me.sacrifice [c.item] or
           56  +							me.consecrate[c.item] then
           57  +							print(' !! accepted')
           58  +							return true end
           59  +						print(' -- rejected')
           60  +					end;
           61  +				})
           62  +				return 1, stack
           63  +			end;
    40     64   			-- ["default:steel_ingot"] = {15, "sorcery:holy_token_harvest"};
    41     65   		};
    42     66   		sacrifice = {
    43     67   			-- beattitudes
    44     68   			["farming:straw"           ] = 1;
    45     69   			["farming:bread_slice"     ] = 1;
    46     70   			["farming:bread"           ] = 2;
................................................................................
   130    154   				Force = {favor=50, cost=5, chance=7};
   131    155   				Longevity = {favor=65, cost=7, chance=3};
   132    156   			};
   133    157   			tools = {
   134    158   				rend = {favor=80, cost=15, chance=20};
   135    159   			};
   136    160   		};
          161  +		laziness = 2;
   137    162   		generosity = 4;
   138    163   		stinginess = 9;
   139    164   		idol = {
   140    165   			desc = "Blood Idol";
   141    166   			width = 0.7;
   142    167   			height = 1.3;
   143    168   			tex = {

Modified data/spells.lua from [2d770c6227] to [f6d0b8e638].

   466    466   			if div.restrict and not div.restrict(ctx) then
   467    467   				return false
   468    468   			end
   469    469   
   470    470   			local dst
   471    471   			local bonus = math.floor(ctx.stats.power or 1) 
   472    472   			if div.mode == 'any' then
   473         -				local lst = sorcery.lib.tbl.cshuf(div.give)
          473  +				local lst = sorcery.lib.tbl.scramble(div.give)
   474    474   				dst = function(i) return lst[i] end
   475    475   			elseif div.mode == 'random' then
   476    476   				dst = function() return tblroll(bonus,div.give) end
   477    477   			elseif div.mode == 'set' then
   478    478   				dst = function(i) return div.give[i] end
   479    479   			elseif div.mode == 'all' then
   480    480   				dst = function() return div.give end

Modified entities.lua from [5b6d6366ce] to [76a0b3b59c].

    78     78   		::collcheck:: do
    79     79   			-- if no collision then return end
    80     80   			-- local nname = minetest.get_node(pos).name 
    81     81   			-- if nname == 'air' or minetest.registered_nodes[nname].walkable ~= true then return
    82     82   			-- elseif nname == 'ignore' then goto destroy end
    83     83   			-- else fall through to explode
    84     84   			if collision then -- since 5.3 only!!
    85         -				print('collision detected!',dump(collision))
    86     85   				if collision.collides == false then return end
    87     86   				if #collision.collisions > 0 then
    88     87   					local col = collision.collisions[1]
    89     88   					if col.node_pos then
    90     89   						pos = col.node_pos
    91     90   					elseif col.object then
    92     91   						pos = col.object:get_pos()

Modified infuser.lua from [6c01c64259] to [0b21397e89].

   101    101   end
   102    102   
   103    103   local elixir_can_apply = function(elixir, potion)
   104    104   	-- accepts an elixir def and potion def
   105    105   	if elixir        == nil or
   106    106   	   potion        == nil then return false end
   107    107   
   108         -	if elixir.apply and potion.on_use then
   109         -		-- the ingredient is an elixir and at least one potion
   110         -		-- is a fully enchanted, usable potion
   111         -		if elixir.flag and potion._proto and
   112         -		   potion._proto['no_' .. elixir.flag] == true then
          108  +	if elixir.apply and potion._proto and potion._proto.quals then
          109  +		-- the ingredient is an elixir and at least one potion has a
          110  +		-- quality that can be enhanced
          111  +		if elixir.qual and potion._proto and not potion._proto.quals[elixir.qual] then
   113    112   			-- does the elixir have a property used to denote
   114    113   			-- compatibility? if so, check the potion to see if it's
   115    114   			-- marked as incompatible
   116    115   			return false
   117    116   		else
   118    117   			return true
   119    118   		end
................................................................................
   122    121   	return false
   123    122   end
   124    123   
   125    124   local effects_table = function(potion)
   126    125   	local meta = potion:get_meta()
   127    126   	local tbl = {}
   128    127   	for k,v in pairs(sorcery.data.elixirs) do
   129         -		if not v.flag then goto skip end
   130         -		local val = meta:get_int(v.flag)
          128  +		if not v.qual then goto skip end
          129  +		local val = meta:get_int(v.qual)
   131    130   		if val > 0 then
   132    131   			local aff, title, desc = v.describe(potion)
   133    132   			if val > 3 then title = title .. ' x' .. val
   134    133   			elseif val == 3 then title = 'thrice-' .. title
   135    134   			elseif val == 2 then title = 'twice-' .. title
   136    135   			end
   137    136   			tbl[#tbl + 1] = {
................................................................................
   146    145   
   147    146   sorcery.alchemy.elixir_apply = function(elixir, potion)
   148    147   	if not potion then return end
   149    148   	local pdef = potion:get_definition()
   150    149   	if elixir_can_apply(elixir, pdef) then
   151    150   		elixir.apply(potion, pdef._proto)
   152    151   		potion:get_meta():set_string('description', sorcery.lib.ui.tooltip {
   153         -			title = pdef._proto.name .. ' Draught';
          152  +			title = string.format('%s %s', pdef._proto.name, pdef._proto.kind.label);
   154    153   			desc = pdef._proto.desc;
   155    154   			color = sorcery.lib.color(pdef._proto.color):readable();
   156    155   			props = effects_table(potion);
   157    156   		});
   158    157   	end
   159    158   	return potion
   160    159   end
................................................................................
   301    300   			elseif r.enhance then
   302    301   				if fx.onenhance then out = fx.onenhance {
   303    302   					pos = pos;
   304    303   					stack = out;
   305    304   					potion = r.proto;
   306    305   					elixir = r.elixir;
   307    306   				} end
   308         -				log.act(dump(r))
   309    307   				log.act(string.format('an infuser at %s has enhanced a %s potion with a %s elixir',
   310    308   					minetest.pos_to_string(pos), out:get_name(), infusion[1]:get_name()))
   311    309   			end
   312    310   			inv:set_stack('potions',i,discharge(out))
   313    311   		end
   314    312   
   315    313   		inv:set_stack('infusion',1,residue)

Modified init.lua from [58ff591804] to [3b59932455].

    19     19   	argjoin(arg, nxt, ...)
    20     20   		if arg and not nxt then return tostring(arg) end
    21     21   		if not arg then return "(nil)" end
    22     22   		return tostring(arg) .. ' ' .. argjoin(nxt, ...)
    23     23   	end
    24     24   
    25     25   	local logger = function(module)
    26         -		local emit = function(lvl)
    27         -			return function(...)
    28         -				if module then
    29         -					minetest.log(lvl,string.format('[%s :: %s] %s',selfname,module,argjoin(...)))
    30         -				else
    31         -					minetest.log(lvl,string.format('[%s] %s',selfname,argjoin(...)))
           26  +		local lg = {}
           27  +		local setup = function(fn, lvl)
           28  +			lvl = lvl or fn
           29  +			local function emit(...)
           30  +				local call = (fn == 'fatal') and error
           31  +					or function(str) minetest.log(lvl, str) end
           32  +				if module
           33  +					then call(string.format('[%s :: %s] %s',selfname,module,argjoin(...)))
           34  +					else call(string.format('[%s] %s',selfname,argjoin(...)))
    32     35   				end
    33     36   			end
           37  +			lg[fn       ] = function(...) emit(...)                end
           38  +			lg[fn .. 'f'] = function(...) emit(string.format(...)) end -- convenience fn
    34     39   		end
    35         -		return {
    36         -			info = emit('info');
    37         -			warn = emit('warning');
    38         -			err = emit('error');
    39         -			act = emit('action');
    40         -		}
           40  +		setup('info')
           41  +		setup('warn','warning')
           42  +		setup('err','error')
           43  +		setup('act','action')
           44  +		setup('fatal')
           45  +		return lg
    41     46   	end;
    42     47   
    43     48   	local stage = function(s,...)
    44     49   		logger().info('entering stage',s)
    45     50   		local f = sorcery.cfg(s .. '.lua')
    46     51   		if test(f) then return loadfile(f)(...) or true end
    47     52   		return false

Modified keg.lua from [7838002de4] to [5c653d450d].

   185    185   })
   186    186   
   187    187   minetest.register_craft {
   188    188   	output = "sorcery:keg";
   189    189   	recipe = {
   190    190   		{'','screwdriver:screwdriver',''};
   191    191   		{'sorcery:screw_bronze', 'sorcery:tap', 'sorcery:screw_bronze'};
   192         -		{'', 'xdecor:barrel', ''};
          192  +		{'sorcery:screw_bronze', 'xdecor:barrel', 'sorcery:screw_bronze'};
   193    193   	};
   194    194   	replacements = {
   195    195   		{'screwdriver:screwdriver', 'screwdriver:screwdriver'};
   196    196   	};
   197    197   }

Modified lib/node.lua from [d8278e1811] to [087023acf7].

   315    315   			if n.name == 'ignore' then
   316    316   				minetest.load_area(sum)
   317    317   				n = minetest.get_node(sum)
   318    318   			end
   319    319   			fn(sum, n)
   320    320   		end
   321    321   	end;
          322  +
          323  +	amass = amass;
   322    324   	
   323    325   	force = force;
   324    326   
   325    327   	-- when items have already been removed; notify cannot be relied on
   326    328   	-- to reach the entire network; this function accounts for the gap
   327    329   	notifyneighbors = function(pos)
   328    330   		sorcery.lib.node.forneighbor(pos, sorcery.ley.txofs, function(sum,node)

Modified lib/tbl.lua from [990a6e8caf] to [0fb14e1ea3].

     4      4   	for i = #list, 2, -1 do
     5      5   		local j = math.random(i)
     6      6   		list[i], list[j] = list[j], list[i]
     7      7   	end
     8      8   	return list
     9      9   end
    10     10   
    11         -fn.cshuf = function(list)
           11  +fn.scramble = function(list)
    12     12   	return fn.shuffle(table.copy(list))
    13     13   end
    14     14   
    15     15   fn.urnd = function(min,max)
    16     16   	local r = {}
    17     17   	for i=min,max do r[1 + (i - min)] = i end
    18     18   	fn.shuffle(r)
................................................................................
    27     27   			hash[v] = true
    28     28   			new[#new+1] = v
    29     29   		end
    30     30   	end
    31     31   	return new
    32     32   end
    33     33   
    34         -fn.scramble = function(list)
    35         -	local new = table.copy(list)
    36         -	fn.shuffle(new)
    37         -	return new
    38         -end
    39         -
    40     34   fn.copy = function(t)
    41     35   	local new = {}
    42     36   	for i,v in pairs(t) do new[i] = v end
    43     37   	setmetatable(new,getmetatable(t))
    44     38   	return new
    45     39   end
    46     40   

Modified liquid.lua from [04ca2e071b] to [6a40bd16e3].

    50     50   
    51     51   sorcery.liquid.mktrough = function(liq)
    52     52   	-- troughs are used for collecting liquid from the environment,
    53     53   	-- like rainwater and tree sap. they hold twice as much as a bucket
    54     54   	local Q = constants.glasses_per_bottle
    55     55   	local trough_mkid = function(l,i)
    56     56   		if type(l) == 'string' then l = sorcery.register.liquid.db[l] end
    57         -		if not l or not i then return 'sorcery:trough' end
           57  +		if (not l) or (not i) or i < 1 then return 'sorcery:trough' end
    58     58   		return string.format('%s:trough_%s_%u', l.mod,l.sid,i)
    59     59   	end
    60     60   	local lid = function(l) return trough_mkid(liq, l) end
    61     61   
    62     62   	local M = constants.bottles_per_trough
    63     63   	local mkbox = function(lvl)
    64     64   		local pxl = function(tbl) -- for mapping to txcoords
................................................................................
   161    161   	end
   162    162   end
   163    163   sorcery.liquid.mktrough()
   164    164   
   165    165   sorcery.liquid.measure_default = function(amt)
   166    166   	return string.format('%s drams', amt*constants.drams_per_glass)
   167    167   end
          168  +
          169  +sorcery.liquid.container = function(liq, ctr)
          170  +	return liq.containers[({
          171  +		bottle = 'vessels:glass_bottle';
          172  +		glass = 'vessels:drinking_glass';
          173  +		keg = 'sorcery:keg';
          174  +		trough = 'sorcery:trough';
          175  +	})[ctr] or ctr]
          176  +end
          177  +
   168    178   sorcery.liquid.register = function(liq)
   169    179   	local fmt = string.format
   170    180   	local Q = constants.glasses_per_bottle
   171    181   	liq.sid = liq.sid or liq.id:gsub('^[^:]+:','')
   172    182   	liq.mod = liq.mod or liq.id:gsub('^([^:]+):.*','%1')
   173    183   	if not liq.measure then
   174    184   		liq.measure = sorcery.liquid.measure_default

Modified potions.lua from [a4fa9f9b09] to [0b54227d82].

    52     52   
    53     53   sorcery.register_oil = function(name,label,desc,color,imgvariant,extra)
    54     54   	local image = 'xdecor_bowl.png^(sorcery_oil_' .. (imgvariant or 'dull') .. '.png^[colorize:'..tostring(color)..':140)'
    55     55   	sorcery.register.residue.link('sorcery:' .. name, 'xdecor:bowl')
    56     56   	extra.description = label;
    57     57   	extra.inventory_image = image;
    58     58   	if not extra.groups then extra.groups = {} end
           59  +	extra.groups.sorcery_oil = 1
    59     60   	minetest.register_craftitem('sorcery:' .. name, extra)
    60     61   end
    61     62   
    62     63   sorcery.register_potion('blood', 'Blood', 'A bottle of sacrificial blood, imbued with stolen (or perhaps donated) life force', u.color(219,19,14), nil, nil, {
    63     64   	_sorcery = {
    64     65   		life_store = 4;
    65     66   		container = {
................................................................................
   102    103   			output = 'sorcery:' .. id;
   103    104   			_proto = proto;
   104    105   		}
   105    106   	end
   106    107   end
   107    108   
   108    109   -- for n,v in pairs(sorcery.data.potions) do
          110  +local kind_potion = {
          111  +	label = 'Potion';
          112  +	kind = 'A mystical liquid crucial to the art of alchemy';
          113  +}
   109    114   sorcery.register.potions.foreach('sorcery:mknodes',{},function(n,v)
   110    115   	local color = u.color(v.color)
   111    116   	local kind = v.style
   112    117   	local glow = v.glow
   113    118   	local id = 'potion_' .. string.lower(n)
   114    119   	local desc = 'A ' .. ((glow and 'glowing ') or '') ..
   115    120   		'bottle of ' .. string.lower(n) .. 
   116    121   		((kind == 'sparkle' and ', fiercely bubbling') or '') ..
   117    122   		' liquid'
   118    123   	local fullname = n .. ' Potion'
   119    124   	sorcery.register.liquid.link('sorcery:'..id, {
   120         -		name = 'Serene Potion';
          125  +		name = fullname;
   121    126   		color = v.color;
   122    127   		proto = v;
   123    128   		kind = 'sorcery:potion';
   124    129   		measure = function(amt) return string.format('%s draughts', amt / 3) end;
   125    130   		containers = {
   126    131   			['vessels:glass_bottle'] = 'sorcery:' .. id;
   127    132   		};
   128    133   	})
          134  +	v.kind = kind_potion;
   129    135   	sorcery.register_potion(id, fullname, desc, color, kind, glow, {
   130    136   		groups = {
   131    137   			sorcery_potion = 1;
   132    138   			sorcery_magical = 1;
   133    139   		};
   134    140   		_proto = v;
   135    141   		_sorcery = {
................................................................................
   142    148   			};
   143    149   		};
   144    150   	})
   145    151   	create_infusion_recipe(id,v,'sorcery:potion_serene',{data=v,name=fullname})
   146    152   end)
   147    153   
   148    154   -- for n,potion in pairs(sorcery.data.draughts) do
          155  +local kind_draught = {
          156  +	label = 'Draught';
          157  +	desc = 'A drink that will suffuse your body and spirit with mystic energies';
          158  +}
   149    159   sorcery.register.draughts.foreach('sorcery:mknodes',{},function(n,potion)
   150    160   	local name = 'draught_' .. n
          161  +	potion.kind = kind_draught
   151    162   	local behavior = {
   152    163   		_proto = potion;
   153    164   		groups = {
   154    165   			sorcery_potion = 2;
   155    166   			sorcery_draught = 1;
   156    167   			sorcery_magical = 1;
   157    168   			sorcery_usable_magic = 1;
................................................................................
   201    212   		potion.style or 'dull',
   202    213   		potion.glow or 0,
   203    214   		behavior)
   204    215   	create_infusion_recipe(name,potion,'sorcery:potion_luminous',{data=potion,name=fullname})
   205    216   end)
   206    217   
   207    218   -- for n,elixir in pairs(sorcery.data.elixirs) do
          219  +local kind_elixir = {
          220  +	label = 'Elixir';
          221  +	desc = 'A special kind of potion that enhances the particular qualities of other alchemical brews';
          222  +}
   208    223   sorcery.register.elixirs.foreach('sorcery:mknodes',{},function(n,elixir)
   209    224   	local color = u.color(elixir.color)
   210    225   	local id = 'elixir_' .. string.lower(n)
   211    226   	local fullname = 'Elixir of ' .. n
          227  +	elixir.kind = kind_elixir;
   212    228   	sorcery.register_potion(id, fullname, nil, color, 'dull', false, {
   213    229   		_proto = elixir;
   214    230   		groups = {
   215    231   			sorcery_elixir = 1;
   216    232   			sorcery_magical = 1;
   217    233   		};
   218    234   	})
................................................................................
   236    252   	local kind = v.style
   237    253   	sorcery.register_oil('grease_' .. n, u.str.capitalize(n) .. ' Grease', nil, color, kind, {
   238    254   		groups = { sorcery_grease = 1 }
   239    255   	})
   240    256   end)
   241    257   
   242    258   -- for n,v in pairs(sorcery.data.philters) do
          259  +local kind_philter = {
          260  +	label = 'Philter';
          261  +	desc = 'A special kind of potion that wooden rods can be soaked in to imbue them with special powers and transform them into wands';
          262  +}
   243    263   sorcery.register.philters.foreach('sorcery:mknodes',{},function(n,v)
   244    264   	local color = u.color(v.color)
   245    265   	local id = 'philter_' .. n
   246    266   	local name = v.name or u.str.capitalize(n)
          267  +	if not v.name then v.name = name end
   247    268   	local fullname = name .. ' Philter'
          269  +	v.kind = kind_philter
   248    270   	sorcery.register_potion(id, fullname, v.desc, color, 'sparkle',v.glow or 4, {
   249    271   		_proto = v;
   250    272   		_protoname = n;
   251    273   		groups = {
   252    274   			sorcery_magical = 1;
   253    275   			sorcery_philter = 1;
   254    276   		};
   255    277   	})
          278  +	v.quals = {force = true};
   256    279   	create_infusion_recipe(id,v,'sorcery:potion_viscous',{data=v,name=fullname})
   257    280   end)
   258    281   
   259    282   -- for n,v in pairs(sorcery.data.extracts) do
   260    283   sorcery.register.extracts.foreach('sorcery:mknodes',{},function(n,v)
   261    284   	local item = v[1]
   262    285   	local color = u.color(v[2])
................................................................................
   302    325   				{"farming:mortar_pestle", "farming:mortar_pestle"};
   303    326   			};
   304    327   		}
   305    328   	end
   306    329   	-- need a relatively pure alcohol for this, tho other alcohols can be used
   307    330   	-- for potionmaking in other ways
   308    331   	add_alcohol('farming:bottle_ethanol')
   309         -	add_alcohol('wine:glass_vodka')
          332  +	if minetest.get_modpath('wine') then
          333  +		add_alcohol('wine:glass_vodka')
          334  +	end
   310    335   end)

Modified runeforge.lua from [69ba246df3] to [65aa0a1ed6].

    36     36   		};
    37     37   		supreme  = {grade = 6, name = 'Supreme';  infusion = 'sorcery:powder_levitanium';
    38     38   			dist = { Fragile = 0, Weak = 0,    Ordinary = 1,   Pristine = 0.7,  Sublime = 0.4 };
    39     39   		};
    40     40   	};
    41     41   }
    42     42   local calc_phial_props = function(phial) --> mine interval: float, time factor: float
           43  +	local m = phial:get_meta()
    43     44   	local g = phial:get_definition()._proto.data.grade
    44     45   	local i = constants.rune_mine_interval 
    45     46   	local fac = (g-1) / 5
           47  +	fac = fac + 0.4 * m:get_int('speed')
    46     48   	return i - ((i*0.5) * fac), 0.5 * fac
    47     49   end
    48     50   sorcery.register.runes.foreach('sorcery:generate',{},function(name,rune)
    49     51   	local id = 'sorcery:rune_' .. name
    50     52   	rune.image = rune.image or string.format('sorcery_rune_%s.png',name)
    51     53   	rune.item = id
    52     54   	local c = sorcery.lib.color(rune.tone)
................................................................................
    55     57   		short_description = rune.name .. ' Rune';
    56     58   		inventory_image = rune.image;
    57     59   		stack_max = 1;
    58     60   		groups = {
    59     61   			sorcery_rune = 1;
    60     62   			not_in_creative_inventory = 1;
    61     63   		};
    62         -		_proto = { id = name, data = rune; };
           64  +		_proto = { id = name, data = rune };
    63     65   	})
    64     66   end)
           67  +
           68  +local phkind = {
           69  +	label = 'Phial';
           70  +	desc = 'An alchemical substance which rune forges consume while coalescing new runes';
           71  +}
    65     72   
    66     73   for name,p in pairs(constants.phial_kinds) do
    67     74   	local f = string.format
    68     75   	local color = sorcery.lib.color(142,232,0)
    69     76   	local fac = p.grade / 6
    70     77   	local id = f('phial_%s', name);
           78  +	local fname = f('%s Phial',p.name);
           79  +	local desc = "A powerful liquid consumed in the operation of a rune forge. Its quality determines how fast new runes can be constructed and how much energy is required by the process, and affects your odds of getting a high-quality rune."
    71     80   	sorcery.register_potion_tbl {
    72     81   		name = id;
    73         -		label = f('%s Phial',p.name);
    74         -		desc = "A powerful liquid consumed in the operation of a rune forge. Its quality determines how fast new runes can be constructed and how much energy is required by the process, and affects your odds of getting a high-quality rune.";
           82  +		label = fname;
           83  +		desc = desc;
    75     84   		color = color:brighten(1 + fac*0.5);
    76     85   		imgvariant = (fac >= 5) and 'sparkle' or 'dull';
    77     86   		glow = 5+p.grade;
    78     87   		extra = {
    79     88   			groups = { sorcery_phial = p.grade };
    80         -			_proto = { id = name, data = p };
           89  +			_proto = { id = name, desc = desc, name = p.name, kind = phkind, data = p, quals = {force = true, speed = true}, color = color };
    81     90   		};
    82     91   	}
    83     92   	sorcery.register.infusions.link {
    84     93   		infuse = p.infusion;
    85     94   		into = 'sorcery:potion_subtle';
    86     95   		output = 'sorcery:'..id;
    87     96   	}
................................................................................
   255    264   	local pow_min = l.self.powerdraw >= l.self.minpower
   256    265   	local pow_max = l.self.powerdraw >= l.self.maxpower
   257    266   	local has_phial = function() return not i:is_empty('phial') end
   258    267   
   259    268   	if time and has_phial() and pow_min and not probe.disjunction then -- roll for runes
   260    269   		local phial = i:get_stack('phial',1)
   261    270   		local int, powerfac = calc_phial_props(phial)
          271  +		local pf = phial:get_meta():get_int('force')
   262    272   		local rolls = math.floor(time/int)
   263    273   		local newrunes = {}
   264    274   		for _=1,rolls do
   265    275   			local choices = {}
   266    276   			for name,rune in pairs(sorcery.data.runes) do
   267    277   				-- print('considering',name)
   268    278   				-- print('-- power',rune.minpower,(rune.minpower*powerfac)*time,'//',l.self.powerdraw,l.self.powerdraw/time,'free',l.freepower,'max',l.maxpower)
   269         -				if (rune.minpower*powerfac)*time <= l.self.powerdraw and math.random(rune.rarity) == 1 then
          279  +				if (rune.minpower*powerfac)*time <= l.self.powerdraw and math.random(rune.rarity - pf) == 1 then
   270    280   					choices[#choices + 1] = rune
   271    281   				end
   272    282   			end
   273    283   			if #choices > 0 then
   274    284   				-- if multiple runes were rolled up, be nice to the player
   275    285   				-- and pick the rarest one to give them
   276    286   				local rare, choice = 0
................................................................................
   491    501   		local wrench if not inv:is_empty('wrench') then
   492    502   			wrench = inv:get_stack('wrench',1):get_definition()._proto
   493    503   		end
   494    504   		if fl == 'cache' then
   495    505   			if probe.disjunction then return 0 end
   496    506   			if tl == 'cache' then return 1 end
   497    507   			if tl == 'active' and inv:is_empty('active') then
   498         -				print(dump(wrench))
   499    508   				if wrench and wrench.powers.imbue and not inv:is_empty('amulet') then
   500    509   					local amulet = inv:get_stack('amulet',1)
   501    510   					local rune = inv:get_stack(fl,fi)
   502    511   					local runeid = rune:get_definition()._proto.id
   503    512   					local runegrade = rune:get_meta():get_int('rune_grade')
   504    513   					if sorcery.data.runes[runeid].amulets[amulet:get_definition()._sorcery.amulet.base] then
   505    514   						local spell do -- haaaack

Modified sorcery.md from [5a3ebfda49] to [17b053c9d3].

    57     57    * if you need to travel quickly between two distant places, and you're wealthy enough to afford it, you can build yourself one of the most powerful and complex of magitech devices — the **Teleporter**. it's no mean feat: even the smallest teleporter requires a teleport pad with a reflector above it and a portal node connected to one or the other. the teleporter will then need to be connected to its destination with cables or conduits, and if where you're travelling is very far away, you'll have to build two separate ley nets and bridge them by using an **Attunement Wand** on a pair of **Raycasters** — or perhaps even **Farcasters**. the power required to operate all of these devices is not trivial, and while a Farcaster's signal can pierce through any substance and cross any distance to reach its destination, the farther away each is from the other, the more power each side will consume. and casters can't send current, they can only send signals, so you may need a sizable power plant on both sides of the portal.
    58     58    * if all you need to do is send small items, of course, a **Displacer** is much cheaper, and more flexible. if you're feeling particularly ambitious, you could use a Displacer net to connect your whole kingdom with instantaneous package service.
    59     59    * stop your foes in their tracks by flipping a switch to turn on your **Force Field Emitters**, generating an impenetrable barrier wherever they aim.
    60     60    * who needs elevators when you have **Gravitators**? float gently up a vast borehole, bring attackers crashing down to earth, or slow a fatal plunge to a soft and easy landing.
    61     61    * build graven **Idols** to your gods and set sacrifices to them upon an altar. if they're feeling generous, they might start sending you presents of their own, or consecrating your offerings. but beware: different gods have different tastes (and different powers), and get bored quickly with repetitive offerings.
    62     62    * to bend the ultimate arcane forces of the cosmos to your will, you'll need a **Rune Forge**. with a strong ley-current and a steady supply of **Phials** from an Infuser, a Rune Forge will crystallize thaumic impurities into Runes that can you can imbue into a gemstone **Amulet** with a **Rune Wrench**. each amulet can only be used once before it loses its charge, and it may be a long time before the same kind of rune happens to coalesce in your forge again, but the spells they unleash are unique and priceless boons — or weapons. teleport without a teleporter! purge your environs of all spellcraft no matter how fearsomely potent! surround yourself with a shimmering Disjunction Field that, for a short while, will snuff out any spell it touches and, rendering enemy mages utterly helpless and piercing otherwise impenetrable defenses! stride through solid stone like open air! carve huge tunnels deep into the rock with only a snap of your fingers! rain obliteration down upon a target of your choosing! send forth a titanic bolt of flame with the power to blast open mountainsides! tear the very life force straight from an enemy's body and use it to fortify your being! all this and more can be done with the power of rune magic.
    63     63   
    64         -there's more as well. i have yet to figure out how i want to go about introducing users to the lore, but for now there's some information on the wiki and some things you can glean from creative mode; otherwise you'll have to read the source code.
           64  +there's more as well. i have yet to figure out how i want to go about introducing users to the lore (although you can occasionally find random recipes in dungeon chests, and gods can be coaxed to bestow recipes and cookbooks), but for now there's some information on the wiki and some things you can glean from creative mode; otherwise you'll have to read the source code.
    65     65   
    66     66   # lore
    67     67   `sorcery` supplies a default system of lore (that is, the arbitrary objects that the basic principles of the setting operate over) but this can be augmented or replaced on a per-world basis. for instance, you can substitute your own gods for the Harvest Goddess, Blood God, change their names, and so on, or simply make your own additions to the pantheon. since lore overrides are stored outside the minetest tree, it can be updated without destroying your changes.
    68     68   
    69     69   lore is stored separately from the rest of the game logic, in the 'data' directory of the `sorcery` mod. it is arranged in a hierarchy of thematically-organized tables. for instance, the table of gods can be found in `data/gods.lua`. ideally, lore tables should contain plain data, though they can also contain lambdas if necessary. lore files are evaluated in the second stage of the init process, after library code has been loaded but before any game logic has been instantiated. lore can thus depend on libraries where reasonable (though e.g. colors should be stored as 3-tuples rather than `sorcery.lib.color` objects, and images as texture strings, unless there is a very good reason to do otherwise).
    70     70   
    71     71   lore files should never depend on functions in the `minetest` or `core` namespace! if you really need such functionality, gate it off with an if statement and be sure to return something useful in the event that the namespace isn't defined. *lore is just data:* as a general principle, non-minetest code should be able to evaluate and make use of the lore files, for instance to produce an HTML file tabulating the various potions and how to create them. lore should also not mutate the global environment: while there is currently nothing preventing it from doing so, steps will likely be taken in the future to give each lore module a clean environment to keep it from contaminating the global namespace. right now, all functions and variables declared in a lore file should be defined `local`. the only job of a lore file is to return a table.

Modified spell.lua from [92125c4f2c] to [4f39d5643f].

    28     28   --    disjunction is cast on one of them, they will be removed from the
    29     29   --    table. each entry should have at least a 'player' field; they can
    30     30   --    also contain any other data useful to the spell. if a subject has
    31     31   --    a 'disjoin' field it must be a function called when they are removed
    32     32   --    from the list of spell targets.
    33     33   --  * caster is the individual who cast the spell, if any. a disjunction
    34     34   --    against their person will totally disrupt the spell.
    35         -local log = function(...) sorcery.log('spell',...) end
           35  +local log = sorcery.logger 'spell'
    36     36   
    37     37   -- FIXME saving object refs is iffy, find a better alternative
    38     38   sorcery.spell = {
    39     39   	active = {}
    40     40   }
    41     41   
    42     42   local get_spell_positions = function(spell)
................................................................................
   224    224   		local t if type(when) == 'number' then
   225    225   			t = s.duration * when
   226    226   		else
   227    227   			t = (s.duration * (when.whence or 0)) + when.secs
   228    228   		end
   229    229   		if t then return math.min(s.duration,math.max(0,t)) end
   230    230   
   231         -		log('invalid timespec ' .. dump(when))
          231  +		log.err('invalid timespec ' .. dump(when))
   232    232   		return 0
   233    233   	end
   234    234   	s.queue = function(when,fn)
   235    235   		local elapsed = s.starttime and minetest.get_server_uptime() - s.starttime or 0
   236    236   		local timepast = interpret_timespec(when)
   237    237   		if not timepast then timepast = 0 end
   238    238   		local timeleft = s.duration - timepast
   239    239   		local howlong = (s.delay + timepast) - elapsed
   240    240   		if howlong < 0 then
   241         -			log('cannot time-travel! queue() called with `when` specifying timepoint that has already passed')
          241  +			log.err('cannot time-travel! queue() called with `when` specifying timepoint that has already passed')
   242    242   			howlong = 0
   243    243   		end
   244    244   		s.jobs[#s.jobs+1] = minetest.after(howlong, function()
   245    245   			-- this is somewhat awkward. since we're using a non-polling approach, we
   246    246   			-- need to find a way to account for a caster or subject walking into an
   247    247   			-- existing antimagic field, or someone with an existing antimagic aura
   248    248   			-- walking into range of the anchor. so every time a spell effect would
................................................................................
   334    334   					termqueued = true
   335    335   					s.queue(1,function(s,...)
   336    336   						what(s,...)
   337    337   						if s.terminate then s:terminate() end
   338    338   						sorcery.spell.active[myid] = nil
   339    339   					end)
   340    340   				else
   341         -					log('multiple final timeline events not possible, ignoring')
          341  +					log.warn('multiple final timeline events not possible, ignoring')
   342    342   				end
   343    343   			elseif when == 0 and s.disjunction then
   344    344   				startqueued = true
   345    345   				s.queue(when_raw,function(...)
   346    346   					perform_disjunction_calls()
   347    347   					what(...)
   348    348   				end)

Modified tap.lua from [7b8c107461] to [17fb78473a].

     1      1   local log = sorcery.logger('tap')
     2      2   minetest.register_node('sorcery:tap',{
     3      3   	description = 'Tree Tap';
     4      4   	drawtype = 'mesh';
     5      5   	mesh = 'sorcery-tap.obj';
            6  +	inventory_image = 'sorcery_tap_inv.png';
     6      7   	tiles = {
     7      8   		'default_copper_block.png';
     8      9   		'default_steel_block.png';
     9     10   	};
    10     11   	groups = {
    11     12   		dig_immediate = 2;
    12     13   		attached_node = 1;

Added textures/sorcery_tap_inv.png version [0ade756825].

cannot compute difference between binary files