sorcery  Check-in [83426a2748]

Overview
Comment:balance amulets better, add sound effects, add debugging privilege for runes, swat various glitches and bugs
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 83426a2748e5b3d65016273423dd93acce6a53af54a1f71a97a5625256f9dbd2
User & Date: lexi on 2020-10-22 15:51:39
Other Links: manifest | tags
Context
2020-10-23
00:08
fix some showstopping bugs, more amulet spells, add sound effects, improve teleportation visuals check-in: 90e64c483c user: lexi tags: trunk
2020-10-22
15:51
balance amulets better, add sound effects, add debugging privilege for runes, swat various glitches and bugs check-in: 83426a2748 user: lexi tags: trunk
2020-10-21
03:35
add rune forges, runes, amulet frames, write sacrifice spell, touch up amulet graphics, enable enchantment of amulets (though spells cannot yet be cast), defuckulate syncresis core icon, unfuckitize sneaky leycalc bug that's probably been the cause of some long-standing wackiness, add item classes, add some more textures, disbungle various other asstastrophes, remove sneaky old debug code, improve library code, add utility for uploading merge requests check-in: 96c5289a2a user: lexi tags: trunk
Changes

Modified coins.lua from [7e4d0ae5aa] to [d6a069e16f].

   179    179   			local inv = meta:get_inventory()
   180    180   			local reduce_slot = function(slot)
   181    181   				local i = inv:get_stack(slot,1)
   182    182   				i:take_item(items_used) inv:set_stack(slot,1,i)
   183    183   			end
   184    184   			reduce_slot('ingot')
   185    185   			if not inv:is_empty('gem') then reduce_slot('gem') end
          186  +			minetest.sound_play('sorcery_coins', { pos = pos, gain = 0.7 })
   186    187   		end
   187    188   		update_press_output(meta)
   188    189   	end;
   189    190   }) end
   190    191   
   191    192   minetest.register_craft {
   192    193   	output = 'sorcery:coin_press';
   193    194   	recipe = {
   194    195   		{'group:wood','group:wood','group:wood'};
   195    196   		{'basic_materials:steel_bar','default:steel_ingot','basic_materials:steel_bar'};
   196    197   		{'default:copper_ingot','default:stone','default:copper_ingot'};
   197    198   	};
   198    199   }

Modified data/metals.lua from [127702c66e] to [3560b06dd9].

   218    218   		slots = {
   219    219   			{
   220    220   				affinity = {'counterpraxic'};
   221    221   				confluence = 0.65;
   222    222   				interference = {speed = 1};
   223    223   			};
   224    224   		};
   225         -		amulet = {};
          225  +		amulet = { power = 1.5 };
   226    226   	};
   227    227   	lithium = {
   228    228   		tone = {255,252,93}, alpha = 80;
   229    229   		dye = 'yellow';
   230    230   		rarity = 13;
   231    231   		hardness = 2;
   232    232   		fuel = 80;
................................................................................
   278    278   		image = {
   279    279   			block = 'sorcery_metal_iridium_shiny.png';
   280    280   		};
   281    281   		slots = {
   282    282   			{affinity={'counterpraxic','syncretic'}, confluence = 1.1};
   283    283   			{affinity={'cognic','entropic'}, confluence = 0.8};
   284    284   		};
   285         -		amulet = {};
          285  +		amulet = { power = 1.7 };
   286    286   	};
   287    287   	duridium = {
   288    288   		tone = {255,64,175}, alpha = 70;
   289    289   		cooktime = 120;
   290    290   		artificial = true;
   291    291   		durability = 3400;
   292    292   		speed = 3.1;

Modified data/oils.lua from [f8f2204848] to [fb12becfb1].

    95     95   			'sorcery:extract_raspberry';
    96     96   			'sorcery:extract_raspberry';
    97     97   			'sorcery:extract_onion';
    98     98   			'farming:peas';
    99     99   			'farming:peas';
   100    100   			'farming:peas';
   101    101   		};
          102  +	};
          103  +	luscious = {
          104  +		color = {10,255,10};
          105  +		mix = {
          106  +			'sorcery:extract_marram';
          107  +			'sorcery:extract_grape';
          108  +			'farming:cocoa_beans';
          109  +			'farming:sugar';
          110  +			'farming:sugar';
          111  +		};
   102    112   	};
   103    113   }

Modified data/potions.lua from [eb3ff7cdbe] to [018fbedf8a].

    31     31   		color = {119,51,111};
    32     32   		infusion = 'sorcery:oil_bleak';
    33     33   	};
    34     34   	Isolating = {
    35     35   		color = {188,78,225};
    36     36   		infusion = 'sorcery:extract_fern';
    37     37   	};
           38  +	Subtle = {
           39  +		color = {230,253,150}, glow = 6;
           40  +		infusion = 'sorcery:oil_luscious';
           41  +	};
    38     42   }

Modified data/runes.lua from [bd35ca52e8] to [720c3d8102].

     9      9   		name = 'Translocate';
    10     10   		tone = {0,235,233};
    11     11   		minpower = 3;
    12     12   		rarity = 15;
    13     13   		amulets = {
    14     14   			amethyst = {
    15     15   				name = 'Joining';
    16         -				desc = 'Give this amulet to another and they can arrive at your side in a flash from anywhere in the world — though returning whence they came may be a more difficult matter';
           16  +				desc = 'Give this amulet to another and they can arrive safely at your side in a flash from anywhere in the world — though returning whence they came may be a more difficult matter';
           17  +				apply = function(ctx)
           18  +					local maker = ctx.user:get_player_name()
           19  +					ctx.meta:set_string('rune_join_target',maker)
           20  +				end;
           21  +				remove = function(ctx) ctx.meta:set_string('rune_join_target','') end;
    17     22   				frame = {
    18     23   					gold = {
    19     24   						name = 'Exchange';
    20     25   						desc = 'Give this amulet to another and they will be able to trade places with you no matter where in the world each of you might be.'; 
    21     26   					};
    22     27   					cobalt = {
    23     28   						name = 'Sending';
................................................................................
    27     32   						name = 'Arrival';
    28     33   						desc = "Give this amulet to another and they will be able to arrive at your side in a flash from anywhere in the world, carrying others with them in the spell's grip";
    29     34   					};
    30     35   				};
    31     36   			};
    32     37   			sapphire = {
    33     38   				name = 'Return';
    34         -				desc = 'Use this amulet once to bind it to a particular point in the world, then use it again to return instantly to that point.';
           39  +				desc = 'Use this amulet once to bind it to a particular point in the world, then discharge its spell to return instantly to that point.';
           40  +				remove = function(ctx)
           41  +					ctx.meta:set_string('rune_return_dest','')
           42  +				end;
           43  +				cast = function(ctx)
           44  +					if not ctx.meta:contains('rune_return_dest') then
           45  +						local pos = ctx.caster:get_pos()
           46  +						ctx.meta:set_string('rune_return_dest',minetest.pos_to_string(pos))
           47  +						return true -- play effects but do not break spell
           48  +					else
           49  +						local pos = minetest.string_to_pos(ctx.meta:get_string('rune_return_dest'))
           50  +						ctx.meta:set_string('rune_return_dest','')
           51  +						local subjects = { ctx.caster }
           52  +						local center = ctx.caster:get_pos()
           53  +						ctx.sparkle = false
           54  +						for _,s in pairs(subjects) do
           55  +							local offset = vector.subtract(s:get_pos(), center)
           56  +							local pt = sorcery.lib.node.get_arrival_point(vector.add(pos,offset))
           57  +							if pt then
           58  +								sorcery.vfx.body_sparkle(s,sorcery.lib.color(20,120,255),2)
           59  +								sorcery.vfx.body_sparkle(nil,sorcery.lib.color(20,255,120),2,pt)
           60  +								s:set_pos(pt)
           61  +							end
           62  +						end
           63  +					end
           64  +				end;
    35     65   				frame = {
    36     66   					iridium = {
    37     67   						name = 'Mass Return';
    38     68   						desc = 'Use this amulet once to bind it to a particular point in the world, then carry yourself and everyone around you back to that point in a flash simply by using it again';
    39     69   					};
    40     70   				};
    41     71   			};
................................................................................
   114    144   			ruby = {
   115    145   				name = 'Shattering';
   116    146   				desc = 'Tear a violent wound in the earth with the destructive force of this amulet';
   117    147   			};
   118    148   			diamond = {
   119    149   				name = 'Killing';
   120    150   				desc = 'Wield this amulet against a foe to instantly snuff the life out of their mortal form, regardless of their physical protections.';
          151  +				cast = function(ctx)
          152  +					if not (ctx.target and ctx.target.type == 'object') then return false end
          153  +					local tgt = ctx.target.ref
          154  +					if not minetest.is_player(obj) then return false end
          155  +					local tgth = tgt:get_properties().eye_height
          156  +					sorcery.vfx.bloodburst(vector.add(tgt:get_pos(),{x=0,y=tgth/2,z=0}),20)
          157  +					minetest.sound_play('sorcery_bloody_burst', { pos = pos, gain = 1.5 })
          158  +					tgt:set_hp(0)
          159  +				end;
   121    160   				frame = {
   122    161   					iridium = {
   123    162   						name = 'Massacre';
   124    163   						desc = "Unleash the dark and wicked force that lurks within this fell amulet to instantaneously slay all those who surround you, friend and foe alike";
   125    164   					};
   126    165   				};
   127    166   			};
................................................................................
   179    218   	};
   180    219   	dominate = {
   181    220   		name = 'Dominate';
   182    221   		tone = {235,0,228};
   183    222   		minpower = 4;
   184    223   		rarity = 40;
   185    224   		amulets = {
          225  +			amethyst = {
          226  +				name = 'Suffocation';
          227  +				desc = 'Wrap this spell tightly around your victim\'s throat, cutting off their oxygen until you release them.';
          228  +			};
   186    229   			ruby = {
   187    230   				name = 'Exsanguination';
   188    231   				desc = 'Rip the life force out of another, leaving them on the brink of death, and use it to mend your own wounds and invigorate your own being';
          232  +				cast = function(ctx)
          233  +					if not (ctx.target and ctx.target.type == 'object') then return false end
          234  +					local tgt = ctx.target.ref
          235  +					local takefac = math.min(99,50 + (ctx.stats.power * 5)) / 100
          236  +					local dmg = tgt:get_hp() * takefac
          237  +					print("!!! dmg calc",takefac,dmg,tgt:get_hp())
          238  +
          239  +					local numhits = math.random(6,10+ctx.stats.power/2)
          240  +					local function dohit(hitsleft)
          241  +						if tgt == nil or tgt:get_properties() == nil then return end
          242  +						tgt:punch(ctx.caster, 1, {
          243  +							full_punch_interval = 1;
          244  +							damage_groups = { fleshy = dmg / numhits }
          245  +						})
          246  +						local tgth = tgt:get_properties().eye_height
          247  +						sorcery.vfx.bloodburst(vector.add(tgt:get_pos(),{x=0,y=tgth/2,z=0}),math.random(10 * takefac, 40 * takefac))
          248  +						ctx.caster:set_hp(ctx.caster:get_hp() + math.max(1,(dmg/numhits)*takefac))
          249  +
          250  +						local sound = {'sorcery_bloody_hit','sorcery_crunch',false}
          251  +						sound = sound[math.random(#sound)]
          252  +						if sound ~= false then
          253  +							minetest.sound_play(sound, { pos = pos, gain = math.random(5,15)*0.1 })
          254  +						end
          255  +
          256  +						local nexthit = math.random() * 0.4 + 0.1
          257  +						local dir = vector.subtract(ctx.caster:get_pos(), tgt:get_pos())
          258  +						local spark = sorcery.lib.image('sorcery_spark.png')
          259  +						minetest.add_particlespawner {
          260  +							amount = math.random(80*takefac,150*takefac);
          261  +							texture = spark:blit(spark:multiply(sorcery.lib.color(255,20,10))):render();
          262  +							time = nexthit;
          263  +							attached = tgt;
          264  +							minpos = {x = -0.3, y = -0.5, z = -0.3};
          265  +							maxpos = {x =  0.3, y = tgth, z = 0.3};
          266  +							minvel = vector.multiply(dir,0.5);
          267  +							maxvel = vector.multiply(dir,0.9);
          268  +							minacc = vector.multiply(dir,0.1);
          269  +							maxacc = vector.multiply(dir,0.2);
          270  +							minexptime = nexthit * 1.5;
          271  +							maxexptime = nexthit * 2;
          272  +							minsize = 0.5;
          273  +							maxsize = 5 * takefac;
          274  +							glow = 14;
          275  +							animation = {
          276  +								type = 'vertical_frames';
          277  +								aspect_w = 16, aspect_h = 16;
          278  +								length = nexthit*2 + 0.1;
          279  +							};
          280  +						}
          281  +
          282  +						if hitsleft > 0 then
          283  +							minetest.after(nexthit, function() dohit(hitsleft-1) end)
          284  +						end
          285  +					end
          286  +					dohit(numhits)
          287  +				end;
   189    288   			};
   190    289   			amethyst = {
   191    290   				name = 'Disarming';
   192    291   				desc = 'Wield this amulet against a foe to rip all the weapons in their possession out of their grasp';
   193    292   				frame = {
   194    293   					iridium = {
   195    294   						name = 'Peacemaking';

Modified enchanter.lua from [bac74a37c3] to [b41a4d81d6].

   311    311   		buildable_to = true;
   312    312   		sunlight_propagates = true;
   313    313   		light_source = i + 4;
   314    314   		groups = {
   315    315   			air = 1, sorcery_air = 1;
   316    316   			not_in_creative_inventory = 1;
   317    317   		};
          318  +		drop = {max_items = 0, items = {}};
          319  +		on_blast = function() end; -- not affected by explosions
   318    320   		on_construct = function(pos)
   319    321   			minetest.get_node_timer(pos):start(0.05)
   320    322   		end;
   321    323   		on_timer = function(pos)
   322    324   			if i <= 2 then minetest.remove_node(pos) else
   323    325   				minetest.set_node(pos, {name='sorcery:air_flash_1'})
   324    326   				return true

Modified forcefield.lua from [72bf5182e0] to [a330742505].

    45     45   	minetest.register_node('sorcery:air_barrier_' .. tostring(i), {
    46     46   		drawtype = 'glasslike';
    47     47   		walkable = true;
    48     48   		pointable = false;
    49     49   		sunlight_propagates = true;
    50     50   		paramtype = 'light';
    51     51   		light_source = i;
           52  +		drop = {max_items = 0, items = {}};
           53  +		on_blast = function() end; -- not affected by explosions
    52     54   		tiles = {'sorcery_transparent.png'};
    53     55   		groups = {
    54     56   			air = 1;
    55     57   			sorcery_air = 1;
    56     58   			sorcery_force_barrier = i;
    57     59   		};
    58     60   		-- _proto = {

Modified gems.lua from [e866e2741b] to [f5bc90eb64].

    52     52   			};
    53     53   		})
    54     54   	end
    55     55   	if not gem.foreign_amulet then
    56     56   		local img = sorcery.lib.image
    57     57   		local img_stone = img('sorcery_amulet.png'):multiply(sorcery.lib.color(gem.tone))
    58     58   		local img_sparkle = img('sorcery_amulet_sparkle.png')
           59  +		local useamulet = function(stack,user,target)
           60  +			local sp = sorcery.amulet.getspell(stack)
           61  +			if not sp or not sp.cast then return nil end
           62  +			local stats = sorcery.amulet.stats(stack)
           63  +
           64  +			local ctx = {
           65  +				caster = user;
           66  +				target = target;
           67  +				stats = stats;
           68  +				sound = "xdecor_enchanting"; --FIXME make own sounds
           69  +				sparkle = true;
           70  +				amulet = stack;
           71  +				meta = stack:get_meta(); -- avoid spell boilerplate
           72  +				color = sorcery.lib.color(sp.tone);
           73  +			}
           74  +			print('casting')
           75  +			local res = sp.cast(ctx)
           76  +
           77  +			if res == nil or res == true then
           78  +				minetest.sound_play(ctx.sound, { 
           79  +					pos = user:get_pos();
           80  +					gain = 1;
           81  +				})
           82  +			end
           83  +			if ctx.sparkle then
           84  +				sorcery.vfx.cast_sparkle(user, ctx.color, stats.power,0.5)
           85  +			end
           86  +			if res == nil then
           87  +				if not minetest.check_player_privs(user, 'sorcery:infinirune') then
           88  +					sorcery.amulet.setrune(stack)
           89  +				end
           90  +			end
           91  +
           92  +			return ctx.amulet
           93  +		end;
    59     94   		minetest.register_craftitem(amuletname, {
    60     95   			description = sorcery.lib.str.capitalize(name) .. ' amulet';
    61     96   			inventory_image = img_sparkle:blit(img_stone):render();
    62     97   			wield_scale = { x = 0.6, y = 0.6, z = 0.6 };
    63     98   			groups = { sorcery_amulet = 1 };
           99  +			on_use = useamulet;
    64    100   			_sorcery = {
    65    101   				material = {
    66    102   					gem = true, id = name, data = gem;
    67    103   					value = (5 * shards_per_gem) + 4;
    68    104   				};
    69    105   				amulet = { base = name };
    70    106   			};
................................................................................
    74    110   			local framedid = string.format("%s_frame_%s", amuletname, metalid)
    75    111   			local img_frame = img(string.format('sorcery_amulet_frame_%s.png',metalid))
    76    112   			minetest.register_craftitem(framedid, {
    77    113   				description = string.format("%s-framed %s amulet",sorcery.lib.str.capitalize(metalid), name);
    78    114   				inventory_image = img_sparkle:blit(img_frame):blit(img_stone):render();
    79    115   				wield_scale = { x = 0.6, y = 0.6, z = 0.6 };
    80    116   				groups = { sorcery_amulet = 1 };
          117  +				on_use = useamulet;
    81    118   				_sorcery = {
    82    119   					amulet = { base = name, frame = metalid };
    83    120   				};
    84    121   			})
    85    122   			local frag = metal.parts.fragment
    86    123   			minetest.register_craft {
    87    124   				output = framedid;

Modified init.lua from [f9d7281393] to [e9275c1995].

   128    128   	'harvester'; 'metallurgy-hot', 'metallurgy-cold';
   129    129   	'entities'; 'recipes'; 'coins'; 'interop';
   130    130   	'tnodes'; 'forcefield'; 'farcaster'; 'portal';
   131    131   	'cookbook', 'writing'; 'disassembly'; 'displacer';
   132    132   	'gravitator'; 'precipitator'; 'calendar', 'astrolabe';
   133    133   	'keypunch'; 'runeforge';
   134    134   
   135         -	'admin';
          135  +	'privs', 'admin';
   136    136   } do sorcery.load(u) end
   137    137   sorcery.stage('finalize')
   138    138   
   139    139   sorcery.registry.defercheck()

Modified lib/node.lua from [b1f018643d] to [5b83bf6142].

    63     63   	offsets = ofs;
    64     64   	purge_container = function(...) return purge_container(nil, ...) end;
    65     65   	purge_only = function(lst)
    66     66   		return function(...)
    67     67   			return purge_container(lst, ...)
    68     68   		end
    69     69   	end; 
           70  +
           71  +	is_air = function(pos)
           72  +		local n = sorcery.lib.node.force(pos)
           73  +		if n.name == 'air' then return true end
           74  +		local d = minetest.registered_nodes[n.name]
           75  +		if not d then return false end
           76  +		return not d.walkable
           77  +	end;
           78  +
           79  +	get_arrival_point = function(pos)
           80  +		local air = sorcery.lib.node.is_air
           81  +		if air(pos) then
           82  +			local n = {x=0,y=1,z=0}
           83  +			if air(vector.add(pos,n)) then return pos end
           84  +			local down = vector.subtract(pos,n)
           85  +			if air(down) then return down end
           86  +		else return nil end
           87  +	end;
    70     88   
    71     89   	amass = function(startpoint,names,directions)
    72     90   		if not directions then directions = ofs.neighbors end
    73     91   		local nodes, positions, checked = {},{},{}
    74     92   		local checkedp = function(pos)
    75     93   			for _,v in pairs(checked) do
    76     94   				if vector.equals(pos,v) then return true end

Modified potions.lua from [32e1f8328e] to [f87e1ae861].

     1      1   local u = sorcery.lib
     2      2   sorcery.registry.mk('infusions',false)
     3      3   sorcery.registry.mk('residue',false)
     4      4   
            5  +sorcery.register_potion_tbl = function(tbl) -- :/
            6  +	return sorcery.register_potion(tbl.name,tbl.label,tbl.desc,tbl.color,tbl.imgvariant,tbl.glow,tbl.extra)
            7  +end
     5      8   sorcery.register_potion = function(name,label,desc,color,imgvariant,glow,extra)
     6      9   	local image = 'sorcery_liquid_'..(imgvariant or 'dull')..'.png' .. 
     7     10   		'^[multiply:'..tostring(color)..
     8     11   		'^vessels_glass_bottle.png'
     9     12   
    10     13   	sorcery.register.residue.link('sorcery:' .. name, 'vessels:glass_bottle')
    11     14   	local node = {
................................................................................
    19     22   		);
    20     23   		short_description = label;
    21     24   		drawtype = "plantlike";
    22     25   		tiles = {image};
    23     26   		inventory_image = image;
    24     27   		paramtype = "light";
    25     28   		is_ground_content = false;
    26         -		light_source = glow or 0;
           29  +		light_source = glow and math.min(minetest.LIGHT_MAX,glow) or 0;
    27     30   		drop = 'sorcery:' .. name;
    28     31   		preserve_metadata = function(pos,node,meta,newstack)
    29     32   			newstack[1]:get_meta():from_table(meta)
    30     33   		end;
    31     34   		walkable = false;
    32     35   		selection_box = {
    33     36   			type = "fixed",

Added privs.lua version [62e9e10513].

            1  +minetest.register_privilege('sorcery:infinirune', {
            2  +	description = "runes don't discharge upon use, for debugging use only";
            3  +	give_to_singleplayer = false;
            4  +	give_to_admin = false;
            5  +})

Modified runeforge.lua from [b4ccf2cc5a] to [e97e283f3c].

            1  +-- TODO make some kind of disposable "filter" tool that runeforges require 
            2  +-- to generate runes and that wears down over time, to make amulets more
            3  +-- expensive than they currently are? the existing system is neat but
            4  +-- i think amulets are a little overpowered for something that just
            5  +-- passively consumes ley-current
            6  +
     1      7   local constants = {
     2         -	rune_mine_interval = 90;
            8  +	rune_mine_interval = 250;
     3      9   	-- how often a powered forge rolls for new runes
     4     10   
     5     11   	rune_cache_max = 4;
     6     12   	-- how many runes a runeforge can hold at a time
     7     13   	
     8         -	rune_grades = {'Fragile', 'Shoddy', 'Ordinary', 'Pristine'};
           14  +	rune_grades = {'Fragile', 'Weak', 'Ordinary', 'Pristine', 'Sublime'};
     9     15   	-- how many grades of rune quality/power there are
           16  +	
           17  +	amulet_grades = {'Slight', 'Minor', 'Major', 'Grand', 'Ultimate' };
           18  +	-- what kind of amulet each rune grade translates to
           19  +	
           20  +	phial_kinds = {
           21  +		lesser   = {grade = 1; name = 'Lesser';   infusion = 'sorcery:powder_brass'};
           22  +		simple   = {grade = 2; name = 'Simple';   infusion = 'sorcery:powder_silver'};
           23  +		great    = {grade = 3; name = 'Great';    infusion = 'sorcery:powder_gold'};
           24  +		splendid = {grade = 4; name = 'Splendid'; infusion = 'sorcery:powder_electrum'};
           25  +		exalted  = {grade = 5; name = 'Exalted';  infusion = 'sorcery:powder_levitanium'};
           26  +		supreme  = {grade = 6; name = 'Supreme';  infusion = 'sorcery:essence_force'};
           27  +	};
    10     28   }
           29  +local calc_phial_props = function(phial) --> mine interval: float, time factor: float
           30  +	local g = phial:get_definition()._proto.grade
           31  +	local i = constants.rune_mine_interval 
           32  +	local fac = (g-1) / 5
           33  +	return i - ((i*0.5) * fac), 0.5 * fac
           34  +end
    11     35   sorcery.register.runes.foreach('sorcery:generate',{},function(name,rune)
    12     36   	local id = 'sorcery:rune_' .. name
    13     37   	rune.image = rune.image or string.format('sorcery_rune_%s.png',name)
    14     38   	rune.item = id
    15     39   	minetest.register_craftitem(id, {
    16     40   		description = sorcery.lib.color(rune.tone):readable():fmt(rune.name .. ' Rune');
    17     41   		short_description = rune.name .. ' Rune';
................................................................................
    20     44   		groups = {
    21     45   			sorcery_rune = 1;
    22     46   			not_in_creative_inventory = 1;
    23     47   		};
    24     48   		_proto = { id = name, data = rune; };
    25     49   	})
    26     50   end)
           51  +
           52  +for name,p in pairs(constants.phial_kinds) do
           53  +	local f = string.format
           54  +	local color = sorcery.lib.color(255,27,188)
           55  +	local fac = p.grade / 6
           56  +	local id = f('phial_%s', name);
           57  +	sorcery.register_potion_tbl {
           58  +		name = id;
           59  +		label = f('%s Phial',p.name);
           60  +		desc = "A powerful liquid consumed in the operation of a rune forge. Its quality determines how fast new runes can be constructed.";
           61  +		color = color:brighten(1 + fac*0.5);
           62  +		imgvariant = (fac >= 5) and 'sparkle' or 'dull';
           63  +		glow = 5+p.grade;
           64  +		extra = {
           65  +			groups = { sorcery_phial = p.grade };
           66  +			_proto = { id = name, data = p };
           67  +		};
           68  +	}
           69  +	sorcery.register.infusions.link {
           70  +		infuse = p.infusion;
           71  +		into = 'sorcery:potion_subtle';
           72  +		output = id;
           73  +	}
           74  +end
           75  +
           76  +local register_rune_wrench = function(w)
           77  +	local mp = sorcery.data.metals[w.metal].parts
           78  +	minetest.register_tool(w.name, {
           79  +		description = w.desc;
           80  +		inventory_image = w.img;
           81  +		groups = {
           82  +			sorcery_magitech = 1;
           83  +			sorcery_rune_wrench = 1;
           84  +			crafttool = 50;
           85  +		};
           86  +		_proto = w;
           87  +		_sorcery = {
           88  +			recipe = { note = w.note };
           89  +		};
           90  +	})
           91  +	minetest.register_craft {
           92  +		output = w.name;
           93  +		recipe = {
           94  +			{'',                        mp.fragment,''};
           95  +			{'',                        mp.ingot,   mp.fragment};
           96  +			{'sorcery:vidrium_fragment','',         ''};
           97  +		};
           98  +	}
           99  +end
          100  +
          101  +register_rune_wrench {
          102  +	name = 'sorcery:rune_wrench', desc = 'Rune Wrench';
          103  +	img = 'sorcery_rune_wrench.png', metal = 'brass';
          104  +	powers = { imbue = 30 };
          105  +	note = 'A runeworking tool used to imbue amulets with enchantments';
          106  +}
          107  +
          108  +register_rune_wrench {
          109  +	name = 'sorcery:rune_wrench_iridium', desc = 'Iridium Rune Wrench';
          110  +	img = 'sorcery_rune_wrench_iridium.png', metal = 'iridium';
          111  +	powers = { imbue = 80, extract = 40 };
          112  +	note = 'A rare and powerful runeworking tool used to imbue amulets with enchantments, or extract runes intact from enchanted amulets';
          113  +}
    27    114   
    28    115   local rune_set = function(stack,r)
    29    116   	local m = stack:get_meta()
    30    117   	local def = stack:get_definition()._proto.data
    31    118   	local grade
    32    119   	if r.grade then grade = r.grade
    33    120   	elseif m:contains('rune_grade') then grade = m:get_int('rune_grade') end
................................................................................
    36    123   	local title = sorcery.lib.color(def.tone):readable():fmt(string.format('%s %s Rune',qpfx,def.name))
    37    124   
    38    125   	m:set_int('rune_grade',grade)
    39    126   	m:set_string('description',title)
    40    127   end
    41    128   
    42    129   sorcery.amulet = {}
    43         -sorcery.amulet.setrune = function(stack,rune)
          130  +sorcery.amulet.setrune = function(stack,rune,user)
    44    131   	local m = stack:get_meta()
    45    132   	if rune then
    46    133   		local rp = rune:get_definition()._proto
    47    134   		local rg = rune:get_meta():get_int('rune_grade')
    48    135   		m:set_string('amulet_rune', rp.id)
    49    136   		m:set_int('amulet_rune_grade', rg)
    50    137   		local spell = sorcery.amulet.getspell(stack)
    51    138   		if not spell then return nil end
    52    139   
    53         -		local name = string.format('Amulet of %s', spell.name)
    54         -
          140  +		local name = string.format('Amulet of %s %s', constants.amulet_grades[rg], spell.name)
    55    141   		m:set_string('description', sorcery.lib.ui.tooltip {
    56    142   			title = name;
    57    143   			color = spell.tone;
    58    144   			desc = spell.desc;
    59    145   		})
          146  +
          147  +		if spell.apply then spell.apply {
          148  +			stack = stack;
          149  +			meta = m;
          150  +			user = user;
          151  +			self = spell;
          152  +		} end
    60    153   	else
          154  +		local spell = sorcery.amulet.getspell(stack)
    61    155   		m:set_string('description','')
    62    156   		m:set_string('amulet_rune','')
    63    157   		m:set_string('amulet_rune_grade','')
          158  +		if spell and spell.remove then spell.remove {
          159  +			stack = stack;
          160  +			meta = m;
          161  +			user = user;
          162  +			self = spell;
          163  +		} end
    64    164   	end
    65    165   	return stack
    66    166   end
          167  +
          168  +sorcery.amulet.stats = function(stack)
          169  +	local spell = sorcery.amulet.getspell(stack)
          170  +	if not spell then return nil end
          171  +	local power = spell.grade
          172  +	
          173  +	if spell.base_spell then
          174  +		-- only consider the default effect of the frame metal
          175  +		-- if the frame doesn't totally override the spell
          176  +		power = power * (spell.framestats and spell.framestats.power or 1)
          177  +	end
          178  +
          179  +	return {
          180  +		power = power;
          181  +	}
          182  +end
    67    183   
    68    184   sorcery.amulet.getrune = function(stack)
    69    185   	local m = stack:get_meta()
    70    186   	if not m:contains('amulet_rune') then return nil end
    71    187   	local rune = m:get_string('amulet_rune')
    72    188   	local grade = m:get_int('amulet_rune_grade')
    73    189   	local rs = ItemStack(sorcery.data.runes[rune].item)
................................................................................
    74    190   	rune_set(rs, {grade = grade})
    75    191   	return rs
    76    192   end
    77    193   
    78    194   sorcery.amulet.getspell = function(stack)
    79    195   	local m = stack:get_meta()
    80    196   	local proto = stack:get_definition()._sorcery.amulet
          197  +	if not m:contains('amulet_rune') then return nil end
    81    198   	local rune = m:get_string('amulet_rune')
          199  +	local rg = m:get_string('amulet_rune_grade')
    82    200   	local rd = sorcery.data.runes[rune]
    83    201   	local spell = rd.amulets[proto.base]
    84    202   	if not spell then return nil end
    85         -	local title,desc,cast = spell.name, spell.desc, spell.cast
          203  +	local title,desc,cast,apply,remove = spell.name, spell.desc, spell.cast, spell.apply, spell.remove -- FIXME in serious need of refactoring
          204  +	local base_spell = true
    86    205   
    87    206   	if proto.frame and spell.frame and spell.frame[proto.frame] then
    88    207   		local sp = spell.frame[proto.frame]
    89    208   		title = sp.name or title
    90    209   		desc = sp.desc or desc
    91    210   		cast = sp.desc or cast
          211  +		apply = sp.apply or apply
          212  +		remove = sp.remove or remove
          213  +		base_spell = false
    92    214   	end
    93    215   	
    94    216   	return {
    95    217   		rune = rune;
          218  +		grade = rg;
    96    219   		spell = spell;
    97         -		name = title;
    98         -		desc = desc;
    99         -		cast = cast;
          220  +		name = title, desc = desc;
          221  +		cast = cast, apply = apply, remove = remove;
          222  +		frame = proto.frame;
          223  +		framestats = proto.frame and sorcery.data.metals[proto.frame].amulet;
   100    224   		tone = sorcery.lib.color(rd.tone);
          225  +		base_spell = base_spell;
   101    226   	}
   102    227   end
   103    228   
   104    229   
   105    230   local runeforge_update = function(pos,time)
   106    231   	local m = minetest.get_meta(pos)
   107    232   	local i = m:get_inventory()
   108    233   	local l = sorcery.ley.netcaps(pos,time or 1)
   109    234   
   110    235   	local pow_min = l.self.powerdraw >= l.self.minpower
   111    236   	local pow_max = l.self.powerdraw >= l.self.maxpower
          237  +	local has_phial = function() return not i:is_empty('phial') end
   112    238   
   113         -	if time and pow_min then -- roll for runes
   114         -		local rolls = math.floor(time/constants.rune_mine_interval)
          239  +	if time and has_phial() and pow_min then -- roll for runes
          240  +		local rolls = math.floor(time/calc_phial_props(i:get_stack('phial',1)))
   115    241   		local newrunes = {}
   116    242   		for _=1,rolls do
   117    243   			local choices = {}
   118    244   			for name,rune in pairs(sorcery.data.runes) do
   119    245   				if rune.minpower*time <= l.self.powerdraw and math.random(rune.rarity) == 1 then
   120    246   					local n = ItemStack(rune.item)
   121    247   					choices[#choices + 1] = n
   122    248   				end
   123    249   			end
   124    250   			if #choices > 0 then newrunes[#newrunes + 1] = choices[math.random(#choices)] end
   125    251   		end
   126    252   
   127         -		print('rolled for runes, got', dump(newrunes))
   128    253   		for _,r in pairs(newrunes) do
   129         -			if i:room_for_item('cache',r) then
          254  +			if i:room_for_item('cache',r) and has_phial() then
   130    255   				local qual = math.random(#constants.rune_grades)
   131    256   				rune_set(r,{grade = qual})
   132    257   				i:add_item('cache',r)
   133         -			end
          258  +				-- consume a phial
          259  +				local ph = i:get_stack('phial',1)
          260  +				local n = ph:get_name()
          261  +				ph:take_item(1) i:set_stack('phial',1,ph)
          262  +				minetest.add_item(pos,i:add_item('refuse',ItemStack(sorcery.register.residue.db[n])))
          263  +			else break end
   134    264   		end
   135    265   	end
   136    266   
          267  +	has_phial = has_phial()
   137    268   	local spec = string.format([[
   138    269   		formspec_version[3] size[10.25,8] real_coordinates[true]
   139    270   		list[context;cache;%f,0.25;%u,1;]
   140    271   		list[context;amulet;3.40,1.50;1,1;]
   141    272   		list[context;active;5.90,1.50;1,1;]
          273  +
          274  +		list[context;wrench;1.25,1.75;1,1;]
          275  +		list[context;phial;7.25,1.75;1,1;]
          276  +		list[context;refuse;8.50,1.75;1,1;]
          277  +
   142    278   		list[current_player;main;0.25,3;8,4;]
   143    279   
   144    280   		image[0.25,0.50;1,1;sorcery_statlamp_%s.png]
   145    281   	]], (10.5 - constants.rune_cache_max*1.25)/2, constants.rune_cache_max,
   146         -	    pow_max and 'green' or (pow_min and 'yellow') or 'off')
          282  +	    ((has_phial and pow_max) and 'green' ) or
          283  +		((has_phial and pow_min) and 'yellow') or 'off')
          284  +
          285  +	local ghost = function(slot,x,y,img)
          286  +		if i:is_empty(slot) then spec = spec .. string.format([[
          287  +			image[%f,%f;1,1;%s.png]
          288  +		]], x,y,img) end
          289  +	end
          290  +
          291  +	ghost('active',5.90,1.50,'sorcery_ui_ghost_rune')
          292  +	ghost('amulet',3.40,1.50,'sorcery_ui_ghost_amulet')
          293  +	ghost('wrench',1.25,1.75,'sorcery_ui_ghost_rune_wrench')
          294  +	ghost('phial',7.25,1.75,'vessels_shelf_slot')
   147    295   	
   148    296   	m:set_string('formspec',spec)
          297  +
          298  +	if i:is_empty('phial') then return false end
   149    299   	return true
   150    300   end
   151    301   
   152    302   local rfbox = {
   153    303   	type = 'fixed';
   154    304   	fixed = {
   155    305   		-0.5, -0.5, -0.5;
................................................................................
   180    330   		'default_copper_block.png';
   181    331   	};
   182    332   	_sorcery = {
   183    333   		ley = {
   184    334   			mode = 'consume';
   185    335   			affinity = {'praxic'};
   186    336   			power = function(pos,time)
          337  +				local i = minetest.get_meta(pos):get_inventory()
          338  +				if i:is_empty('phial') then return 0 end
          339  +				local phial = i:get_stack('phial',1)
          340  +
   187    341   				local max,min = 0
   188    342   				for _,r in pairs(sorcery.data.runes) do
   189    343   					if r.minpower > max then max = r.minpower end
   190    344   					if min == nil or r.minpower < min then min = r.minpower end
   191    345   				end
          346  +				-- high-quality phials reduce power usage
          347  +				local fac = select(2, calc_phial_props(phial))
          348  +				min = min * fac  max = max * fac
   192    349   				return min*time,max*time
   193    350   			end;
   194    351   		};
   195    352   		on_leychange = runeforge_update;
   196    353   		recipe = {
   197    354   			note = 'Periodically creates runes when sufficiently powered and can be used to imbue them into an amulet, giving it a powerful magical effect';
   198    355   		};
   199    356   	};
   200    357   	on_construct = function(pos)
   201    358   		local m = minetest.get_meta(pos)
   202    359   		local i = m:get_inventory()
   203    360   		i:set_size('cache',constants.rune_cache_max)
   204         -		i:set_size('amulet',1)
   205         -		i:set_size('active',1)
          361  +		i:set_size('wrench',1) i:set_size('phial',1) i:set_size('refuse',1)
          362  +		i:set_size('amulet',1) i:set_size('active',1)
   206    363   		m:set_string('infotext','Rune Forge')
   207    364   		runeforge_update(pos)
   208         -		minetest.get_node_timer(pos):start(constants.rune_mine_interval)
   209    365   	end;
   210    366   	after_dig_node = sorcery.lib.node.purge_only {'amulet'};
   211    367   	on_timer = runeforge_update;
   212    368   	on_metadata_inventory_move = function(pos, fl,fi, tl,ti, count, user)
   213    369   		local inv = minetest.get_meta(pos):get_inventory()
          370  +		local wrench if not inv:is_empty('wrench') then
          371  +			wrench = inv:get_stack('wrench',1):get_definition()._proto
          372  +		end
          373  +		local wwear = function(cap)
          374  +			local s = inv:get_stack('wrench',1)
          375  +			local wear = 65535 / wrench.powers[cap]
          376  +			s:add_wear(wear)
          377  +			inv:set_stack('wrench',1,s)
          378  +		end
   214    379   		if fl == 'active' then
   215         -			inv:set_stack('amulet',1,sorcery.amulet.setrune(inv:get_stack('amulet',1)))
   216         -		elseif tl == 'active' then
   217         -			inv:set_stack('amulet',1,sorcery.amulet.setrune(inv:get_stack('amulet',1), inv:get_stack(tl,ti)))
          380  +			inv:set_stack('amulet',1,sorcery.amulet.setrune(inv:get_stack('amulet',1),nil,user))
          381  +			-- only special wrenches can extract runes intact
          382  +			if wrench.powers.extract then wwear('extract')
          383  +				minetest.sound_play('sorcery_chime', { pos = pos, gain = 0.5 })
          384  +			elseif wrench.powers.purge then wwear('purge')
          385  +				inv:set_stack(tl,ti,ItemStack(nil))
          386  +				minetest.sound_play('sorcery_disjoin', { pos = pos, gain = 0.5 })
          387  +			end
          388  +		elseif tl == 'active' and wrench.powers.imbue then
          389  +			local amulet = sorcery.amulet.setrune(inv:get_stack('amulet',1), inv:get_stack(tl,ti), user)
          390  +			local spell = sorcery.amulet.getspell(amulet)
          391  +			sorcery.vfx.enchantment_sparkle({
          392  +				under = pos;
          393  +				above = vector.add(pos,{x=0,y=1,z=0});
          394  +			}, spell.tone:brighten(1.2):hex())
          395  +			minetest.sound_play('xdecor_enchanting', { pos = pos, gain = 0.5 })
          396  +			inv:set_stack('amulet',1,amulet)
          397  +			wwear('imbue')
   218    398   		end
          399  +		-- trigger the update early to clean up the ghost image :/
          400  +		-- minetest needs a cleaner way to handle these
          401  +		runeforge_update(pos)
   219    402   	end;
   220    403   	on_metadata_inventory_put = function(pos, list, idx, stack, user)
          404  +		local inv = minetest.get_meta(pos):get_inventory()
   221    405   		if list == 'amulet' then
   222         -			local inv = minetest.get_meta(pos):get_inventory()
   223    406   			inv:set_stack('active',1,ItemStack(sorcery.amulet.getrune(stack)))
   224    407   		end
          408  +		runeforge_update(pos)
          409  +		if not inv:is_empty('phial') then
          410  +			minetest.get_node_timer(pos):start(calc_phial_props(inv:get_stack('phial',1)))
          411  +		end
   225    412   	end;
   226    413   	on_metadata_inventory_take = function(pos, list, idx, stack, user)
   227    414   		if list == 'amulet' then
   228    415   			minetest.get_meta(pos):get_inventory():set_stack('active',1,ItemStack())
   229    416   		end
          417  +		runeforge_update(pos)
   230    418   	end;
   231    419   	allow_metadata_inventory_put = function(pos,list,idx,stack,user)
   232    420   		if list == 'amulet' then
   233    421   			if minetest.get_item_group(stack:get_name(), 'sorcery_amulet') ~= 0 then
   234    422   				return 1
   235    423   			end
          424  +		end
          425  +		if list == 'phial' then
          426  +			if minetest.get_item_group(stack:get_name(), 'sorcery_phial') ~= 0 then
          427  +				return stack:get_count()
          428  +			end
          429  +		end
          430  +		if list == 'wrench' then
          431  +			if minetest.get_item_group(stack:get_name(), 'sorcery_rune_wrench') ~= 0 then
          432  +				return 1
          433  +			end
   236    434   		end
   237    435   		return 0
   238    436   	end;
   239    437   	allow_metadata_inventory_take = function(pos,list,idx,stack,user)
   240         -		if list == 'amulet' then return 1 end
          438  +		if list == 'amulet' or list == 'wrench' then return 1 end
          439  +		if list == 'phial' or list == 'refuse' then return stack:get_count() end
   241    440   		return 0
   242    441   	end;
   243    442   	allow_metadata_inventory_move = function(pos, fl,fi, tl,ti, count, user)
          443  +		local inv = minetest.get_meta(pos):get_inventory()
          444  +		local wrench if not inv:is_empty('wrench') then
          445  +			wrench = inv:get_stack('wrench',1):get_definition()._proto
          446  +		end
   244    447   		if fl == 'cache' then
   245    448   			if tl == 'cache' then return 1 end
   246    449   			if tl == 'active' then
   247         -				local inv = minetest.get_meta(pos):get_inventory()
   248         -				if not inv:is_empty('amulet') then
          450  +				print(dump(wrench))
          451  +				if wrench and wrench.powers.imbue and not inv:is_empty('amulet') then
   249    452   					local amulet = inv:get_stack('amulet',1)
   250    453   					local rune = inv:get_stack(fl,fi)
   251    454   					if sorcery.data.runes[rune:get_definition()._proto.id].amulets[amulet:get_definition()._sorcery.amulet.base] then
   252    455   						return 1
   253    456   					end
   254    457   				end
   255    458   			end
   256    459   		end
   257    460   		if fl == 'active' then
   258         -			if tl == 'cache' then return 1 end
          461  +			if tl == 'cache' and wrench and (wrench.powers.extract or wrench.powers.purge) then return 1 end
   259    462   		end
   260    463   		return 0
   261    464   	end;
   262    465   })
   263    466   
   264    467   do local m = sorcery.data.metals
   265    468   	-- temporary recipe until a fancier multi-part crafting path can be come up with

Added sounds/sorcery_bloody_burst.ogg version [86beb5bbfe].

cannot compute difference between binary files

Added sounds/sorcery_bloody_hit.1.ogg version [4a47968931].

cannot compute difference between binary files

Added sounds/sorcery_bloody_hit.2.ogg version [6bea57cc69].

cannot compute difference between binary files

Added sounds/sorcery_bloody_hit.3.ogg version [3d0dd5fb33].

cannot compute difference between binary files

Added sounds/sorcery_chime.1.ogg version [f5f3fa1f0b].

cannot compute difference between binary files

Added sounds/sorcery_chime.2.ogg version [e2c798dcb3].

cannot compute difference between binary files

Added sounds/sorcery_coins.ogg version [24495c2ec0].

cannot compute difference between binary files

Added sounds/sorcery_crunch.1.ogg version [bd7af2ce27].

cannot compute difference between binary files

Added sounds/sorcery_crunch.2.ogg version [436bf86ffa].

cannot compute difference between binary files

Added sounds/sorcery_crunch.3.ogg version [b858a7a30d].

cannot compute difference between binary files

Added sounds/sorcery_crunch.4.ogg version [5a749d38d7].

cannot compute difference between binary files

Added sounds/sorcery_disjoin.1.ogg version [bafe4e15de].

cannot compute difference between binary files

Added sounds/sorcery_disjoin.2.ogg version [72809eab09].

cannot compute difference between binary files

Added textures/sorcery_rune_wrench.png version [c39260a038].

cannot compute difference between binary files

Added textures/sorcery_rune_wrench_iridium.png version [076b434e79].

cannot compute difference between binary files

Added textures/sorcery_ui_ghost_amulet.png version [1e1e56f3c1].

cannot compute difference between binary files

Added textures/sorcery_ui_ghost_rune.png version [8e47e791d3].

cannot compute difference between binary files

Added textures/sorcery_ui_ghost_rune_wrench.png version [1bd60d3986].

cannot compute difference between binary files

Modified tnodes.lua from [1f5f95dd4b] to [4064266f0d].

     3      3   		drawtype = 'airlike';
     4      4   		light_source = 5 + math.ceil(i * (11/minetest.LIGHT_MAX));
     5      5   		sunlight_propagates = true;
     6      6   		buildable_to = true;
     7      7   		pointable = false;
     8      8   		walkable = false;
     9      9   		floodable = true;
           10  +		drop = {max_items = 0, items = {}};
           11  +		on_blast = function() end; -- not affected by explosions
    10     12   		groups = { air = 1; sorcery_air = 1; not_in_creative_inventory = 1; };
    11     13   		on_construct = function(pos)
    12     14   			local meta = minetest.get_meta(pos)
    13     15   			meta:set_float('duration',10)
    14     16   			meta:set_float('timeleft',10)
    15     17   			meta:set_int('power',minetest.LIGHT_MAX)
    16     18   			minetest.get_node_timer(pos):start(1)

Modified vfx.lua from [291a2c52f8] to [343a5ccf55].

     1      1   sorcery.vfx = {}
     2      2   
     3         -sorcery.vfx.cast_sparkle = function(caster,color,strength,duration)
            3  +sorcery.vfx.cast_sparkle = function(caster,color,strength,duration,pos)
            4  +	local ofs = pos
            5  +		and function(x) return vector.add(pos,x) end
            6  +		or  function(x) return x end
            7  +	local height = caster:get_properties().eye_height
     4      8   	minetest.add_particlespawner {
     5      9   		amount = 70 * strength;
     6     10   		time = duration or 1.5;
     7     11   		attached = caster;
     8     12   		texture = sorcery.lib.image('sorcery_spark.png'):multiply(color):render();
     9         -		minpos = { x = -0.1, z =  0.5, y =  1.2}; 
    10         -		maxpos = { x =  0.1, z =  0.3, y =  1.6}; 
           13  +		minpos = ofs({ x =  0.0, z =  0.6, y =  height*0.7});
           14  +		maxpos = ofs({ x =  0.4, z =  0.2, y =  height*1.1});
    11     15   		minvel = { x = -0.5, z = -0.5, y = -0.5};
    12     16   		maxvel = { x =  0.5, z =  0.5, y =  0.5};
    13     17   		minacc = { x =  0.0, z =  0.0, y =  0.5};
    14     18   		maxacc = { x =  0.0, z =  0.0, y =  0.5};
    15     19   		minsize = 0.4, maxsize = 0.8;
    16     20   		minexptime = 1, maxexptime = 1;
    17     21   		glow = 14;
................................................................................
    19     23   			type = 'vertical_frames';
    20     24   			aspect_w = 16;
    21     25   			aspect_h = 16;
    22     26   			length = 1.1;
    23     27   		};
    24     28   	}
    25     29   end
           30  +
           31  +sorcery.vfx.body_sparkle = function(body,color,str,pos)
           32  +	local img = sorcery.lib.image
           33  +	local tex = img('sorcery_spark.png')
           34  +	local pi = tex:blit(tex:multiply(color)):render()
           35  +	local ofs = pos
           36  +		and function(x) return vector.add(pos,x) end
           37  +		or  function(x) return x end
           38  +	return minetest.add_particlespawner {
           39  +		amount = 25 * str;
           40  +		time = 0.5;
           41  +		attached = body;
           42  +		minpos = ofs{x = -0.5, y = -0.5, z = -0.5};
           43  +		maxpos = ofs{x =  0.5, y =  1.5, z =  0.5};
           44  +		minacc = {x = -0.3, y =  0.0, z =  0.3};
           45  +		maxacc = {x = -0.3, y =  0.0, z =  0.3};
           46  +		minvel = {x = -0.6, y = -0.2, z =  0.6};
           47  +		maxvel = {x = -0.6, y =  0.2, z =  0.6};
           48  +		minexptime = 1.0;
           49  +		maxexptime = 1.5;
           50  +		texture = pi;
           51  +		glow = 14;
           52  +		animation = {
           53  +			type = 'vertical_frames';
           54  +			aspect_w = 16, aspect_h = 16;
           55  +			length = 1.6;
           56  +		};
           57  +	}
           58  +end
    26     59   
    27     60   sorcery.vfx.enchantment_sparkle = function(tgt,color)
    28     61   	local minvel, maxvel
    29     62   	if minetest.get_node(vector.add(tgt.under,{y=1,z=0,x=0})).name == 'air' then
    30     63   		minvel = {x=0,z=0,y= 0.3}  maxvel = {x=0,z=0,y= 1.5};
    31     64   	else
    32     65   		local dir = vector.subtract(tgt.under,tgt.above)

Modified wands.lua from [e3e1a760a8] to [a021a4fd8f].

    91     91   				-- but power levels are unpredictable
    92     92   				tone = u.color(255,117,40);
    93     93   				tex = u.image('default_copper_block.png');
    94     94   				wandprops = { flux = 0.7, chargetime = 0.5 };
    95     95   			};
    96     96   			silver = {
    97     97   				tone = u.color(215,238,241);
    98         -				tex = u.image('default_gold_block'):colorize(u.color(255,238,241), 255);
           98  +				tex = u.image('default_gold_block.png'):colorize(u.color(255,238,241), 255);
    99     99   				wandprops = {};
   100    100   			};
   101    101   			steel = {
   102    102   				tone = u.color(255,255,255);
   103         -				tex = u.image('default_steel_block');
          103  +				tex = u.image('default_steel_block.png');
   104    104   				wandprops = {};
   105    105   			};
   106    106   		};
   107    107   		gem = sorcery.data.gems;
   108    108   	};
   109    109   	util = {
   110    110   		baseid = function(wand)