sorcery  Check-in [f7f6898cbd]

Overview
Comment:add force field emitters, generators, rework leyline distribution algorithm, add utility function for automatically dumping out inventories when an item is dug, add draconium and tyrannium alloys (tentatively), various fixes
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: f7f6898cbda00c7271bbe38e53f5e936538e041b399e1dfa82a17c35fbeda42d
User & Date: lexi on 2020-08-21 11:52:17
Other Links: manifest | tags
Context
2020-08-28
14:08
add recipes, cookbooks, disassembly (to create recipes from items), attunement, farcasters, and portals; various edits for bug fixes and improvements check-in: 9ef6cbcf31 user: lexi tags: trunk
2020-08-21
11:52
add force field emitters, generators, rework leyline distribution algorithm, add utility function for automatically dumping out inventories when an item is dug, add draconium and tyrannium alloys (tentatively), various fixes check-in: f7f6898cbd user: lexi tags: trunk
2020-08-17
13:35
fix bugs, finish grinder, add conduits and condensers to extract and transmit energy from leylines check-in: 9278734b41 user: lexi tags: trunk
Changes

Modified coins.lua from [e5af87fe4c] to [f5d6bba856].

    49     49   	end
    50     50   	inv:set_stack('output',1,ItemStack(nil))
    51     51   	do return end
    52     52   
    53     53   	::foundmetal:: if not inv:is_empty('gem') then
    54     54   		local id = slot_gem:get_name()
    55     55   		for name,gem in pairs(sorcery.data.gems) do
    56         -			print('scanning gem',name,dump(gem))
    57     56   			if gem.foreign then
    58     57   				if id == gem.foreign then gemname = name 
    59     58   					else goto skip end
    60     59   			else
    61     60   				if id == 'sorcery:gem_' .. name then gemname = name
    62     61   					else goto skip end
    63     62   			end
    64     63   			coincount = math.min(coincount, slot_gem:get_count())
    65     64   			do break end
    66     65   		::skip::end
    67     66   	end
    68     67   	
    69     68   	coincount = coincount * coins_per_ingot
    70         -	print('names',coinname,gemname)
    71     69   
    72     70   	local coinname = 'sorcery:coin_' .. metalname ..
    73     71   		((gemname and '_' .. gemname) or '')
    74     72   
    75     73   	inv:set_stack('output',1,ItemStack {
    76     74   		name = coinname;
    77     75   		count = coincount;

Modified data/enchants.lua from [9a3424a733] to [fc080855fd].

    58     58   			{lens = 'concave',   gem = 'mese',     dmg = 1};
    59     59   			{lens = 'concave',   gem = 'sapphire', dmg = 1};
    60     60   		};
    61     61   		desc = 'some damage is repaired when used to mine ore or kill an attacker';
    62     62   		on_dig = function(ctx)
    63     63   			local orepfx = "stone_with_" -- }:<
    64     64   			-- local oredrop = ' lump'
    65         -			local dug = minetest.get_node(ctx.target.under)
    66         -			local barename = string.sub(dug.name, string.find(dug.name, ':') + 1)
    67         -			print('is ore? ',dug.name,barename)
    68         -			if minetest.get_item_group(dug.name, ore) ~= 0 or
           65  +			local barename = string.sub(ctx.node.name, string.find(ctx.node.name, ':') + 1)
           66  +			if minetest.get_item_group(ctx.node.name, 'ore') ~= 0 or
    69     67   			   string.sub(barename,1,string.len(orepfx)) == orepfx 
    70     68   			then
    71         -				print('is ore!')
    72     69   				ctx.tool:add_wear(-(sorcery.enchant.strength(ctx.tool,'harvest') * 2000))
    73     70   				ctx.cost = 3
    74     71   			end
    75     72   		end;
    76     73   	};
    77     74   	conserve = { -- use less magical energy
    78     75   		name = 'Conserve';

Modified data/gems.lua from [6638dda90c] to [c84e6c472a].

    19     19   return {
    20     20   	diamond = {
    21     21   		foreign = 'default:diamond';
    22     22   		tone = {137,240,255};
    23     23   		items = default_items('diamond');
    24     24   		tools = true, armor = true;
    25     25   		maxenergy = 2000;
           26  +		hardness = 8;
           27  +		randomfind = false;
    26     28   		slots = {
    27     29   			{affinity = {'praxic','counterpraxic'}, confluence = 1};
    28     30   			{affinity = {'praxic','syncretic'}, confluence = 0.6};
    29     31   			{affinity = {'counterpraxic', 'entropic'}, confluence = 0.7};
    30     32   		};
    31     33   	};
    32     34   	mese = {
................................................................................
    33     35   		foreign = 'default:mese_crystal';
    34     36   		foreign_shard = 'default:mese_crystal_fragment';
    35     37   		tone = {255,253,94};
    36     38   		energysource = 5;
    37     39   		maxenergy = 600;
    38     40   		items = default_items('mese');
    39     41   		tools = true, armor = true;
           42  +		randomfind = false;
    40     43   		slots = {
    41     44   			{affinity = {'praxic'}, confluence = 1};
    42     45   			{affinity = {'praxic'}, confluence = 0.5};
    43     46   			{affinity = {'cognic'}, confluence = 1};
    44     47   			{affinity = {'syncretic'}, confluence = 0.9};
    45     48   		};
    46     49   	};

Modified data/greases.lua from [018bd8935a] to [0a57245423].

    59     59   		color = {222,57,239};
    60     60   		core = { 'sorcery:oil_mystic', 'sorcery:oil_dawn', 'sorcery:oil_mushroom' };
    61     61   		mix = {
    62     62   			'sorcery:powder_cobalt';
    63     63   			'sorcery:powder_cobalt';
    64     64   			'sorcery:extract_grape';
    65     65   		};
           66  +	};
           67  +	neutralizing = {
           68  +		color = {221,148,95};
           69  +		core = { 'sorcery:oil_dawn', 'sorcery:oil_berry' };
           70  +		mix = {
           71  +			'sorcery:powder_aluminum';
           72  +			'sorcery:powder_lithium';
           73  +			'sorcery:extract_pine';
           74  +		};
    66     75   	};
    67     76   }

Modified data/metals.lua from [f9ae7215e3] to [a6eaeb7ad1].

    27     27   	copper = {
    28     28   		dye = 'orange';
    29     29   		ingot = 'default:copper_ingot';
    30     30   		block = 'default:copperblock';
    31     31   		tone = {255,176,61};
    32     32   		no_tools = true; no_armor = true;
    33     33   		hardness = 2;
           34  +		conduct = 0.1;
    34     35   	};
    35     36   	brass = {
    36     37   		tone = {255,226,87};
    37     38   		dye = 'bright_orange';
    38     39   		artificial = true;
    39     40   		no_tools = true; no_armor = true;
    40     41   		hardness = 3;
................................................................................
   136    137   		no_armor = true; no_tools = true;
   137    138   		power = 1; cooktime = 8; hardness = 1;
   138    139   	};
   139    140   	electrum = {
   140    141   		tone = {212, 255, 0}, alpha = 80;
   141    142   		artificial = true;
   142    143   		hardness = 1;
          144  +		conduct = 0.5;
   143    145   		dye = 'bright_green';
   144    146   		mix = {
   145    147   			metals = {
   146    148   				silver = 1;
   147    149   				gold = 1;
   148    150   			};
   149    151   		};
................................................................................
   172    174   		rarity = 17;
   173    175   		durabilty = 900;
   174    176   		hardness = 6;
   175    177   		power = 3;
   176    178   		speed = 3.5;
   177    179   		cooktime = 30;
   178    180   		maxenergy = 3500;
          181  +		conduct = 2.0;
   179    182   		slots = {
   180    183   			{
   181    184   				affinity = {'counterpraxic'};
   182    185   				confluence = 0.65;
   183    186   				interference = {speed = 1};
   184    187   			};
   185    188   		}
................................................................................
   197    200   		tone = {209,88,241}, alpha = 80;
   198    201   		dye = 'purple';
   199    202   		rarity = 18;
   200    203   		meltpoint = 3;
   201    204   		cooktime = 340;
   202    205   		hardness = 7;
   203    206   		maxenergy = 1800;
          207  +		conduct = 5;
   204    208   		durability = 1900;
   205    209   		speed = 3.2;
   206    210   		img = {
   207    211   			-- ingot = 'sorcery_iridium_ingot.png';
   208    212   			-- block = 'sorcery_iridium_block.png';
   209    213   		};
   210    214   		slots = {
................................................................................
   238    242   	};
   239    243   	impervium = {
   240    244   		tone = {226,255,107}, alpha = 90;
   241    245   		cooktime = 260;
   242    246   		meltpoint = 5;
   243    247   		artificial = true;
   244    248   		speed = 2.1;
   245         -		hardness = 20;
          249  +		hardness = 15;
   246    250   		durability = 5300;
   247    251   		maxenergy = 2300;
   248    252   		watercool = true;
   249    253   		mix = {
   250    254   			metals = {
   251    255   				duranium = 4;
   252    256   				iridium = 2;
................................................................................
   261    265   	};
   262    266   	eternium = {
   263    267   		tone = {156,82,222}, alpha = 100;
   264    268   		cooktime = 500;
   265    269   		meltpoint = 6;
   266    270   		artificial = true;
   267    271   		speed = 2;
   268         -		hardness = 9;
          272  +		hardness = 10;
   269    273   		maxenergy = 1200;
   270    274   		durability = 8100;
   271    275   		watercool = true;
   272    276   		mix = {
   273    277   			metals = {
   274    278   				iridium = 2;
   275    279   				tungsten = 2;
   276    280   				lithium = 1;
   277    281   			};
   278    282   		};
          283  +		sinter = {
          284  +			'sorcery:powder_iridium';
          285  +			'sorcery:powder_iridium';
          286  +			'sorcery:powder_tungsten';
          287  +			'sorcery:powder_tungsten';
          288  +			'sorcery:powder_lithium';
          289  +		};
   279    290   		slots = {
   280    291   			{affinity={'praxic','entropic'}, confluence = 1.4};
   281    292   			{affinity={'praxic','syncretic'}, confluence = 1.1};
   282    293   		}
   283    294   	};
   284    295   	unobtanium = {
   285    296   		tone = {114,255,214}, alpha = 120;
   286    297   		meltpoint = 3;
   287    298   		cooktime = 330;
   288    299   		artificial = true;
   289    300   		maxenergy = 4000;
   290    301   		hardness = 7;
   291    302   		durability = 3300;
          303  +		conduct = 15;
   292    304   		speed = 3.4;
   293    305   		slots = {
   294    306   			{affinity={'praxic'}, confluence = 0.7};
   295    307   			{affinity={'counterpraxic'}, confluence = 1.2};
   296    308   			{affinity={'cognic'}, confluence = 1.1};
   297    309   		};
   298    310   		mix = {
................................................................................
   299    311   			metals = {
   300    312   				impervium = 8;
   301    313   				eternium = 4;
   302    314   				cobalt = 1;
   303    315   			};
   304    316   		};
   305    317   	};
   306         -	-- draconium = {artificial=true;};
   307         -	-- tyrannium = {artificial=true;};
          318  +	draconium = {
          319  +		tone = {255,20,80}, alpha = 110;
          320  +		artificial=true;
          321  +		meltpoint = 5;
          322  +		cooktime = 120;
          323  +		hardness = 8;
          324  +		maxconduct = 10;
          325  +		speed = 1.7;
          326  +		maxenergy = 2200;
          327  +		durability = 1500;
          328  +		slots = {
          329  +			{affinity={'praxic'},confluence=3};
          330  +			{affinity={'syncretic'},confluence=2};
          331  +		};
          332  +		sinter = {
          333  +			'sorcery:powder_lithium';
          334  +			'sorcery:powder_aluminum';
          335  +			'sorcery:powder_iridium';
          336  +			'sorcery:oil_flame';
          337  +		};
          338  +	};
          339  +	tyrannium = {
          340  +		tone = {46,255,245}, alpha = 80;
          341  +		artificial=true;
          342  +		hardness = 20;
          343  +		meltpoint = 6;
          344  +		cooktime = 240;
          345  +		maxenergy = 800;
          346  +		durability = 4000;
          347  +		speed = 3.0;
          348  +		slots = {};
          349  +	};
   308    350   };

Modified data/spells.lua from [47b0c7f3e9] to [a738b55bd0].

   384    384   			local duration = (ctx.base.gem == 'amethyst' and 60) or 30
   385    385   			if ctx.base.gem == 'diamond' then
   386    386   				power = power * (math.random()*2)
   387    387   				range = range * (math.random()*2)
   388    388   				duration = duration * (math.random()*2)
   389    389   			end
   390    390   			local lum = math.ceil((power/maxpower) * minetest.LIGHT_MAX)
   391         -			print('setting lum',lum)
   392    391   			for i=1,power do
   393    392   				local pos = vector.add(center, {
   394    393   					x = math.random(-range,range);
   395    394   					z = math.random(-range,range);
   396    395   					y = math.random(0,range/2);
   397    396   				})
   398    397   				local delta = vector.subtract(pos,center)

Modified enchanter.lua from [41724791de] to [15ff8624ed].

   231    231   	mesh = 'sorcery-enchanter.obj';
   232    232   	paramtype = 'light';
   233    233   	paramtype2 = 'facedir';
   234    234   	groups = { cracky = 2, oddly_breakable_by_hand = 2 };
   235    235   	sunlight_propagates = true;
   236    236   	selection_box = hitbox;
   237    237   	collision_box = hitbox;
          238  +	after_dig_node = sorcery.lib.node.purge_container;
   238    239   	tiles = {
   239    240   		"default_obsidian.png";
   240    241   		"default_steel_block.png";
   241    242   		"default_bronze_block.png";
   242    243   		"default_junglewood.png";
   243    244   		"default_gold_block.png";
   244    245   	};
................................................................................
   412    413   	if ench.energy == 0 then
   413    414   		ench.spells = {}
   414    415   		sorcery.enchant.set(tool,ench)
   415    416   	else
   416    417   		sorcery.enchant.set(tool,ench,true)
   417    418   	end
   418    419   	puncher:set_wielded_item(tool)
          420  +
          421  +	-- perform leyline checks and call notify if necessary
          422  +	if minetest.get_item_group(node.name, 'sorcery_ley_device') ~= 0 then
          423  +		for _,p in pairs(sorcery.ley.txofs) do
          424  +			local sum = vector.add(pos,p)
          425  +			if minetest.get_item_group(minetest.get_node(sum).name, 'sorcery_ley_device') ~= 0 then
          426  +				sorcery.ley.notify(sum)
          427  +			end
          428  +		end
          429  +	end
   419    430   end)
   420    431   
   421    432   minetest.register_chatcommand('enchants', {
   422    433   	description = 'Log information about the currently held object\'s enchantment';
   423    434   	privs = { server = true };
   424    435   	func = function(caller,params)
   425    436   		local tool = minetest.get_player_by_name(caller):get_wielded_item()

Added forcefield.lua version [4c792fc807].

            1  +local constants = {
            2  +	cost_per_barrier = 0.1;
            3  +	-- 0.1 points of current will be required per second per
            4  +	-- unit of charge given to a barrier-block when ley-force
            5  +	-- of counterpraxic affinity is not available
            6  +}
            7  +local aimtbl = {
            8  +	[0] = {x =  0, y =  1, z =  0};
            9  +	[1] = {x =  0, y =  0, z =  1};
           10  +	[2] = {x =  0, y =  0, z = -1};
           11  +	[3] = {x =  1, y =  0, z =  0};
           12  +	[4] = {x = -1, y =  0, z =  0};
           13  +	[5] = {x =  0, y = -1, z =  0};
           14  +}
           15  +local pofstbl = {
           16  +	[0] = {x=0.5, y=0,   z=0.5};
           17  +	[1] = {x=0.5, y=0.5, z=0};
           18  +	[2] = {x=0.5, y=0.5, z=0};
           19  +	[3] = {x=0,   y=0.5, z=0.5};
           20  +	[4] = {x=0,   y=0.5, z=0.5};
           21  +	[5] = {x=0.5, y=0,   z=0.5};
           22  +}
           23  +local calc_cost = function(pos,time)
           24  +	local node = minetest.get_node(pos)
           25  +	local aim = aimtbl[math.floor(node.param2 / 4)]
           26  +	local tgts = {}
           27  +	for i=1,5 do
           28  +		local tpos = vector.add(pos, vector.multiply(aim,i))
           29  +		local n = minetest.get_node(tpos)
           30  +		if n.name == 'air' then tgts[#tgts + 1] = {tpos,0} else
           31  +			local f = minetest.get_item_group(n.name, 'sorcery_force_barrier')
           32  +			if f > 0 then
           33  +				tgts[#tgts + 1] = {tpos,f}
           34  +			else break end
           35  +		end
           36  +	end
           37  +	return {
           38  +		aim = aim;
           39  +		maxcost = #tgts * constants.cost_per_barrier * time;
           40  +		mincost = math.min(1,#tgts) * constants.cost_per_barrier * time;
           41  +		targets = tgts;
           42  +	}
           43  +end
           44  +for i=1,10 do
           45  +	minetest.register_node('sorcery:air_barrier_' .. tostring(i), {
           46  +		drawtype = 'airlike';
           47  +		walkable = true;
           48  +		pointable = false;
           49  +		sunlight_propagates = true;
           50  +		paramtype = 'light';
           51  +		light_source = i;
           52  +		groups = {
           53  +			sorcery_force_barrier = i;
           54  +		};
           55  +		-- _proto = {
           56  +		-- 	strength = i;
           57  +		-- };
           58  +		on_construct = function(pos)
           59  +			minetest.get_node_timer(pos):start(1)
           60  +		end;
           61  +		on_timer = function(pos,delta)
           62  +			local dec = 40*delta
           63  +			local node = minetest.get_node(pos)
           64  +			local newstr = math.ceil(10 * ((node.param2 - dec) / 0xFF))
           65  +			local newnode = 'sorcery:air_barrier_' .. tostring(newstr)
           66  +			if newstr <= 0 then
           67  +				minetest.remove_node(pos)
           68  +				return false
           69  +			else
           70  +				for _,c in pairs {{165,255,252}, {146,205,255}, {190,93,253}} do
           71  +					minetest.add_particlespawner {
           72  +						time = 1;
           73  +						amount = 15;
           74  +						minpos = vector.add(pos, -0.6);
           75  +						maxpos = vector.add(pos, 0.6);
           76  +						minvel = {x = 0, y = 0, z = 0};
           77  +						maxvel = {x = 0, y = 0, z = 0};
           78  +						minacc = {x = -0.1, y = -0.1, z = -0.1};
           79  +						maxacc = {x =  0.1, y =  0.1, z =  0.1};
           80  +						minexptime = 0.5;
           81  +						maxexptime = 1.0;
           82  +						minsize = 0.2;
           83  +						minsize = 0.7;
           84  +						texture = sorcery.lib.image('sorcery_spark.png'):multiply(sorcery.lib.color(c)):render();
           85  +						glow = 14;
           86  +						animation = {
           87  +							length = 1.1;
           88  +							type = 'vertical_frames';
           89  +							aspect_h = 16, aspect_w = 16;
           90  +						};
           91  +					}
           92  +				end
           93  +				minetest.swap_node(pos, {
           94  +					name = newnode;
           95  +					param1 = node.param1;
           96  +					param2 = newstr;
           97  +				})
           98  +				return true
           99  +			end
          100  +		end;
          101  +	})
          102  +end
          103  +minetest.register_node('sorcery:emitter_barrier', {
          104  +	description = "Barrier Screen Emitter";
          105  +	paramtype2 = 'facedir';
          106  +	groups = {
          107  +		cracky = 2;
          108  +		sorcery_ley_device = 1;
          109  +	};
          110  +	tiles = {
          111  +		'sorcery_emitter_barrier_top.png';
          112  +		'sorcery_emitter_barrier_bottom.png';
          113  +		'sorcery_emitter_barrier_front.png^[transformR270';
          114  +		'sorcery_emitter_barrier_front.png^[transformFXR90';
          115  +		'sorcery_emitter_barrier_side.png';
          116  +		'sorcery_emitter_barrier_side.png';
          117  +	};
          118  +	on_construct = function(pos)
          119  +		minetest.get_node_timer(pos):start(1)
          120  +	end;
          121  +	on_timer = function(pos,delta)
          122  +		local orientation = math.floor(minetest.get_node(pos).param2 / 4)
          123  +		local costs = calc_cost(pos,delta)
          124  +		local l = sorcery.ley.netcaps(pos,delta)
          125  +		if l.self.powerdraw >= costs.mincost then
          126  +			local dist = l.self.powerdraw / (constants.cost_per_barrier * delta)
          127  +			for i=1,math.floor(dist) do
          128  +				local t = costs.targets[i]
          129  +				local str = math.min(0xFF,t[2] + 50*delta);
          130  +				minetest.swap_node(t[1], {
          131  +					name = 'sorcery:air_barrier_' .. math.max(1, math.floor(10*(str/0xFF)));
          132  +					param2 = str;
          133  +				})
          134  +				minetest.get_node_timer(t[1]):start(1)
          135  +			end
          136  +
          137  +			local pn = vector.add(pos, vector.divide(costs.aim,2));
          138  +			local pp = vector.add(pn, pofstbl[orientation])
          139  +			pn = vector.subtract(pn,  pofstbl[orientation])
          140  +			
          141  +			minetest.add_particlespawner {
          142  +				time = 1;
          143  +				amount = 20 * dist;
          144  +				minpos = pn;
          145  +				maxpos = pp;
          146  +				minvel = costs.aim;
          147  +				maxvel = vector.multiply(costs.aim,2);
          148  +				minsize = 0.3;
          149  +				maxsize = 0.5;
          150  +				minexptime = dist*0.5;
          151  +				maxexptime = dist*0.5;
          152  +				texture = sorcery.lib.image('sorcery_spark.png'):multiply(sorcery.lib.color(240,255,160)):render();
          153  +				glow = 14;
          154  +				animation = {
          155  +					length = dist + 0.1;
          156  +					type = 'vertical_frames';
          157  +					aspect_h = 16, aspect_w = 16;
          158  +				};
          159  +			}
          160  +			return true
          161  +		end
          162  +		return false
          163  +	end;
          164  +	after_place_node = function(pos, placer, stack, point)
          165  +		local vec = vector.subtract(point.under, pos)
          166  +		local n = minetest.get_node(pos)
          167  +		n.param2 = minetest.dir_to_facedir(vec)
          168  +		minetest.swap_node(pos,n)
          169  +	end;
          170  +	_sorcery = {
          171  +		ley = {
          172  +			mode='consume', affinity={'counterpraxic'},
          173  +			power = function(pos,time)
          174  +				local l = calc_cost(pos,time)
          175  +				return l.mincost, l.maxcost
          176  +			end;
          177  +		};
          178  +		on_leychange = function(pos)
          179  +			minetest.get_node_timer(pos):start(1)
          180  +		end;
          181  +	};
          182  +})

Modified harvester.lua from [8384a43ac0] to [97dec98beb].

    15     15   
    16     16   minetest.register_node('sorcery:harvester', {
    17     17   	description = 'Harvester';
    18     18   	drawtype = 'mesh';
    19     19   	paramtype = 'light';
    20     20   	paramtype2 = 'facedir';
    21     21   	mesh = 'sorcery-harvester.obj';
           22  +	after_dig_node = sorcery.lib.node.purge_container;
    22     23   	groups = { cracky = 2; oddly_breakable_by_hand = 1; };
    23     24   	sunlight_propagates = true;
    24     25   	selection_box = hitbox;
    25     26   	collision_box = hitbox;
    26     27   	tiles = {
    27     28   		amethyst:render();
    28     29   		'default_copper_block.png';

Modified infuser.lua from [c06011384f] to [3997cbe28f].

   194    194   end
   195    195   
   196    196   minetest.register_node("sorcery:infuser", {
   197    197   	description = "Infuser";
   198    198   	drawtype = "mesh";
   199    199   	mesh = "infuser.obj";
   200    200   	paramtype2 = "facedir";
          201  +	after_dig_node = sorcery.lib.node.purge_container;
   201    202   	tiles = { -- FIXME
   202    203   		"default_stone.png",
   203    204   		"default_copper_block.png",
   204    205   		"default_steel_block.png",
   205    206   		"default_bronze_block.png",
   206    207   		"default_tin_block.png",
   207    208   	};

Modified init.lua from [8277bf1fce] to [752c791349].

    25     25   -- and load them automatically, as interdependencies
    26     26   -- exist (especially with /lib) and we need to be very
    27     27   -- careful about the order they're loaded in
    28     28   
    29     29   sorcery.unit('data') {'ui'}
    30     30   sorcery.unit('lib') {
    31     31   	-- convenience
    32         -	'str';
           32  +	'str', 'node';
    33     33   	-- serialization
    34     34   	'marshal', 'json';
    35     35   	-- data structures
    36     36   	'tbl', 'class';
    37     37   	-- wrappers
    38     38   	'color', 'image', 'ui';
    39     39   }
................................................................................
    47     47   	'potions', 'oils', 'greases',
    48     48   		'draughts', 'elixirs',
    49     49   		'philters', 'extracts';
    50     50   	'register';
    51     51   }
    52     52   
    53     53   for _,u in pairs {
    54         -	'leylines'; 'ores'; 'gems';
           54  +	'ores'; 'gems';	'leylines';
    55     55   	'potions'; 'infuser'; 'altar'; 'wands';
    56     56   	'tools'; 'enchanter'; 'harvester';
    57     57   	'metallurgy-hot', 'metallurgy-cold';
    58     58   	'entities'; 'recipes'; 'coins';
    59         -	'interop'; 'tnodes';
           59  +	'interop'; 'tnodes'; 'forcefield';
    60     60   } do sorcery.load(u) end

Modified leylines.lua from [112e3b2a50] to [8532b8e5fe].

    71     71   	output = 'sorcery:conduit 4';
    72     72   	recipe = {
    73     73   		{'default:copper_ingot', 'default:copper_ingot',  'default:copper_ingot'};
    74     74   		{'default:copper_ingot', 'sorcery:electrumblock', 'default:copper_ingot'};
    75     75   		{'default:copper_ingot', 'default:copper_ingot',  'default:copper_ingot'};
    76     76   	};
    77     77   };
    78         -minetest.register_craft {
    79         -	output = 'sorcery:wire 4';
    80         -	recipe = {
    81         -		{'', 'basic_materials:copper_wire',''};
    82         -		{'', 'sorcery:fragment_electrum',  ''};
    83         -		{'', 'basic_materials:copper_wire',''};
           78  +
           79  +local makeswitch = function(switch, desc, tex, tiles, power)
           80  +	for _,active in pairs{true,false} do
           81  +		local turn = function(pos)
           82  +			local n = minetest.get_node(pos)
           83  +			minetest.sound_play('doors_steel_door_open', {
           84  +				gain = 0.7;
           85  +				pos = pos;
           86  +			}, true)
           87  +			local leymap = active and sorcery.ley.mapnet(pos) or nil
           88  +			minetest.swap_node(pos, {
           89  +				name = active and (switch .. '_off')
           90  +							   or  switch;
           91  +				param1 = n.param1;
           92  +				param2 = n.param2;
           93  +			})
           94  +			if active then
           95  +				-- if we're turning it off, use the old map,
           96  +				-- because post-swap the network will be
           97  +				-- broken and notify won't reach everyone
           98  +				leymap.map[leymap.startpos] = nil
           99  +				sorcery.ley.notifymap(leymap.map)
          100  +			else sorcery.ley.notify(pos) end
          101  +		end
          102  +		local tl = table.copy(tiles)
          103  +		tl[6] = tex .. '^sorcery_ley_switch_panel.png^sorcery_ley_switch_' .. (active and 'down' or 'up') .. '.png';
          104  +		minetest.register_node(switch .. (active and '' or '_off'), {
          105  +			description = desc;
          106  +			drop = switch;
          107  +			tiles = tl;
          108  +			paramtype2 = 'facedir';
          109  +			groups = {
          110  +				cracky = 2; choppy = 1;
          111  +				punch_operable = 1;
          112  +				sorcery_ley_device = active and 1 or 0;
          113  +			};
          114  +			_sorcery = {
          115  +				ley = active and {
          116  +					mode = 'signal'; power = power;
          117  +				} or nil;
          118  +			};
          119  +			on_punch = function(pos,node,puncher,point)
          120  +				if puncher ~= nil then
          121  +					if puncher:get_wielded_item():is_empty() then
          122  +						turn(pos)
          123  +					end
          124  +				end
          125  +				return minetest.node_punch(pos,node,puncher,point)
          126  +			end;
          127  +			on_rightclick = turn;
          128  +		})
          129  +	end
          130  +end
          131  +
          132  +for _,b in pairs {
          133  +	{'Applewood', 'wood', 'default_wood.png'};
          134  +	{'Junglewood', 'junglewood', 'default_junglewood.png'};
          135  +	{'Pine', 'pine_wood', 'default_pine_wood.png'};
          136  +	{'Acacia', 'acacia_wood', 'default_pine_wood.png'};
          137  +	{'Aspen', 'aspen_wood', 'default_aspen_wood.png'};
          138  +	{'Stone', 'stone', 'default_stone.png'};
          139  +	{'Cobblestone', 'cobble', 'default_cobble.png'};
          140  +	{'Stone Brick', 'stonebrick', 'default_stone_brick.png'};
          141  +	{'Brick', 'brick', 'default_brick.png'};
          142  +} do
          143  +	local id = 'sorcery:conduit_half_' .. b[2]
          144  +	local switch = 'sorcery:conduit_switch_' .. b[2]
          145  +	local item = (b[4] or 'default') .. ':' .. b[2]
          146  +	local tex = b[3]
          147  +	local mod = '^[lowpart:50:'
          148  +	local sidemod = '^[transformR270^[lowpart:50:'
          149  +	local unflip = '^[transformR90'
          150  +	local tiles = {
          151  +		'sorcery_conduit_copper_top.png'..mod..tex; -- top
          152  +		tex..mod..'sorcery_conduit_copper_top.png';
          153  +		tex .. sidemod .. 'sorcery_conduit_copper_side.png' .. unflip; -- side
          154  +		'sorcery_conduit_copper_side.png' .. sidemod .. tex .. unflip; -- side
          155  +		'sorcery_conduit_copper_side.png'; -- back
          156  +		tex; -- front
          157  +	}
          158  +	minetest.register_node(id, {
          159  +		description = 'Half-' .. b[1] .. ' Conduit';
          160  +		paramtype2 = 'facedir';
          161  +		groups = {
          162  +			cracky = 2;
          163  +			choppy = 1;
          164  +			sorcery_ley_device = 1;
          165  +			sorcery_ley_conduit = 1;
          166  +		};
          167  +		_sorcery = {
          168  +			ley = { mode = 'signal'; power = 5; }
          169  +		};
          170  +		tiles = tiles;
          171  +	})
          172  +	minetest.register_craft {
          173  +		output = id .. ' 4';
          174  +		recipe = {
          175  +			{item, 'sorcery:conduit'};
          176  +			{item, 'sorcery:conduit'};
          177  +		};
          178  +	};
          179  +	makeswitch(switch, b[1] .. " Conduit Switch", tex, tiles, 5)
          180  +	minetest.register_craft {
          181  +		output = switch;
          182  +		recipe = {
          183  +			{'xdecor:lever_off',id};
          184  +		};
    84    185   	}
    85         -};
          186  +end
          187  +makeswitch('sorcery:conduit_switch', "Conduit Switch", 'sorcery_conduit_copper_side.png', {
          188  +	'sorcery_conduit_copper_top.png';
          189  +	'sorcery_conduit_copper_top.png';
          190  +	'sorcery_conduit_copper_side.png';
          191  +	'sorcery_conduit_copper_side.png';
          192  +	'sorcery_conduit_copper_side.png';
          193  +	'sorcery_conduit_copper_side.png';
          194  +}, 10)
          195  +minetest.register_craft {
          196  +	output = 'sorcery:conduit_switch';
          197  +	recipe = {
          198  +		{'xdecor:lever_off','sorcery:conduit'};
          199  +	};
          200  +}
          201  +
          202  +for name,metal in pairs(sorcery.data.metals) do
          203  +	if metal.conduct then
          204  +		local cable = 'sorcery:cable_' .. name
          205  +		minetest.register_node(cable, {
          206  +			description = sorcery.lib.str.capitalize(name) .. " Cable";
          207  +			drawtype = 'nodebox';
          208  +			groups = {
          209  +				sorcery_ley_device = 1; snappy = 3; attached = 1;
          210  +				sorcery_ley_cable = 1;
          211  +			};
          212  +			_sorcery = {
          213  +				ley = { mode = 'signal', power = metal.conduct };
          214  +			};
          215  +			sunlight_propagates = true;
          216  +			node_box = {
          217  +				type = 'connected';
          218  +				disconnected   = { -0.05, -0.35, -0.40; 0.05, -0.25, 0.40 };
          219  +				connect_front  = { -0.05, -0.35, -0.50; 0.05, -0.25, 0.05 };
          220  +				connect_back   = { -0.05, -0.35, -0.05; 0.05, -0.25, 0.50 };
          221  +				connect_right  = { -0.05, -0.35, -0.05; 0.50, -0.25, 0.05 };
          222  +				connect_left   = { -0.50, -0.35, -0.05; 0.05, -0.25, 0.05 };
          223  +				connect_top    = { -0.05, -0.25, -0.05; 0.05,  0.50, 0.05 };
          224  +				connect_bottom = { -0.05, -0.50, -0.05; 0.05, -0.35, 0.05 };
          225  +			};
          226  +			connects_to = { 'group:sorcery_ley_device', 'default:mese' };
          227  +			-- harcoding mese is kind of cheating -- figure out a
          228  +			-- better way to do this for the longterm
          229  +			paramtype = 'light';
          230  +			-- paramtype2 = 'facedir';
          231  +			after_place_node = function(pos, placer, stack, point)
          232  +				local vec = vector.subtract(point.under, pos)
          233  +				local n = minetest.get_node(pos)
          234  +				n.param2 = minetest.dir_to_facedir(vec)
          235  +				minetest.swap_node(pos,n)
          236  +			end;
          237  +			tiles = { 'sorcery_ley_plug.png' };
          238  +		})
          239  +
          240  +		minetest.register_craft {
          241  +			output = cable .. ' 8';
          242  +			recipe = {
          243  +				{'basic_materials:copper_wire','basic_materials:copper_wire','basic_materials:copper_wire'};
          244  +				{ metal.parts.fragment, metal.parts.fragment, metal.parts.fragment };
          245  +				{'basic_materials:copper_wire','basic_materials:copper_wire','basic_materials:copper_wire'};
          246  +			};
          247  +			replacements = {
          248  +				{'basic_materials:copper_wire', 'basic_materials:empty_spool'};
          249  +				{'basic_materials:copper_wire', 'basic_materials:empty_spool'};
          250  +				{'basic_materials:copper_wire', 'basic_materials:empty_spool'};
          251  +				{'basic_materials:copper_wire', 'basic_materials:empty_spool'};
          252  +				{'basic_materials:copper_wire', 'basic_materials:empty_spool'};
          253  +				{'basic_materials:copper_wire', 'basic_materials:empty_spool'};
          254  +			};
          255  +		};
          256  +	end
          257  +end
          258  +
          259  +-- ley.notify will normally be called automatically, but if a
          260  +-- ley-producer or consume has fluctuating levels of energy
          261  +-- consumption, it should call this function when levels change
          262  +sorcery.ley.notifymap = function(map)
          263  +	for pos,name in pairs(map) do
          264  +		local props = minetest.registered_nodes[name]._sorcery
          265  +		if props and props.on_leychange then
          266  +			props.on_leychange(pos)
          267  +		end
          268  +	end
          269  +end
          270  +sorcery.ley.notify = function(pos)
          271  +	local n = sorcery.ley.mapnet(pos)
          272  +	sorcery.ley.notifymap(n.map)
          273  +end
    86    274   
    87    275   sorcery.ley.field_to_current = function(strength,time)
    88    276   	local ley_factor = 0.25
    89    277   	-- a ley harvester will produce this much current with
    90    278   	-- access to a full-strength leyline
    91    279   	
    92    280   	return strength * ley_factor * time;
................................................................................
   102    290   			-0.5, -0.5, -0.5;
   103    291   			 0.5,  1.2,  0.5;
   104    292   		};
   105    293   	};
   106    294   	minetest.register_node('sorcery:condenser', {
   107    295   		description = 'Condenser';
   108    296   		drawtype = 'mesh';
          297  +		paramtype2 = 'facedir';
   109    298   		mesh = 'sorcery-condenser.obj';
   110    299   		selection_box = box;
   111    300   		collision_box = box;
   112    301   		tiles = {
   113    302   			amethyst:render();
   114    303   			'sorcery_condenser.png';
   115    304   			'default_tin_block.png';
................................................................................
   122    311   			sorcery_ley_device = 1;
   123    312   		};
   124    313   		on_construct = function(pos)
   125    314   			local meta = minetest.get_meta(pos)
   126    315   			meta:set_string('infotext','Condenser')
   127    316   		end;
   128    317   		_sorcery = {
   129         -			ley = { mode = 'produce' };
   130         -			on_leycalc = function(pos,time)
   131         -				local l = sorcery.ley.estimate(pos)
   132         -				return {
   133         -					power = sorcery.ley.field_to_current(l.force, time);
   134         -					affinity = l.aff;
   135         -				}
   136         -			end;
          318  +			ley = { mode = 'produce';
          319  +				power = function(pos,time)
          320  +					return sorcery.ley.field_to_current(sorcery.ley.estimate(pos).force, time);
          321  +				end;
          322  +				affinity = function(pos)
          323  +					return sorcery.ley.estimate(pos).aff
          324  +				end;
          325  +			};
   137    326   		};
   138    327   	})
   139    328   end
   140    329   
   141    330   minetest.register_craft {
   142    331   	output = 'sorcery:condenser';
   143    332   	recipe = {
   144    333   		{'sorcery:accumulator'};
   145    334   		{'sorcery:conduit'};
   146    335   	};
   147    336   }
          337  +sorcery.ley.txofs = {
          338  +	{x =  0, z =  0, y =  0};
          339  +	{x = -1, z =  0, y =  0};
          340  +	{x =  1, z =  0, y =  0};
          341  +	{x =  0, z = -1, y =  0};
          342  +	{x =  0, z =  1, y =  0};
          343  +	{x =  0, z =  0, y = -1};
          344  +	{x =  0, z =  0, y =  1};
          345  +}
   148    346   sorcery.ley.mapnet = function(startpos,power)
   149    347   	-- this function returns a list of all the nodes accessible from
   150    348   	-- a ley network and their associated positions
   151         -	local net = {}
          349  +	local net,checked = {},{}
   152    350   	power = power or 0
   153    351   	
   154    352   	local devices = {
   155    353   		consume = {};
   156    354   		produce = {};
   157    355   		signal = {};
   158    356   	}
   159    357   	local numfound = 0
   160    358   	local maxconduct = 0
   161    359   	local minconduct
          360  +	local startkey
   162    361   	local foundp = function(p)
   163         -		for k in pairs(net) do
          362  +		for _,k in pairs(checked) do
   164    363   			if vector.equals(p,k) then return true end
   165    364   		end
   166    365   		return false
   167    366   	end
   168    367   	-- we're implementing this with a recursive function to start with
   169    368   	-- but this could rapidly lead to stack overflows so we should
   170    369   	-- replace it with a linear one at some point
   171    370   	local function find(positions)
   172    371   		local searchnext = {}
   173    372   		for _,pos in pairs(positions) do
   174         -			for _,p in pairs {
   175         -				{x =  0, z =  0, y =  0};
   176         -				{x = -1, z =  0, y =  0};
   177         -				{x =  1, z =  0, y =  0};
   178         -				{x =  0, z = -1, y =  0};
   179         -				{x =  0, z =  1, y =  0};
   180         -				{x =  0, z =  0, y = -1};
   181         -				{x =  0, z =  0, y =  1};
   182         -			} do local sum = vector.add(pos,p)
          373  +			for _,p in pairs(sorcery.ley.txofs) do
          374  +				local sum = vector.add(pos,p)
   183    375   				if not foundp(sum) then
          376  +					checked[#checked + 1] = sum
   184    377   					local nodename = minetest.get_node(sum).name
          378  +					if nodename == 'ignore' then
          379  +						minetest.load_area(sum)
          380  +						nodename = minetest.get_node(sum).name
          381  +					end
   185    382   					if minetest.get_item_group(nodename,'sorcery_ley_device') ~= 0
   186    383   					   or sorcery.data.compat.ley[nodename] then
   187         -						local d = sorcery.ley.sample(pos,1,nodename)
          384  +						local d = sorcery.ley.sample(pos,1,nodename,{query={mode=true}})
   188    385   						assert(d.mode == 'signal'
   189    386   						    or d.mode == 'consume'
   190    387   						    or d.mode == 'produce')
   191    388   						devices[d.mode][#(devices[d.mode]) + 1] = {
   192    389   							id = nodename; pos = sum;
   193    390   						}
   194    391   						if d.mode == 'signal' then
          392  +							d.power = sorcery.ley.sample(pos,1,nodename,{query={power=true}}).power
   195    393   							if d.power > power then
   196    394   								if minconduct then
   197    395   									if d.power < minconduct then
   198    396   										minconduct = d.power
   199    397   									end
   200    398   								else minconduct = d.power end
   201    399   								if d.power > maxconduct then
   202    400   									maxconduct = d.power
   203    401   								end
   204    402   							end
   205    403   						end
   206    404   						numfound = numfound + 1;
   207    405   						net[sum] = nodename;
          406  +						if not startkey then
          407  +							if vector.equals(startpos,sum) then
          408  +								startkey = sum
          409  +							end
          410  +						end
   208    411   						searchnext[#searchnext + 1] = sum;
   209    412   					end
   210    413   				end
   211    414   			end
   212    415   		end
   213    416   		if #searchnext > 0 then find(searchnext) end
   214    417   	end
................................................................................
   216    419   	find{startpos}
   217    420   
   218    421   	if numfound > 0 then
   219    422   		return {
   220    423   			count = numfound;
   221    424   			map = net;
   222    425   			devices = devices;
          426  +			startpos = startkey;
   223    427   			conduct = {
   224    428   				min = minconduct;
   225    429   				max = maxconduct;
   226    430   			};
   227    431   		}
   228    432   	else return nil end
   229    433   end
................................................................................
   241    445   		[3] = 'signal';
   242    446   	}
   243    447   	for i=1,#afftbl  do afftbl [afftbl [i]] = i end
   244    448   	for i=1,#modetbl do modetbl[modetbl[i]] = i end
   245    449   	local m = sorcery.lib.marshal
   246    450   	local enc, dec = m.transcoder {
   247    451   		mode = m.t.u8;
   248         -		power = m.t.u32; -- power generated/consumed * 10,000
          452  +		minpower = m.t.u32; -- min power generated/consumed * 10,000
          453  +		maxpower = m.t.u32; -- max power generated/consumed * 10,000
   249    454   		affinity = m.g.array(m.t.u8); -- indexes into afftbl
   250    455   	}
   251    456   	sorcery.ley.encode = function(l)
   252    457   		local idxs = {}
   253    458   		for _,k in pairs(l.affinity) do
   254    459   			idxs[#idxs+1] = afftbl[k]
   255    460   		end
   256    461   		return meta_armor(enc {
   257    462   			mode = modetbl[l.mode];
   258         -			power = l.power * 10000;
          463  +			minpower = l.minpower * 10000;
          464  +			maxpower = l.maxpower * 10000;
   259    465   			affinity = idxs;
   260    466   		}, true)
   261    467   	end
   262    468   	sorcery.ley.decode = function(str)
   263    469   		local obj = dec(meta_dearmor(str,true))
   264    470   		local affs = {}
   265    471   		for _,k in pairs(obj.affinity) do
   266    472   			affs[#affs+1] = afftbl[k]
   267    473   		end
   268    474   		return {
   269    475   			mode = modetbl[obj.mode];
   270         -			power = obj.power / 10000.0;
          476  +			minpower = obj.minpower / 10000.0;
          477  +			maxpower = obj.maxpower / 10000.0;
          478  +			power = (obj.minpower == obj.maxpower) and obj.minpower or nil;
   271    479   			affinity = affs;
   272    480   		}
   273    481   	end
   274    482   end
   275    483   sorcery.ley.setnode = function(pos,l)
   276    484   	local meta = minetest.get_node(pos)
   277    485   	meta:set_string('sorcery:ley',sorcery.ley.encode(l))
   278    486   end
   279    487   
   280         -sorcery.ley.sample = function(pos,timespan,name)
          488  +sorcery.ley.sample = function(pos,timespan,name,flags)
   281    489   	-- returns how much ley-force can be transmitted by a
   282    490   	-- device over timespan
          491  +	local ret = {}
   283    492   	name = name or minetest.get_node(pos).name
          493  +	flags = flags or {}
          494  +	flags.query = flags.query or {
          495  +		mode = true; power = true; affinity = true;
          496  +		minpower = true; maxpower = true;
          497  +	}
   284    498   	local props = minetest.registered_nodes[name]._sorcery
   285         -	local callback = props and props.on_leycalc or nil
   286         -	local p,a,m
   287         -	if callback then
   288         -		local gen = callback(pos,timespan)
   289         -		p = gen.power
   290         -		a = gen.affinity
   291         -		m = gen.mode
          499  +
          500  +	local evaluate = function(v)
          501  +		if type(v) == 'function' then
          502  +			return v(pos)
          503  +		else return v end
   292    504   	end
   293    505   
   294         -	if not (p and a and m) then
          506  +	local leymeta do
   295    507   		local nm = minetest.get_meta(pos)
   296    508   		if nm:contains('sorcery:ley') then
   297         -			local l = sorcery.ley.decode(nm:get_string('sorcery:ley'))
   298         -			p = p or sorcery.ley.field_to_current(l.power,timespan)
   299         -			a = a or l.affinity
   300         -			m = m or l.mode
          509  +			leymeta = sorcery.ley.decode(nm:get_string('sorcery:ley'))
          510  +		end
          511  +	end
          512  +
          513  +	local compat = sorcery.data.compat.ley[name] 
          514  +
          515  +	local lookup = function(k,default)
          516  +		if leymeta and leymeta[k] then return leymeta[k]
          517  +		elseif props and props.ley and props.ley[k] then return props.ley[k]
          518  +		elseif compat and compat[k] then return compat[k]
          519  +		else return default end 
          520  +	end
          521  +	if flags.query.mode     then ret.mode     = evaluate(lookup('mode','none')) end
          522  +	if flags.query.affinity then ret.affinity = evaluate(lookup('affinity',{})) end
          523  +	if flags.query.minpower or flags.query.maxpower or flags.query.power then
          524  +		local condset = function(name,var)
          525  +			if flags.query[name] then ret[name] = var end
          526  +		end
          527  +		local p = lookup('power')
          528  +		if p then
          529  +			if type(p) == 'function' then
          530  +			-- we have a single function to calculate power usage; we need to
          531  +			-- check whether it returns min,max or a constant
          532  +				local min, max = p(pos,timespan)
          533  +				if (not max) or min == max then
          534  +					ret.power = min
          535  +					condset('power',min)
          536  +					condset('minpower',min)
          537  +					condset('maxpower',min)
          538  +				else
          539  +					condset('minpower',min)
          540  +					condset('maxpower',max)
          541  +				end
          542  +			else -- power usage is simply a constant
          543  +				condset('power',p)
          544  +				condset('minpower',p)
          545  +				condset('maxpower',p)
          546  +			end
          547  +		else
          548  +			local feval = function(v)
          549  +				if type(v) == 'function' then
          550  +					return v(pos,timespan)
          551  +				else return v * timespan end
          552  +			end
          553  +			local min = feval(lookup('minpower'))
          554  +			local max = feval(lookup('maxpower'))
          555  +			condset('minpower',min)
          556  +			condset('maxpower',max)
          557  +			if min == max then condset('power',min) end
   301    558   		end
   302    559   	end
   303    560   
   304         -	if (not (p and a and m)) and props and props.ley then
   305         -		p = p or sorcery.ley.field_to_current(props.ley.power,timespan)
   306         -		a = a or props.ley.affinity
   307         -		m = m or props.ley.mode
          561  +	if ret.power then
          562  +		if flags.query.minpower and not ret.minpower then ret.minpower = power end
          563  +		if flags.query.maxpower and not ret.maxpower then ret.maxpower = power end
   308    564   	end
   309         -
   310         -	if (not (p and a and m)) then
   311         -		local compat = sorcery.data.compat.ley[name] 
   312         -		if compat then
   313         -			p = p or sorcery.ley.field_to_current(compat.power,timespan)
   314         -			a = a or compat.affinity
   315         -			m = m or compat.mode
   316         -		end
   317         -	end
   318         -
   319         -	return {
   320         -		power = p or 0;
   321         -		mode = m or 'none';
   322         -		affinity = a or {};
   323         -	}
          565  +	return ret
   324    566   end
   325    567   
   326    568   sorcery.ley.netcaps = function(pos,timespan,exclude)
   327    569   	local net = sorcery.ley.mapnet(pos)
   328    570   	local maxpower = 0
   329    571   	local freepower = 0
   330    572   	local affs,usedaffs = {},{}
          573  +	local flexpowerdevs = {}
          574  +	local devself
   331    575   	for _,n in pairs(net.devices.produce) do
          576  +		if vector.equals(pos,n.pos) then devself = n end
   332    577   		if not exclude or not vector.equals(n.pos,exclude) then
   333    578   			local ln = sorcery.ley.sample(n.pos,timespan,n.id)
          579  +			n.powersupply = ln.power
          580  +			n.affinity = ln.affinity
   334    581   			maxpower = maxpower + ln.power
          582  +			-- production power does not vary, tho at some point it
          583  +			-- might be useful to enable some kind of power scaling
   335    584   			for _,a in pairs(ln.affinity) do
   336    585   				affs[a] = (affs[a] or 0) + 1
   337    586   			end
   338    587   		end
   339    588   	end
   340    589   	freepower = maxpower;
   341    590   	for _,n in pairs(net.devices.consume) do
          591  +		if vector.equals(pos,n.pos) then devself = n end
   342    592   		if not exclude or not vector.equals(n.pos,exclude) then
   343         -			local ln = sorcery.ley.sample(n.pos,timespan,n.id)
   344         -			freepower = freepower - ln.power
          593  +			local ln = sorcery.ley.sample(n.pos,timespan,n.id, {
          594  +				query = { power = true; minpower = true; maxpower = true; affinity = true; };
          595  +			})
          596  +			n.powerdraw = (ln.minpower <= freepower) and ln.minpower or 0
          597  +			freepower = freepower - n.powerdraw
          598  +			-- merge in sample data and return it along with the map
          599  +			n.minpower = ln.minpower
          600  +			n.maxpower = ln.maxpower
          601  +			n.affinity = ln.affinity
          602  +			if ln.maxpower > ln.minpower then
          603  +				flexpowerdevs[#flexpowerdevs+1] = n
          604  +			end
   345    605   			for _,a in pairs(ln.affinity) do
   346    606   				usedaffs[a] = (usedaffs[a] or 0) + 1
   347    607   			end
   348    608   		end
   349    609   	end
          610  +
          611  +	-- now we know the following: all devices; if possible, have been
          612  +	-- given the minimum amount of power they need to run. if freepower
          613  +	-- < 0 then the network is overloaded and inoperable. if freepower>0,
          614  +	-- we now need to distribute the remaining power to devices that
          615  +	-- have a variable power consumption. there's no clean way of doing
          616  +	-- this, so we use the following algorithm:
          617  +	--   1. take a list of devices that want more power
          618  +	--   2. divide the amount of free power by the number of such devices
          619  +	--      to derive the maximum power that can be allocated to any device
          620  +	--   3. iterate through the devices. increase their power consumption by
          621  +	--      the maximum term. any device that is satiated can be removed from
          622  +	--      the list.
          623  +	--   4. if there is still power remaining, repeat until there is not.
          624  +
          625  +	while freepower > 0 and #flexpowerdevs > 0 do
          626  +		local nextiter = {}
          627  +		local maxgive = freepower / #flexpowerdevs
          628  +		for _,d in pairs(flexpowerdevs) do
          629  +			local give = math.min(maxgive,d.maxpower - d.powerdraw)
          630  +			freepower = freepower - give
          631  +			d.powerdraw = d.powerdraw + give
          632  +			if d.powerdraw < d.maxpower then
          633  +				nextiter[#nextiter+1] = d
          634  +			end
          635  +		end
          636  +		flexpowerdevs = nextiter
          637  +	end
   350    638   	
   351    639   	return {
   352    640   		net = net;
   353    641   		freepower = freepower;
   354    642   		maxpower = maxpower;
   355    643   		affinity = affs;
   356    644   		affinity_balance = usedaffs;
          645  +		self = devself;
   357    646   	}
   358    647   end
          648  +
          649  +minetest.register_on_placenode(function(pos, node)
          650  +	if minetest.get_item_group(node.name, 'sorcery_ley_device') ~= 0 then
          651  +		sorcery.ley.notify(pos)
          652  +	end
          653  +end)
          654  +
          655  +local constants = {
          656  +	generator_max_energy_output = 5;
          657  +	-- how much energy a generator makes after
          658  +
          659  +	generator_time_to_max_energy = 150;
          660  +	-- seconds of activity
          661  +	
          662  +	generator_power_drain_speed = 0.1;
          663  +	-- points of energy output drained per second of no fuel
          664  +}
          665  +local update_generator = function(pos)
          666  +	minetest.get_node_timer(pos):start(1)
          667  +end
          668  +local generator_update_formspec = function(pos)
          669  +	local meta = minetest.get_meta(pos)
          670  +	local burnprog = math.min(1,meta:get_float('burnleft') / meta:get_float('burntime'))
          671  +	local power = meta:get_float('power')
          672  +	local inv = meta:get_inventory()
          673  +	local lamps = ''
          674  +	for i=0,4 do 
          675  +		local color
          676  +		if power - i >= 1 then
          677  +			color = 'red'
          678  +		elseif power - i > 0 then
          679  +			color = 'yellow'
          680  +		else
          681  +			color = 'off'
          682  +		end
          683  +		lamps = lamps .. string.format([[
          684  +			image[%f,0.5;1,1;sorcery_statlamp_%s.png]
          685  +		]], 2.5 + i, color)
          686  +	end
          687  +	meta:set_string('formspec', string.format([[
          688  +		size[8,5.8]
          689  +		list[context;fuel;0.5,0.5;1,1]
          690  +		list[current_player;main;0,2;8,4]
          691  +		image[1.5,0.5;1,1;default_furnace_fire_bg.png^[lowpart:%u%%:default_furnace_fire_fg.png]
          692  +	]], math.floor(burnprog * 100)) .. lamps)
          693  +end
          694  +for _,active in pairs{true,false} do
          695  +	local id = 'sorcery:generator' .. (active and '_active' or '')
          696  +	minetest.register_node(id, {
          697  +		description = 'Generator';
          698  +		paramtype2 = 'facedir';
          699  +		groups = { cracky = 2; sorcery_ley_device = 1; };
          700  +		drop = 'sorcery:generator';
          701  +		tiles = {
          702  +			'sorcery_ley_generator_top.png';
          703  +			'sorcery_ley_generator_bottom.png';
          704  +			'sorcery_ley_generator_side.png';
          705  +			'sorcery_ley_generator_side.png';
          706  +			'sorcery_ley_generator_back.png';
          707  +			'sorcery_ley_generator_front_' .. (active and 'on' or 'off') .. '.png';
          708  +		};
          709  +		on_construct = function(pos)
          710  +			local meta = minetest.get_meta(pos)
          711  +			local inv = meta:get_inventory()
          712  +			meta:set_string('infotext','Generator')
          713  +			meta:set_float('burntime',0)
          714  +			meta:set_float('burnleft',0)
          715  +			meta:set_float('power',0)
          716  +			generator_update_formspec(pos)
          717  +			inv:set_size('fuel',1)
          718  +		end;
          719  +		after_dig_node = sorcery.lib.node.purge_container;
          720  +		on_metadata_inventory_put = update_generator;
          721  +		on_metadata_inventory_take = update_generator;
          722  +		on_timer = function(pos,delta)
          723  +			local meta = minetest.get_meta(pos)
          724  +			local inv = meta:get_inventory()
          725  +			local self = minetest.get_node(pos)
          726  +			local timeleft = meta:get_float('burnleft') - delta
          727  +			local again = false
          728  +			local power = meta:get_float('power')
          729  +			local burning = active
          730  +			if timeleft < 0 then timeleft = 0 end
          731  +			if not active or timeleft == 0 then
          732  +				if inv:is_empty('fuel') then
          733  +					-- no fuel, can't start/keep going. drain power if
          734  +					-- necessary, otherwise bail
          735  +					burning = false
          736  +					if power > 0 then
          737  +						power = math.max(0, power - constants.generator_power_drain_speed)
          738  +						again = true
          739  +					end
          740  +				else
          741  +					-- fuel is present, let's burn it
          742  +					local res,decin = minetest.get_craft_result {
          743  +						method = 'fuel';
          744  +						items = {inv:get_stack('fuel',1)};
          745  +					}
          746  +					meta:set_float('burntime',res.time)
          747  +					timeleft = res.time
          748  +					inv:set_stack('fuel',1,decin.items[1])
          749  +					again = true
          750  +					burning = true
          751  +				end
          752  +			else
          753  +				local eps = constants.generator_max_energy_output / constants.generator_time_to_max_energy
          754  +				power = math.min(constants.generator_max_energy_output, power + eps*delta)
          755  +				again = true
          756  +			end
          757  +			::stop:: meta:set_float('power',power)
          758  +			         meta:set_float('burnleft',timeleft)
          759  +					 generator_update_formspec(pos)
          760  +					 if burning and not active then
          761  +						 minetest.swap_node(pos, {
          762  +							 name = 'sorcery:generator_active';
          763  +							 param1 = self.param1, param2 = self.param2;
          764  +						 })
          765  +					 elseif active and not burning then
          766  +						 minetest.swap_node(pos, {
          767  +							 name = 'sorcery:generator';
          768  +							 param1 = self.param1, param2 = self.param2;
          769  +						 })
          770  +					 end
          771  +					 return again
          772  +		end;
          773  +		allow_metadata_inventory_put = function(pos,listname,index,stack,user)
          774  +			local res = minetest.get_craft_result {
          775  +				method = 'fuel';
          776  +				items = {stack};
          777  +			}
          778  +			if res.time ~= 0 then return stack:get_count()
          779  +				else return 0 end
          780  +		end;
          781  +		_sorcery = {
          782  +			ley = {
          783  +				mode = 'produce', affinity = {'praxic'};
          784  +				power = function(pos,delta)
          785  +					local meta = minetest.get_meta(pos)
          786  +					return meta:get_float('power') * delta;
          787  +				end;
          788  +			};
          789  +		};
          790  +	})
          791  +end

Added lib/node.lua version [a0d38cfe10].

            1  +return {
            2  +	purge_container = function(pos,node,meta,user)
            3  +		local offset = function(pos,range)
            4  +			local r = function(min,max)
            5  +				return (math.random() * (max - min)) + min
            6  +			end
            7  +			return {
            8  +				x = pos.x + r(0 - range, range);
            9  +				y = pos.y;
           10  +				z = pos.z + r(0 - range, range);
           11  +			}
           12  +		end
           13  +		for name, inv in pairs(meta.inventory) do
           14  +			for _, item in pairs(inv) do
           15  +				if not item:is_empty() then
           16  +					minetest.add_item(offset(pos,0.4), item)
           17  +				end
           18  +			end
           19  +		end
           20  +	end;
           21  +}

Modified metallurgy-cold.lua from [c21bb1a7ca] to [c1d9a4ce4b].

    34     34   	
    35     35   	metal_grindvalue = 4;
    36     36   	-- how many powders an ingot is worth
    37     37   
    38     38   	default_grindvalue = 1;
    39     39   	-- number of items produced when not otherwise
    40     40   	-- specified
           41  +
           42  +	default_grindcost = 1;
           43  +	-- number of items needed to perform a grind when
           44  +	-- not otherwise specified
    41     45   	
    42     46   	default_hardness = 1;
    43     47   	-- difficulty to grind an item when not otherwise
    44     48   	-- specified
    45     49   
    46     50   	metal_grindcost = 1;
    47     51   	-- number of metal items needed to perform a grind
    48     52   }
    49     53   local mill_formspec_update = function(pos,pct,stat1,stat2)
    50     54   	-- eventually we'll want to display available
    51         -	-- energy here, but for now we're just assuming
    52         -	-- max energy available
           55  +	-- energy here
    53     56   
    54     57   	-- local meta = minetest.get_meta(pos)
    55     58   	-- local inv = meta:get_inventory()
    56     59   	-- local torque = 20
    57     60   	stat1 = stat1 or 'off'
    58     61   	minetest.get_meta(pos):set_string('formspec', string.format([[
    59     62   		size[8,7.2]
................................................................................
    79     82   	if slot == 'grinder' then
    80     83   		if minetest.get_item_group(item:get_name(), 'sorcery_mill_grindhead')~=0 
    81     84   			then return 1 end
    82     85   	elseif slot == 'input' then
    83     86   		local metal = sorcery.data.metallookup[item:get_name()]
    84     87   		local mat = sorcery.matreg.lookup[item:get_name()]
    85     88   		local comp = sorcery.data.compat.grindables[item:get_name()]
    86         -		if metal or mat.metal or comp then
           89  +		if metal or (mat and mat.metal) or comp then
    87     90   			return item:get_count()
    88     91   		else
    89         -			local mat = item:get_definition()._matprop
           92  +			mat = item:get_definition()._matprop
    90     93   			if mat and mat.grindvalue then
    91     94   				return item:get_count() 
    92     95   			end
    93     96   		end
    94     97   	end
    95     98   	return 0
    96     99   end
    97    100   local matprops = function(item)
    98    101   	local metal = sorcery.data.metallookup[item:get_name()]
    99    102   	if not metal then
   100    103   		-- allow grinding of armor and tools back to their
   101    104   		-- original components
   102    105   		local mat = sorcery.matreg.lookup[item:get_name()]
   103         -		if mat.metal then
          106  +		if mat and mat.metal then
   104    107   			metal = mat
   105         -		else
   106         -			return nil
   107    108   		end
   108    109   	end
   109    110   	local mp = item:get_definition()._matprop
   110    111   		or sorcery.data.compat.grindables[item:get_name()]
   111    112   		or {}
   112    113   
   113    114   	again = true
   114    115   	if metal then mp = {
   115         -		hardness = metal.data.hardness or mp.hardness or constants.default_hardness;
   116         -		grindvalue = metal.value or mp.grindvalue or (metal and constants.metal_grindvalue) or constants.default_grindvalue;
   117         -		powder = metal.data.parts.powder or mp.powder;
   118         -		grindcost = constants.metal_grindcost or mp.grindcost; -- invariant for metal
          116  +		hardness = mp.hardness or metal.data.hardness;
          117  +		grindvalue = ((mp.grindvalue or metal.value) or (metal and constants.metal_grindvalue));
          118  +		powder = mp.powder or metal.data.parts.powder;
          119  +		grindcost = mp.grindcost or constants.metal_grindcost; -- invariant for metal
   119    120   	} end
   120    121   
   121         -	mp.torque = constants.grind_torque_factor * mp.hardness
          122  +	mp.torque     = constants.grind_torque_factor * mp.hardness
          123  +	mp.grindvalue = mp.grindvalue or constants.default_grindvalue
          124  +	mp.grindcost  = mp.grindcost  or constants.default_grindcost
          125  +	mp.hardness   = mp.hardness   or constants.default_hardness;
   122    126   
   123    127   	if item:get_wear() ~= 0 then
   124    128   		-- prevent cheating by recovering metal from items before they
   125    129   		-- are destroyed
   126    130   		local wearfac = (item:get_wear() / 65535)
   127    131   		mp.grindvalue = math.max(1,math.ceil(mp.grindvalue * wearfac))
   128    132   		mp.hardness = math.max(1,math.ceil(mp.grindcost * wearfac))
................................................................................
   134    138   minetest.register_node('sorcery:mill',{
   135    139   	description = 'Mill';
   136    140   	groups = {
   137    141   		cracky = 2;
   138    142   		sorcery_ley_device = 1;
   139    143   	};
   140    144   	paramtype2 = 'facedir';
          145  +	after_dig_node = sorcery.lib.node.purge_container;
   141    146   	on_construct = function(pos)
   142    147   		local meta = minetest.get_meta(pos)
   143    148   		local inv = meta:get_inventory()
   144    149   		inv:set_size('input',1)
   145    150   		inv:set_size('output',4)
   146    151   		inv:set_size('grinder',2)
   147    152   		meta:set_float('grindtime',0)
................................................................................
   163    168   	on_metadata_inventory_move = function(pos,fl,fi,tl,ti)
   164    169   		if fl == 'input' or tl == 'input' then
   165    170   			minetest.get_meta(pos):set_float('grindtime',0)
   166    171   		end
   167    172   		mill_update(pos)
   168    173   	end;
   169    174   	_sorcery = {
   170         -		ley = { mode = 'consume'; affinity = {'praxic'}};
   171         -		on_leyconnect = mill_update;
   172         -		on_leycalc = function(pos,time)
   173         -			local meta = minetest.get_meta(pos)
   174         -			local active = meta:get_int('active') == 1
   175         -			if not active then return { power = 0; } end
   176         -			local inv = meta:get_inventory()
   177         -			local item = inv:get_stack('input',1)
   178         -			if item:is_empty() then
   179         -				meta:set_int('active', 0)
   180         -				return { power = 0; }
   181         -			end
   182         -			return {
   183         -				power = matprops(item).torque * time;
   184         -			}
   185         -		end;
          175  +		ley = {
          176  +			mode = 'consume', affinity = {'praxic'};
          177  +			power = function(pos,time)
          178  +				local meta = minetest.get_meta(pos)
          179  +				local active = meta:get_int('active') == 1
          180  +				if not active then return 0 end
          181  +				local inv = meta:get_inventory()
          182  +				local item = inv:get_stack('input',1)
          183  +				if item:is_empty() then
          184  +					meta:set_int('active', 0)
          185  +					return 0
          186  +				end
          187  +				return matprops(item).torque * time;
          188  +			end;
          189  +		};
          190  +		on_leychange = mill_update;
   186    191   	};
   187    192   	on_timer = function(pos,delta)
   188    193   		local meta = minetest.get_meta(pos)
   189    194   		local inv = meta:get_inventory()
   190    195   		local elapsed = meta:get_float('grindtime') + delta
   191    196   		local ley = sorcery.ley.netcaps(pos,delta,pos)
   192    197   		local again = false
   193    198   		local active = false
   194    199   		local reqtime -- sigh
   195    200   		local statcolor = 'off'
   196    201   		local grinders_on
   197    202   		if inv:is_empty('input') or inv:is_empty('grinder') then
   198         -			print('empty')
   199    203   			elapsed = 0
   200    204   			mill_formspec_update(pos, 0)
   201    205   		else
   202    206   			local item = inv:get_stack('input',1)
   203    207   			local mp = matprops(item)
   204    208   			if mp.grindcost > item:get_count() then
   205    209   				elapsed = 0
   206    210   				mill_formspec_update(pos, 0)
   207         -				print('bad grindcost')
   208    211   				goto stop
   209    212   			end
   210    213   
   211         -			-- print('power supply',ley.maxpower)
   212         -			-- print('power available',ley.freepower)
   213         -			-- print('power needed',mp.torque*delta)
   214    214   			if ley.maxpower < (mp.torque * delta) then
   215    215   				-- not enough potential energy in the system to grind
   216    216   				-- so don't bother
   217         -				print('not enough power')
   218    217   				statcolor = 'red'
   219    218   				elapsed = 0 goto stop
   220    219   			elseif ley.freepower < (mp.torque * delta) then
   221    220   				-- the net has enough potential energy to supply us,
   222    221   				-- but too much of it is in use right now. give up
   223    222   				-- on this round, but try again in a bit to see if
   224    223   				-- more energy is available
   225         -				print('currently not enough power')
   226    224   				statcolor = 'yellow'
   227    225   				elapsed = 0 again = true goto stop
   228    226   			end
   229    227   
   230    228   			local grinders = 0
   231    229   			local grindpower = 0
   232    230   			local grind_wear = {}
................................................................................
   242    240   					if dif == 0 then
   243    241   						wear = constants.grind_wear
   244    242   					elseif dif < 0 then
   245    243   						wear = constants.grind_wear * ((dif * -1)/constants.grind_range)
   246    244   					elseif dif > 0 then
   247    245   						if dif > constants.grind_grace_range then
   248    246   							wear = 0
   249         -							print('grinder reject')
   250    247   							goto reject
   251    248   						else
   252    249   							wear = constants.grind_wear * (1 + (dif/constants.grind_range)) * constants.grind_grace_penalty
   253    250   						end
   254    251   					end
   255    252   					::accept:: grinders = grinders + 1
   256    253   					           grindpower = grindpower + hh
................................................................................
   269    266   				if grindpower < mp.hardness then
   270    267   					statcolor = 'yellow'
   271    268   				else statcolor='green' end
   272    269   				grindpower = grindpower / grinders
   273    270   				-- if there is more power available than needed,
   274    271   				-- and/or if the blades are stronger than needed,
   275    272   				-- speed up the grind
   276         -				local speedboost = math.max(0.05,((grindpower - mp.hardness)/constants.grind_range) * grinders) * ((mp.torque * delta) / ley.freepower)
          273  +				local speedboost = math.max(0.05,((grindpower - mp.hardness)/constants.grind_range) * grinders)
   277    274   				reqtime = mp.grindvalue * mp.hardness * constants.grind_factor * (1-speedboost)
   278    275   				if elapsed >= reqtime then
   279    276   					item:take_item(mp.grindcost)
   280    277   					inv:set_stack('input',1,item)
   281    278   					local pw = ItemStack{
   282    279   						name=mp.powder;
   283    280   						count=mp.grindvalue;
................................................................................
   345    342   			metal = name;
   346    343   		};
   347    344   	});
   348    345   	minetest.register_craft {
   349    346   		output = id;
   350    347   		recipe = {
   351    348   			{f,i,f};
   352         -			{i,i,i};
          349  +			{i,'',i};
   353    350   			{f,i,f};
   354    351   		};
   355    352   	}
   356    353   end

Modified metallurgy-hot.lua from [3c9b60f14d] to [45fd80ebe6].

   294    294   	for _,state in pairs{'open','closed'} do
   295    295   		local id_closed = id .. '_closed'
   296    296   		local id_current = (state == 'closed' and id_closed) or id
   297    297   		local desc = (kind.temp_name and sorcery.lib.str.capitalize(kind.temp_name) .. ' kiln') or 'Kiln'
   298    298   		minetest.register_node(id_current, {
   299    299   			description = desc;
   300    300   			drawtype = "mesh";
          301  +			after_dig_node = sorcery.lib.node.purge_container;
   301    302   			mesh = 'sorcery-kiln-' .. state .. '.obj';
   302    303   			drop = id;
   303    304   			groups = {
   304    305   				cracky = (state == 'open' and 2) or nil;
   305    306   			};
   306    307   			sunlight_propagates = true;
   307    308   			paramtype1 = 'light';
................................................................................
   376    377   	local id = 'sorcery:smelter_' .. kind.material .. kind.size_name
   377    378   	kind.id = id
   378    379   	for _, active in pairs {false, true} do
   379    380   		minetest.register_node((active and id .. '_active') or id, {
   380    381   			_proto = kind;
   381    382   			description = desc;
   382    383   			drop = id;
          384  +			after_dig_node = sorcery.lib.node.purge_container;
   383    385   			groups = {
   384    386   				cracky = (active and 2) or nil;
   385    387   			};
   386    388   			paramtype2 = 'facedir';
   387    389   			light_source = (active and 9) or 0;
   388    390   			on_construct = function(pos)
   389    391   				local meta = minetest.get_meta(pos)

Modified ores.lua from [48691b218f] to [caa4bf5370].

   125    125   			no_armor = metal.no_armor;
   126    126   			no_tools = metal.no_tools;
   127    127   			durability = metal.durability;
   128    128   			power = metal.power;
   129    129   			speed = metal.speed;
   130    130   			artificial = metal.artificial;
   131    131   			cooktime = metal.cooktime;
   132         -			hardness = metal.hardness;
          132  +			hardness = (metal.hardness/8) * 3; -- scaled wrt diamond
          133  +			level = math.ceil(((metal.hardness/8) * 3)) + 1;
   133    134   			ingot_image = (metal.image and metal.image.ingot) or nil;
   134    135   			lump_image = (metal.image and metal.image.lump) or nil;
   135    136   			armor_weight = metal.armor_weight;
   136    137   			armor_protection = metal.armor_protection;
   137    138   		}
   138    139   	end
   139    140   	minetest.register_craftitem(fragment, {

Modified recipes.lua from [0b99f84f61] to [c451769631].

   250    250   	};
   251    251   	output = "sorcery:infuser";
   252    252   }
   253    253   
   254    254   
   255    255   ---- altar
   256    256   -- candles
          257  +minetest.register_craftitem('sorcery:core_counterpraxic',{
          258  +	description = 'Counterpraxis Core';
          259  +	inventory_image = 'sorcery_core_counterpraxic.png';
          260  +});
          261  +--
          262  +minetest.register_craftitem('sorcery:suppression_matrix',{
          263  +	description = 'Suppression Matrix';
          264  +	inventory_image = 'sorcery_suppression_matrix.png';
          265  +});
          266  +
          267  +minetest.register_craftitem('sorcery:inverter_coil',{
          268  +	description = 'Inverter Coil';
          269  +	inventory_image = 'sorcery_inverter_coil.png';
          270  +});
          271  +
          272  +minetest.register_craftitem('sorcery:beam_generator',{
          273  +	description = 'Beam Generator';
          274  +	inventory_image = 'sorcery_beam_generator.png';
          275  +});
          276  +
          277  +minetest.register_craftitem('sorcery:leyline_stabilizer',{
          278  +	description = 'Leyline Stabilizer';
          279  +	inventory_image = 'sorcery_leyline_stabilizer.png';
          280  +});
          281  +
          282  +minetest.register_craftitem('sorcery:field_emitter',{
          283  +	description = 'Field Emitter';
          284  +	inventory_image = 'sorcery_field_emitter.png';
          285  +})
          286  +
          287  +minetest.register_craft {
          288  +	output = 'sorcery:leyline_stabilizer';
          289  +	recipe = {
          290  +		{'basic_materials:copper_wire','group:sorcery_ley_cable','basic_materials:copper_wire'};
          291  +		{'sorcery:grease_neutralizing','group:sorcery_ley_cable','sorcery:grease_neutralizing'};
          292  +		{'basic_materials:copper_wire','group:sorcery_ley_cable','basic_materials:copper_wire'};
          293  +	};
          294  +	replacements = {
          295  +		{'basic_materials:copper_wire', 'basic_materials:empty_spool'};
          296  +		{'basic_materials:copper_wire', 'basic_materials:empty_spool'};
          297  +		{'basic_materials:copper_wire', 'basic_materials:empty_spool'};
          298  +		{'basic_materials:copper_wire', 'basic_materials:empty_spool'};
          299  +		{'sorcery:grease_neutralizing', 'xdecor:bowl'};
          300  +		{'sorcery:grease_neutralizing', 'xdecor:bowl'};
          301  +	};
          302  +}
          303  +
          304  +minetest.register_craft {
          305  +	output = 'sorcery:beam_generator';
          306  +	recipe = {
          307  +		{'xpanes:bar_flat','xpanes:pane_flat','xpanes:bar_flat'};
          308  +		{'sorcery:screw_aluminum','sorcery:gem_sapphire','sorcery:screw_aluminum'};
          309  +		{'xpanes:bar_flat','sorcery:gem_luxite','xpanes:bar_flat'};
          310  +	};
          311  +}
          312  +
          313  +minetest.register_craft {
          314  +	output = 'sorcery:suppression_matrix';
          315  +	recipe = {
          316  +		{'sorcery:fragment_electrum','basic_materials:steel_strip','sorcery:fragment_cobalt'};
          317  +		{'basic_materials:copper_strip','xpanes:bar_flat','basic_materials:copper_strip'};
          318  +		{'sorcery:fragment_cobalt','basic_materials:steel_strip','sorcery:fragment_electrum'};
          319  +	};
          320  +}
          321  +
          322  +minetest.register_craft {
          323  +	output = 'sorcery:core_counterpraxic';
          324  +	recipe = {
          325  +		{'sorcery:gem_ruby_shard','sorcery:tungsten_ingot','sorcery:gem_ruby_shard'};
          326  +		{'sorcery:tungsten_ingot','sorcery:gem_emerald','sorcery:tungsten_ingot'};
          327  +		{'sorcery:gem_ruby_shard','sorcery:tungsten_ingot','sorcery:gem_ruby_shard'};
          328  +	};
          329  +}
          330  +
          331  +minetest.register_craft {
          332  +	output = 'sorcery:inverter_coil';
          333  +	recipe = {
          334  +		{'sorcery:screw_platinum','basic_materials:steel_wire','sorcery:screw_platinum'};
          335  +		{'basic_materials:copper_wire','default:tin_ingot','basic_materials:copper_wire'};
          336  +		{'sorcery:screw_platinum','basic_materials:steel_wire','sorcery:screw_platinum'};
          337  +	};
          338  +	replacements = {
          339  +		{'basic_materials:steel_wire', 'basic_materials:empty_spool'};
          340  +		{'basic_materials:copper_wire', 'basic_materials:empty_spool'};
          341  +		{'basic_materials:copper_wire', 'basic_materials:empty_spool'};
          342  +		{'basic_materials:steel_wire', 'basic_materials:empty_spool'};
          343  +	};
          344  +}
          345  +
          346  +minetest.register_craft {
          347  +	output = 'sorcery:field_emitter';
          348  +	recipe = {
          349  +		{'basic_materials:steel_bar','xpanes:pane_flat','basic_materials:steel_bar'};
          350  +		{'sorcery:beam_generator','sorcery:leyline_stabilizer','sorcery:beam_generator'};
          351  +		{'sorcery:beam_generator','basic_materials:copper_wire','sorcery:beam_generator'};
          352  +	};
          353  +	replacements = {
          354  +		{'basic_materials:copper_wire', 'basic_materials:empty_spool'};
          355  +	};
          356  +}
          357  +
          358  +minetest.register_craft {
          359  +	output = 'sorcery:emitter_barrier';
          360  +	recipe = {
          361  +		{'sorcery:suppression_matrix','sorcery:suppression_matrix','sorcery:suppression_matrix'};
          362  +		{'sorcery:field_emitter','sorcery:core_counterpraxic','sorcery:inverter_coil'};
          363  +		{'default:tin_ingot','default:tin_ingot','default:tin_ingot'};
          364  +	};
          365  +}
          366  +
          367  +minetest.register_craft {
          368  +	output = 'sorcery:generator';
          369  +	recipe = {
          370  +		{'sorcery:aluminum_ingot','sorcery:aluminum_ingot','sorcery:aluminum_ingot'};
          371  +		{'default:bronze_ingot','basic_materials:copper_wire','default:bronze_ingot'};
          372  +		{'default:steel_ingot','default:furnace','default:steel_ingot'};
          373  +	};
          374  +	replacements = {
          375  +		{'basic_materials:copper_wire', 'basic_materials:empty_spool'};
          376  +	};
          377  +}
          378  +
   257    379   minetest.register_craftitem('sorcery:candle', {
   258    380   	-- TODO make candle node
   259    381   	inventory_image = 'sorcery_candle.png';
   260    382   	description = 'Votive Candle';
   261    383   	groups = {
   262    384   		candle = 1;
   263    385   	};

Added textures/sorcery_beam_generator.png version [58c2e4401c].

cannot compute difference between binary files

Added textures/sorcery_core_counterpraxic.png version [39479c1b28].

cannot compute difference between binary files

Added textures/sorcery_emitter_barrier_bottom.png version [a6363e789d].

cannot compute difference between binary files

Added textures/sorcery_emitter_barrier_front.png version [76e1531fe0].

cannot compute difference between binary files

Added textures/sorcery_emitter_barrier_side.png version [b566c1148a].

cannot compute difference between binary files

Added textures/sorcery_emitter_barrier_top.png version [2705ace703].

cannot compute difference between binary files

Added textures/sorcery_field_emitter.png version [fdceff2168].

cannot compute difference between binary files

Added textures/sorcery_inverter_coil.png version [a40914f554].

cannot compute difference between binary files

Added textures/sorcery_ley_generator_back.png version [aedb4f096c].

cannot compute difference between binary files

Added textures/sorcery_ley_generator_bottom.png version [f23afab857].

cannot compute difference between binary files

Added textures/sorcery_ley_generator_front_off.png version [811455addd].

cannot compute difference between binary files

Added textures/sorcery_ley_generator_front_on.png version [cf5b54dd34].

cannot compute difference between binary files

Added textures/sorcery_ley_generator_side.png version [07ad2d2926].

cannot compute difference between binary files

Added textures/sorcery_ley_generator_top.png version [7147d2203f].

cannot compute difference between binary files

Added textures/sorcery_ley_plug.png version [ac3ad24938].

cannot compute difference between binary files

Added textures/sorcery_ley_switch_base.png version [eca6656993].

cannot compute difference between binary files

Added textures/sorcery_ley_switch_down.png version [5722e6a77c].

cannot compute difference between binary files

Added textures/sorcery_ley_switch_panel.png version [3f4a278966].

cannot compute difference between binary files

Added textures/sorcery_ley_switch_up.png version [3e480e03fd].

cannot compute difference between binary files

Added textures/sorcery_leyline_stabilizer.png version [148e93c8c1].

cannot compute difference between binary files

Added textures/sorcery_suppression_matrix.png version [9c64f4969e].

cannot compute difference between binary files

Modified tnodes.lua from [74f7716464] to [27d6ea2995].

    15     15   			minetest.get_node_timer(pos):start(1)
    16     16   		end;
    17     17   		on_timer = function(pos,dtime)
    18     18   			local meta = minetest.get_meta(pos)
    19     19   			local elapsed = dtime + meta:get_float('duration') - meta:get_float('timeleft')
    20     20   			local level = 1 - (elapsed / meta:get_float('duration'))
    21     21   			local lum = math.ceil(level*meta:get_int('power'))
    22         -			print('elapsed time',elapsed)
    23         -			print('light level',level)
    24         -			print('lum',lum)
    25     22   			if lum ~= i then
    26     23   				if lum <= 0 then
    27     24   					minetest.remove_node(pos)
    28     25   					return false
    29     26   				else
    30     27   					minetest.swap_node(pos,{name='sorcery:air_glimmer_'..tostring(lum)})
    31     28   				end

Modified wands.lua from [8ffba5fe57] to [12fce408ba].

   687    687   			-- meta:set_string('formspec',wandwork_form(false))
   688    688   		end;
   689    689   		on_timer = function(...)
   690    690   			if water then return wandwork_soak(...);
   691    691   			         else return false end
   692    692   		end;
   693    693   		on_rightclick = wandwork_rightclick;
          694  +		after_dig_node = sorcery.lib.node.purge_container;
   694    695   		allow_metadata_inventory_put = function(pos, list, index, stack, user)
   695    696   			local meta = minetest.get_meta(pos)
   696    697   			local wwi = meta:get_inventory()
   697    698   			if list == 'preview' then return 0
   698    699   			elseif list == 'wandparts' then
   699    700   				if (not wwi:is_empty('preview')) and
   700    701   					find_core(stack) then return 1