sorcery  Check-in [00922196a9]

Overview
Comment:add some more spells, add spell infrastructure to support metamagic, especially disjunction, various tweaks and bugfixes. [emergency commit]
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 00922196a9362e9cadeb8f074f7a94590c213adf34cfc28a1ab51fe86ba99b5b
User & Date: lexi on 2020-10-24 01:21:08
Other Links: manifest | tags
Context
2020-10-26
03:58
add over-time spellcasting abstraction to enable metamagic and in particular disjunction, add more animations and sound effects, add excavation spell, possibly some others, forget when the last commit was, edit a bunch of magitech to make it subject to the disjunction mechanism (throw up a disjunction aura and waltz right through those force fields bby, wheee), also illumination spells, tweak runeforge and rune frequence to better the balance and also limit player frustration, move some math functions into their own library category, various tweaks and bugfixes, probably other shit i don't remember check-in: 147592b8e9 user: lexi tags: trunk
2020-10-24
01:21
add some more spells, add spell infrastructure to support metamagic, especially disjunction, various tweaks and bugfixes. [emergency commit] check-in: 00922196a9 user: lexi tags: trunk
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
Changes

Modified data/runes.lua from [47aa367750] to [4908250df2].

     1      1   -- a rune is an abstract object created by a runeforge, which can be
     2      2   -- applied to an amulet in order to imbue that amulet with unique
     3      3   -- and fearsome powers. the specific spell depends on the stone the
     4      4   -- rune is applied to, and not all runes can necessarily be applied
     5      5   -- to all stones.
     6      6   
            7  +local sparkle = function(color, spell, amt,time,minsize,maxsize,sh)
            8  +	spell.visual_subjects {
            9  +		amount = amt, time = time, -- attached = s;
           10  +		minpos = { x = -0.3, y = -0.5, z = -0.3 };
           11  +		maxpos = { x =  0.3, y = sh*1.1, z = 0.3 };
           12  +		minvel = { x = -0.4, y = -0.2, z = -0.4 };
           13  +		maxvel = { x =  0.4, y =  0.2, z =  0.4 };
           14  +		minacc = { x = -0.5, y = -0.4, z = -0.5 };
           15  +		maxacc = { x =  0.5, y =  0.4, z =  0.5 };
           16  +		minexptime = 1.0, maxexptime = 2.0;
           17  +		minsize = minsize, maxsize = maxsize, glow = 14;
           18  +		texture = sorcery.vfx.glowspark(color):render();
           19  +		animation = {
           20  +			type = 'vertical_frames';
           21  +			aspect_w = 16, aspect_h = 16;
           22  +		};
           23  +	}
           24  +end
           25  +local sparktrail = function(fn,tgt,color)
           26  +	return (fn or minetest.add_particlespawner)({
           27  +		amount = 240, time = 1, attached = tgt;
           28  +		minpos = {x = -0.4, y = -0.5, z = -0.4};
           29  +		maxpos = {x =  0.4, y = tgt:get_properties().eye_height or 0.5, z =  0.4};
           30  +		minacc = {x =  0.0, y = 0.05, z =  0.0};
           31  +		maxacc = {x =  0.0, y = 0.15, z =  0.0};
           32  +		minexptime = 1.5, maxexptime = 5;
           33  +		minsize = 0.5, maxsize = 2.6, glow = 14;
           34  +		texture = sorcery.vfx.glowspark(color):render();
           35  +		animation = {
           36  +			type = 'vertical_frames', length = 5.1;
           37  +			aspect_w = 16, aspect_h = 16;
           38  +		};
           39  +	});
           40  +end
     7     41   return {
     8     42   	translocate = {
     9     43   		name = 'Translocate';
    10     44   		tone = {0,235,233};
    11     45   		minpower = 3;
    12     46   		rarity = 15;
    13     47   		amulets = {
................................................................................
    48     82   					else
    49     83   						local pos = minetest.string_to_pos(ctx.meta:get_string('rune_return_dest'))
    50     84   						ctx.meta:set_string('rune_return_dest','')
    51     85   						local subjects = { ctx.caster }
    52     86   						local center = ctx.caster:get_pos()
    53     87   						ctx.sparkle = false
    54     88   						local delay = math.max(3,10 - ctx.stats.power) + 3*(math.random()*2-1)
    55         -						print('teledelay',delay,ctx.stats.power)
    56     89   						for _,s in pairs(subjects) do
    57     90   							local offset = vector.subtract(s:get_pos(), center)
    58     91   							local pt = sorcery.lib.node.get_arrival_point(vector.add(pos,offset))
    59     92   							if pt then
    60         -								minetest.sound_play('sorcery_stutter', {
    61         -									object = s, gain = 0.8;
    62         -								},true)
    63         -								local windup = minetest.sound_play('sorcery_windup',{
    64         -									object = s, gain = 0.4;
    65         -								})
           93  +								-- minetest.sound_play('sorcery_stutter', {
           94  +								-- 	object = s, gain = 0.8;
           95  +								-- },true)
    66     96   								local mydelay = delay + math.random(-10,10)*.1;
    67         -								local spark = sorcery.lib.image('sorcery_spark.png')
    68     97   								local sh = s:get_properties().eye_height
    69         -								local sparkle = function(amt,time,minsize,maxsize)
    70         -									minetest.add_particlespawner {
    71         -										amount = amt, time = time, attached = s;
    72         -										minpos = { x = -0.3, y = -0.5, z = -0.3 };
    73         -										maxpos = { x =  0.3, y = sh*1.1, z = 0.3 };
    74         -										minvel = { x = -0.4, y = -0.2, z = -0.4 };
    75         -										maxvel = { x =  0.4, y =  0.2, z =  0.4 };
    76         -										minacc = { x = -0.5, y = -0.4, z = -0.5 };
    77         -										maxacc = { x =  0.5, y =  0.4, z =  0.5 };
    78         -										minexptime = 1.0, maxexptime = 2.0;
    79         -										minsize = minsize, maxsize = maxsize, glow = 14;
    80         -										texture = spark:blit(spark:multiply(sorcery.lib.color(29,205,247))):render();
    81         -										animation = {
    82         -											type = 'vertical_frames';
    83         -											aspect_w = 16, aspect_h = 16;
           98  +								local color = sorcery.lib.color(29,205,247)
           99  +								sorcery.lib.node.preload(pt,s)
          100  +								sorcery.spell.cast {
          101  +									duration = mydelay;
          102  +									caster = ctx.caster;
          103  +									subjects = {{player=s,dest=pt}};
          104  +									timeline = {
          105  +										[0] = function(sp,_,timeleft)
          106  +											sparkle(color,sp,timeleft*100, timeleft, 0.3,1.3, sh)
          107  +											sp.windup = (sp.play_now{
          108  +												sound = 'sorcery_windup';
          109  +												where = 'subjects';
          110  +												gain = 0.4;
          111  +												fade = 1.5;
          112  +											})[1]
          113  +										end;
          114  +										[0.4] = function(sp,_,timeleft)
          115  +											sparkle(color,sp,timeleft*150, timeleft, 0.6,1.8, sh)
          116  +										end;
          117  +										[0.7] = function(sp,_,timeleft)
          118  +											sparkle(color,sp,timeleft*80, timeleft, 2,4, sh)
          119  +										end;
          120  +										[1] = function(sp)
          121  +											sp.silence(sp.windup)
          122  +											minetest.sound_play('sorcery_zap', { pos = pt, gain = 0.4 },true)
          123  +											minetest.sound_play('sorcery_zap', { pos = s:get_pos(), gain = 0.4 },true)
          124  +											sorcery.vfx.body_sparkle(nil,sorcery.lib.color(20,255,120),2,s:get_pos())
          125  +											s:set_pos(pt)
          126  +											sorcery.vfx.body_sparkle(s,sorcery.lib.color(20,120,255),2)
          127  +										end;
          128  +									};
          129  +									sounds = {
          130  +										[0] = {
          131  +											pos = 'subjects';
          132  +											sound = 'sorcery_stutter';
    84    133   										};
    85         -									}
    86         -								end
    87         -								sparkle(mydelay*100,mydelay,0.3,1.3)
    88         -								minetest.after(mydelay*0.4, function()
    89         -									local timeleft = mydelay - (mydelay*0.4)
    90         -									sparkle(timeleft*150, timeleft, 0.6,1.8)
    91         -								end)
    92         -								minetest.after(mydelay*0.7, function()
    93         -									local timeleft = mydelay - (mydelay*0.7)
    94         -									sparkle(timeleft*80, timeleft, 2,4)
    95         -								end)
    96         -								sorcery.lib.node.preload(pt,s)
    97         -								minetest.after(mydelay, function()
    98         -									minetest.sound_stop(windup)
    99         -									minetest.sound_play('sorcery_zap', { pos = pt, gain = 0.4 },true)
   100         -									minetest.sound_play('sorcery_zap', { pos = s:get_pos(), gain = 0.4 },true)
   101         -									sorcery.vfx.body_sparkle(nil,sorcery.lib.color(20,255,120),2,s:get_pos())
   102         -									s:set_pos(pt)
   103         -									sorcery.vfx.body_sparkle(s,sorcery.lib.color(20,120,255),2)
   104         -								end)
          134  +									};
          135  +								}
   105    136   							end
   106    137   						end
   107    138   					end
   108    139   				end;
   109    140   				frame = {
   110    141   					iridium = {
   111    142   						name = 'Mass Return';
................................................................................
   162    193   	};
   163    194   	disjoin = {
   164    195   		name = 'Disjoin';
   165    196   		tone = {159,235,0};
   166    197   		minpower = 4;
   167    198   		rarity = 20;
   168    199   		amulets = {
   169         -			amethyst = {
          200  +			sapphire = {
   170    201   				name = 'Unsealing';
   171    202   				desc = 'Wielding this amulet, a touch of your hand will unravel even the mightiest protective magics, leaving doors unsealed and walls free to tear down';
          203  +			};
          204  +			amethyst = {
          205  +				name = 'Purging';
          206  +				desc = 'Free yourself from the grip of any malicious spellwork with a snap of your fingers — interrupting all of your own active spells in the process, including impending translocations';
          207  +				cast = function(ctx)
          208  +					local h = ctx.heading.eyeheight * 1.1
          209  +					minetest.add_particlespawner {
          210  +						time = 0.2, amount = math.random(200,250), attached = ctx.caster;
          211  +						glow = 14, texture = sorcery.vfx.glowspark(sorcery.lib.color(156,255,10)):render();
          212  +						minpos = {x = -0.3, y = -0.5, z = -0.3};
          213  +						maxpos = {x =  0.3, y =  h,   z =  0.3};
          214  +						minvel = {x = -1.8, y = -1.8, z = -1.8};
          215  +						maxvel = {x =  1.8, y =  1.8, z =  1.8};
          216  +						minsize = 0.2, maxsize = 5;
          217  +						animation = {
          218  +							type = 'vertical_frames', length = 4.1;
          219  +							aspect_w = 16, aspect_h = 16;
          220  +						};
          221  +						minexptime = 2, maxexptime = 4;
          222  +					}
          223  +					minetest.sound_play('sorcery_disjoin',{object=ctx.caster},true)
          224  +					sorcery.spell.disjoin{target=ctx.caster}
          225  +				end;
   172    226   			};
   173    227   			emerald = {
          228  +				name = 'Disjunction Field';
          229  +				desc = 'Render an area totally opaque to spellwork for a period of time, disrupting any existing spells and preventing further spellcasting therein';
          230  +			};
          231  +			ruby = {
          232  +				name = 'Disjunction';
          233  +				desc = 'Wield this amulet against a spellcaster to disrupt and abort all their spells in progress, perhaps to trap a foe intent on translocating away, or unleash its force upon the victim of a malign hex to free them from its clutches';
          234  +				frame = {
          235  +					iridium = {
          236  +						name = 'Nullification';
          237  +						desc = 'Not only will your victim\'s spells be nullified, but all enchanted objects they carry will be stripped of their power — or possibly even destroyed outright';
          238  +					};
          239  +				};
          240  +			};
          241  +			luxite = {
          242  +				name = 'Disjunctive Aura';
          243  +				desc = 'For a time, all magic undertaken in your vicinity will fail totally';
          244  +				cast = function(ctx)
          245  +					sorcery.spell.cast {
          246  +						caster = ctx.caster, attach = 'caster';
          247  +						disjunction = true, range = 4 + ctx.stats.power;
          248  +						duration = 10 + ctx.stats.power * 3;
          249  +						timeline = {
          250  +							[0] = function(s,_,tl)
          251  +								sparkle(sorcery.lib.color(120,255,30), s,
          252  +									30 * tl, tl, 0.3,1.4, ctx.heading.eyeheight*1.1)
          253  +							end
          254  +						};
          255  +						sounds = {
          256  +							[0] = { sound = 'sorcery_disjoin',   pos = 'caster' };
          257  +							[1] = { sound = 'sorcery_powerdown', pos = 'caster' };
          258  +						};
          259  +					}
          260  +				end
          261  +			};
          262  +			diamond = {
   174    263   				name = 'Mundanity';
   175    264   				desc = 'Strip away the effects of all active potions and spells in your immediate vicinity, leaving adversaries without their magicks to enhance and protect them, and allies free of any curses they may be hobbled by -- and, of course, vice versa';
          265  +				frame = {
          266  +					iridium = {
          267  +						name = 'Spellshatter';
          268  +						desc = 'Blast out a tidal wave of anti-magic that will nullify active spells, but also disenchant or destroy all magical items in range of its violently mundane grip';
          269  +					};
          270  +				};
   176    271   			};
   177    272   		}
   178    273   	};
   179    274   	repulse = {
   180    275   		name = 'Repulse';
   181    276   		tone = {0,180,235};
   182    277   		minpower = 1;
   183    278   		rarity = 7;
   184    279   		amulets = {
   185    280   			amethyst = {
   186    281   				name = 'Hurling';
   187    282   				desc = 'Wielding this amulet, a mere flick of your fingers will lift any target of your choice bodily into the air and press upon them with tremendous repulsive force, throwing them like a hapless ragdoll out of your path';
          283  +				cast = function(ctx)
          284  +					if not (ctx.target and ctx.target.type == 'object') then return false end
          285  +					local tgt = ctx.target.ref
          286  +					local line = vector.subtract(ctx.caster:get_pos(), tgt:get_pos())
          287  +					-- direction vector from target to caster
          288  +					print('line',dump(line))
          289  +					local dir,mag = sorcery.lib.math.vsep(line)
          290  +					if mag > 6 then return false end -- no cheating!
          291  +					local force = 20 + (ctx.stats.power * 2.5)
          292  +					minetest.sound_play('sorcery_hurl',{pos=tgt:get_pos()},true)
          293  +					local immortal = tgt:get_luaentity():get_armor_groups().immortal or 0
          294  +					if minetest.is_player(tgt) or immortal == 0 then
          295  +						tgt:punch(ctx.caster, 1, {
          296  +							full_punch_interval = 1;
          297  +							damage_groups = { fleshy = force / 10 };
          298  +						})
          299  +					end
          300  +					sparktrail(nil,tgt,sorcery.lib.color(101,226,255))
          301  +					if dir.y > 0 then dir.y = 0 end -- spell always lifts
          302  +					dir = vector.add(dir, {x=0,z=0,y=-0.25})
          303  +					local vel = vector.multiply(dir,0-force)
          304  +					tgt:add_velocity(vel)
          305  +				end;
          306  +			};
          307  +			ruby = {
          308  +				name = 'Liftoff';
          309  +				desc = 'Lift yourself high into the air with a blast of violent repulsive force against the ground, and drift down safely to a position of your choice';
          310  +				cast = function(ctx)
          311  +					local power = 14 * (1+(ctx.stats.power * 0.2))
          312  +					minetest.sound_play('sorcery_hurl',{object=ctx.caster},true)
          313  +					sorcery.spell.cast {
          314  +						caster = ctx.caster;
          315  +						subjects = {{player=ctx.caster}};
          316  +						duration = power * 0.25;
          317  +						timeline = {
          318  +							[0] = function(s,_,tl)
          319  +								sparktrail(s.visual_subjects,ctx.caster,sorcery.lib.color(255,252,93))
          320  +								ctx.caster:add_velocity{y=power;x=0,z=0}
          321  +								s.affect {
          322  +									duration = power * 0.25;
          323  +									raise = 2;
          324  +									fall = (power * 0.25) * 0.3;
          325  +									impacts = {
          326  +										gravity = 0.1;
          327  +									};
          328  +								}
          329  +							end;
          330  +						};
          331  +					}
          332  +				end;
   188    333   			};
   189    334   			sapphire = {
   190    335   				name = 'Flinging';
   191    336   				desc = 'Toss an enemy violently into the air, and allow the inevitable impact to do your dirty work for you';
   192    337   			};
   193    338   			emerald = {
   194    339   				name = 'Shockwave';
................................................................................
   212    357   		amulets = {
   213    358   			amethyst = {
   214    359   				name = 'Sapping';
   215    360   				desc = 'Punch a hole in enemy fortifications big enough to slip through but small enough to avoid immediate attention';
   216    361   			};
   217    362   			ruby = {
   218    363   				name = 'Shattering';
   219         -				desc = 'Tear a violent wound in the earth with the destructive force of this amulet';
          364  +				desc = 'Tear a violent wound in the land with the destructive force of this amulet';
   220    365   			};
   221    366   			emerald = {
   222    367   				name = 'Detonate';
   223    368   				desc = 'Wielding this amulet, you can loose an extraordinarily powerful bolt of flame from your fingertips that will explode violently on impact, wreaking total havoc wherever it lands';
   224    369   				cast = function(ctx)
   225    370   					local speed = 40
   226    371   					local radius = math.random(math.floor(ctx.stats.power*0.5),math.ceil(ctx.stats.power))
................................................................................
   233    378   					bolt:set_velocity(vel)
   234    379   				end;
   235    380   			};
   236    381   			luxite = {
   237    382   				name = 'Lethal Aura';
   238    383   				desc = 'For a time, anyone who approaches you, whether friend or foe, will suffer immediate retaliation as they are quickly sapped of their life force';
   239    384   			};
          385  +			mese = {
          386  +				name = 'Cataclysm';
          387  +				desc = 'Use this amulet once to pick a target, then visit devastation upon it from afar with a mere snap of your fingers';
          388  +			};
   240    389   			diamond = {
   241    390   				name = 'Killing';
   242    391   				mingrade = 4;
   243    392   				desc = 'Wield this amulet against a foe to instantly snuff the life out of their mortal form, regardless of their physical protections.';
   244    393   				cast = function(ctx)
   245    394   					if not (ctx.target and ctx.target.type == 'object') then return false end
   246    395   					local tgt = ctx.target.ref

Modified entities.lua from [2261aac8b4] to [9e257faffc].

     3      3   	age = u.marshal.t.u32;
     4      4   	lastemit = u.marshal.t.u32;
     5      5   }
     6      6   
     7      7   minetest.register_entity('sorcery:spell_projectile_flamebolt',{
     8      8   	initial_properties = {
     9      9   		visual = "sprite";
    10         -        -- use_texture_alpha = true;
           10  +        use_texture_alpha = true;
    11     11   		textures = {'sorcery_fireball.png'};
    12         -		groups = {immortal = 1};
    13     12   		visual_size = { x = 2, y = 2, z = 2 };
    14     13   		physical = true;
    15     14   		collide_with_objects = true;
    16     15   		pointable = false;
    17     16   		glow = 14;
    18     17   		static_save = false;
           18  +		shaded = false;
    19     19   	};
           20  +	on_activate = function(self)
           21  +		self.object:set_armor_groups{immortal = 1}
           22  +	end;
    20     23   	on_step = function(self,dtime,collision)
    21     24   		local pos = self.object:get_pos()
    22     25   		if not self._meta then
    23     26   			self._meta = { age = 0; lastemit = 0; emitters = {} }
    24     27   			goto emit
    25     28   		end
    26     29   
    27     30   		self._meta.age = self._meta.age + dtime
    28     31   		if self._meta.age >= 6 then
    29     32   			goto destroy
    30     33   		elseif (self._meta.age - self._meta.lastemit) < 3 then
    31     34   			goto collcheck
    32     35   		end
           36  +
           37  +		-- fireballs dissipate when entering antimagic fields
           38  +		do local probe = sorcery.spell.probe(self.object:get_pos())
           39  +		if probe.disjunction and not self._meta.ignore_disjunction then
           40  +			sorcery.vfx.cast_sparkle(nil,sorcery.lib.color(255,90,10),3,0.3,self.object:get_pos())
           41  +			goto destroy
           42  +		end end
    33     43   		
    34     44   		::emit:: do
    35     45   			self._meta.lastemit = self._meta.age
    36     46   			local spawn = function(num, life_min, life_max, size_min, size_max, gl, speed, img)
    37     47   				table.insert(self._meta.emitters, minetest.add_particlespawner {
    38     48   					amount = num;
    39     49   					minexptime = life_min;

Modified gems.lua from [d9755d7797] to [58885d2bb0].

    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     59   		local useamulet = function(stack,user,target)
    60     60   			local sp = sorcery.amulet.getspell(stack)
    61     61   			if not sp or not sp.cast then return nil end
    62     62   			local stats = sorcery.amulet.stats(stack)
           63  +			local probe = sorcery.spell.probe(user:get_pos())
           64  +			-- amulets don't work in antimagic fields, though some may want to 
           65  +			-- implement this logic themselves (for instance to check a range)
           66  +			if (probe.disjunction and not sp.ignore_disjunction) then return nil end
    63     67   
    64     68   			local ctx = {
    65     69   				caster = user;
    66     70   				target = target;
    67     71   				stats = stats;
    68     72   				amulet = stack;
    69     73   				meta = stack:get_meta(); -- avoid spell boilerplate
    70     74   				color = sorcery.lib.color(sp.tone);
    71     75   				today = minetest.get_day_count();
           76  +				probe = probe;
    72     77   				heading = {
    73     78   					pos   = user:get_pos();
    74     79   					yaw   = user:get_look_dir();
    75     80   					pitch = user:get_look_vertical();
    76     81   					angle = user:get_look_horizontal();
    77     82   					eyeheight = user:get_properties().eye_height;
    78     83   				};

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

    80     80   local data = sorcery.unit('data',nil,'lore')
    81     81   local root = sorcery.unit()
    82     82   sorcery.stage('bootstrap',data,root)
    83     83   
    84     84   data {'ui'}
    85     85   sorcery.unit('lib') {
    86     86   	-- convenience
    87         -	'str';
           87  +	'str', 'math';
    88     88   	-- serialization
    89     89   	'marshal', 'json';
    90     90   	-- data structures
    91     91   	'tbl', 'class';
    92     92   	-- wrappers
    93     93   	'color', 'image', 'ui';
    94     94   	-- game
................................................................................
   118    118   			sorcery.registry.mk(k,v)
   119    119   		end
   120    120   	end
   121    121   end
   122    122   
   123    123   sorcery.stage('startup',data)
   124    124   for _,u in pairs {
   125         -	'vfx'; 'attunement'; 'context'; 'itemclass';
          125  +	'vfx'; 'attunement'; 'context'; 'itemclass'; 'spell';
   126    126   	'potions'; 'metal', 'gems'; 'leylines'; 'infuser';
   127    127   	'altar'; 'wands'; 'tools', 'crafttools'; 'enchanter';
   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';

Modified lib/tbl.lua from [eefda5e589] to [6f943d189b].

    86     86   	local new = fn.copy(r1)
    87     87   	for i=1,#r2 do
    88     88   		new[#new + 1] = r2[i]
    89     89   	end
    90     90   	return new
    91     91   end
    92     92   
    93         -fn.capitalize = function(str)
    94         -	return string.upper(string.sub(str, 1,1)) .. string.sub(str, 2)
    95         -end
    96         -
    97     93   fn.has = function(tbl,value,eqfn)
    98     94   	for k,v in pairs(tbl) do
    99     95   		if eqfn then
   100     96   			if eqfn(v,value,tbl) then return true, k end
   101     97   		else
   102     98   			if value == v then return true, k end
   103     99   		end
................................................................................
   142    138   	table.sort(keys)
   143    139   	return fn.each(keys, function(k,i)
   144    140   		return f(tbl[k],k,i)
   145    141   	end)
   146    142   end
   147    143   
   148    144   fn.iter = function(tbl,fn)
   149         -	for i=1,#tbl do
   150         -		fn(tbl[i], i)
   151         -	end
          145  +	for i,v in ipairs(tbl) do fn(v, i) end
   152    146   end
   153    147   
   154    148   fn.map = function(tbl,fn)
   155    149   	local new = {}
   156    150   	for k,v in pairs(tbl) do
   157    151   		local nv, nk = fn(v, k)
   158    152   		new[nk or k] = nv
................................................................................
   162    156   
   163    157   fn.fold = function(tbl,fn,acc)
   164    158   	if #tbl == 0 then
   165    159   		fn.each_o(tbl, function(v)
   166    160   			acc = fn(acc, v, k)
   167    161   		end)
   168    162   	else
   169         -		for i=0,#tbl do
   170         -			acc = fn(acc,tbl[i],i)
          163  +		for i,v in ipairs(tbl) do
          164  +			acc = fn(acc,v,i)
   171    165   		end
   172    166   	end
   173    167   	return acc
   174    168   end
   175    169   
   176    170   fn.walk = function(tbl,path)
   177    171   	if type(path) == 'table' then

Modified sorcery.md from [246ca8c1c8] to [b729085cc9].

    16     16    * **xdecor** for various tools and ingredients, especially honey and the hammer
    17     17    * **basic_materials** for crafting ingredients
    18     18    * **instant_ores** for ore generation. temporary, will be removed and replaced with home-grown mechanism soon
    19     19    * **farming redo** for potion ingredients
    20     20    * **late** for spell, potion, and gravitator effects
    21     21      * **note**: in order for the gravitator to work, the late condition interval must be lowered from its default of 1.0 to 0.1. this currently can only be done by altering a variable at the top of `late/conditions.lua`, though a note in the source suggests a configuration option will be added eventually. hopefully this is so.
    22     22   
           23  +## libraries
           24  + * **luajit**, because `sorcery`'s code uses modern features not available in the ancient lua version bundled with minetest. alternately, it may be possible to build minetest against a more recent lua version if you're feeling masochistic; luajit will probably be faster tho and has first-party support
           25  +
    23     26   # interoperability
    24     27   sorcery has special functionality to ensure it can cooperate with various other modules, although they are not necessarily required for it to function.
    25     28   
    26     29   ## xdecor
    27     30   by default, `sorcery` disables the xdecor enchanter, since `sorcery` offers its own, much more sophisticated enchantment mechanism. however, the two can coexist if you really want; a configuration flag can be used to prevent `sorcery` disabling the xdecor enchanter.
    28     31   
    29     32   ## hopper

Modified vfx.lua from [343a5ccf55] to [aaa85eb70f].

     1      1   sorcery.vfx = {}
            2  +
            3  +sorcery.vfx.glowspark = function(color)
            4  +	local spark = sorcery.lib.image('sorcery_spark.png')
            5  +	return spark:blit(spark:multiply(color))
            6  +end
     2      7   
     3      8   sorcery.vfx.cast_sparkle = function(caster,color,strength,duration,pos)
     4      9   	local ofs = pos
     5     10   		and function(x) return vector.add(pos,x) end
     6     11   		or  function(x) return x end
     7     12   	local height = caster:get_properties().eye_height
     8     13   	minetest.add_particlespawner {

Modified wands.lua from [b35b5eeee9] to [e661ef77a3].

   207    207   end
   208    208   
   209    209   local wand_cast = function(stack, user, target)
   210    210   	local meta = stack:get_meta()
   211    211   	local wand = sorcery.wands.util.getproto(stack)
   212    212   	if meta:contains('sorcery_wand_spell') == false then return nil end
   213    213   	local spell = meta:get_string('sorcery_wand_spell')
   214         -	local castfn = sorcery.data.spells[spell].cast
          214  +	local spelldata = sorcery.data.spells[spell]
          215  +
          216  +	-- wands don't work in anti-magic fields
          217  +	local probe = sorcery.spell.probe(user:get_pos())
          218  +	if probe.disjunction and not spelldata.ignore_disjunction then return nil end
          219  +
          220  +	local castfn = spelldata.cast
   215    221   	if castfn == nil then return nil end
   216    222   	local matprops = sorcery.wands.util.matprops(wand)
   217    223   	if matprops.bond then
   218    224   		local userct, found = 0, false
   219    225   		for i=1,matprops.bond do
   220    226   			local prop = 'bound_user_' .. tostring(i)
   221    227   			if meta:contains(prop) then
................................................................................
   253    259   	local context = {
   254    260   		base = wand;
   255    261   		stats = matprops;
   256    262   		meta = meta;
   257    263   		item = stack;
   258    264   		caster = user;
   259    265   		target = target;
          266  +		probe = probe;
   260    267   		today = minetest.get_day_count();
   261    268   		heading = {
   262    269   			pos   = user:get_pos();
   263    270   			yaw   = user:get_look_dir();
   264    271   			pitch = user:get_look_vertical();
   265    272   			angle = user:get_look_horizontal();
   266    273   			eyeheight = uprops.eye_height;