sorcery  Check-in [147592b8e9]

Overview
Comment: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
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 147592b8e9972e05b003073cb2fde9b6fd622dc971c92ef0ff14ae6e00168675
User & Date: lexi on 2020-10-26 03:58:08
Other Links: manifest | tags
Context
2020-10-30
18:47
add duplicate and elevate spells, add more sfx, various tweaks and bugfixes, add object handle class check-in: 6e106c135c user: lexi tags: trunk
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
Changes

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

     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 };
            7  +local sparkle_region = function(s)
            8  +	s.spell.visual_subjects {
            9  +		amount = s.amt, time = s.time, -- attached = s;
           10  +		minpos = s.minpos;
           11  +		maxpos = s.maxpos;
    12     12   		minvel = { x = -0.4, y = -0.2, z = -0.4 };
    13     13   		maxvel = { x =  0.4, y =  0.2, z =  0.4 };
    14     14   		minacc = { x = -0.5, y = -0.4, z = -0.5 };
    15     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();
           16  +		minexptime = 1.0*(s.length or 1), maxexptime = 2.0 * (s.length or 1);
           17  +		minsize = s.minsize, maxsize = s.maxsize, glow = 14;
           18  +		texture = (s.img or sorcery.vfx.glowspark(s.color)):render();
    19     19   		animation = {
    20     20   			type = 'vertical_frames';
    21     21   			aspect_w = 16, aspect_h = 16;
           22  +			length = 0.1 + (s.length or 1)*2;
    22     23   		};
    23     24   	}
    24     25   end
    25         -local sparktrail = function(fn,tgt,color)
           26  +local sparkle = function(color, spell, amt,time,minsize,maxsize,sh)
           27  +	sparkle_region { spell = spell;
           28  +		amt = amt, time = time, color = color;
           29  +		minsize = minsize, maxsize = maxsize;
           30  +		minpos = { x = -0.3, y = -0.5, z = -0.3 };
           31  +		maxpos = { x =  0.3, y = sh*1.1, z = 0.3 };
           32  +	}
           33  +end
           34  +local sparktrail = function(fn,tgt,color,time)
    26     35   	return (fn or minetest.add_particlespawner)({
    27         -		amount = 240, time = 1, attached = tgt;
           36  +		amount = 240, time = time or 1, attached = tgt;
    28     37   		minpos = {x = -0.4, y = -0.5, z = -0.4};
    29     38   		maxpos = {x =  0.4, y = tgt:get_properties().eye_height or 0.5, z =  0.4};
    30     39   		minacc = {x =  0.0, y = 0.05, z =  0.0};
    31     40   		maxacc = {x =  0.0, y = 0.15, z =  0.0};
    32     41   		minexptime = 1.5, maxexptime = 5;
    33     42   		minsize = 0.5, maxsize = 2.6, glow = 14;
    34     43   		texture = sorcery.vfx.glowspark(color):render();
................................................................................
    39     48   	});
    40     49   end
    41     50   return {
    42     51   	translocate = {
    43     52   		name = 'Translocate';
    44     53   		tone = {0,235,233};
    45     54   		minpower = 3;
    46         -		rarity = 15;
           55  +		rarity = 10;
    47     56   		amulets = {
    48     57   			amethyst = {
    49     58   				name = 'Joining';
    50     59   				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';
    51     60   				apply = function(ctx)
    52     61   					local maker = ctx.user:get_player_name()
    53     62   					ctx.meta:set_string('rune_join_target',maker)
................................................................................
    82     91   					else
    83     92   						local pos = minetest.string_to_pos(ctx.meta:get_string('rune_return_dest'))
    84     93   						ctx.meta:set_string('rune_return_dest','')
    85     94   						local subjects = { ctx.caster }
    86     95   						local center = ctx.caster:get_pos()
    87     96   						ctx.sparkle = false
    88     97   						local delay = math.max(3,10 - ctx.stats.power) + 3*(math.random()*2-1)
           98  +						if ctx.amulet.frame == 'tungsten' then delay = delay * 0.5 end
    89     99   						for _,s in pairs(subjects) do
    90    100   							local offset = vector.subtract(s:get_pos(), center)
    91    101   							local pt = sorcery.lib.node.get_arrival_point(vector.add(pos,offset))
    92    102   							if pt then
    93    103   								-- minetest.sound_play('sorcery_stutter', {
    94    104   								-- 	object = s, gain = 0.8;
    95    105   								-- },true)
................................................................................
   123    133   											minetest.sound_play('sorcery_zap', { pos = s:get_pos(), gain = 0.4 },true)
   124    134   											sorcery.vfx.body_sparkle(nil,sorcery.lib.color(20,255,120),2,s:get_pos())
   125    135   											s:set_pos(pt)
   126    136   											sorcery.vfx.body_sparkle(s,sorcery.lib.color(20,120,255),2)
   127    137   										end;
   128    138   									};
   129    139   									sounds = {
   130         -										[0] = {
   131         -											pos = 'subjects';
   132         -											sound = 'sorcery_stutter';
   133         -										};
          140  +										[0] = { sound = 'sorcery_stutter', pos = 'subjects' };
   134    141   									};
   135    142   								}
   136    143   							end
   137    144   						end
   138    145   					end
   139    146   				end;
   140    147   				frame = {
          148  +					tungsten = {
          149  +						name = 'Quick Return';
          150  +						desc = 'Use this amulet once to bind it to a particular place, then discharge its spell to translocate yourself rapidly back to that point from anywhere in the world.';
          151  +					};
   141    152   					iridium = {
   142    153   						name = 'Mass Return';
   143    154   						desc = 'Use this amulet once to bind it to a particular place, then carry yourself and everyone around you back to that point in a flash simply by using it again';
   144    155   					};
   145    156   				};
   146    157   			};
   147    158   			emerald = {
................................................................................
   191    202   			};
   192    203   		};
   193    204   	};
   194    205   	disjoin = {
   195    206   		name = 'Disjoin';
   196    207   		tone = {159,235,0};
   197    208   		minpower = 4;
   198         -		rarity = 20;
          209  +		rarity = 40;
   199    210   		amulets = {
   200    211   			sapphire = {
   201    212   				name = 'Unsealing';
   202    213   				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    214   			};
   204    215   			amethyst = {
   205    216   				name = 'Purging';
................................................................................
   236    247   						name = 'Nullification';
   237    248   						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    249   					};
   239    250   				};
   240    251   			};
   241    252   			luxite = {
   242    253   				name = 'Disjunctive Aura';
   243         -				desc = 'For a time, all magic undertaken in your vicinity will fail totally';
          254  +				desc = 'For a time, all magic undertaken in your vicinity will fail totally — including your own';
   244    255   				cast = function(ctx)
          256  +					local h = ctx.heading.eyeheight*1.1
   245    257   					sorcery.spell.cast {
   246    258   						caster = ctx.caster, attach = 'caster';
          259  +						subjects = {{player=ctx.caster}};
   247    260   						disjunction = true, range = 4 + ctx.stats.power;
   248    261   						duration = 10 + ctx.stats.power * 3;
   249    262   						timeline = {
   250    263   							[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
          264  +								local ttns = 0.8
          265  +								local vel = s.range / ttns
          266  +								s.visual_caster {
          267  +									amount = 300, time = ttns, glow = 14;
          268  +									texture = sorcery.lib.image('sorcery_sputter.png'):glow(sorcery.lib.color(160,255,80)):render();
          269  +									minpos = { x = -0.0, y = h*0.5,z = -0.0 };
          270  +									maxpos = { x =  0.0, y = h*0.5,z =  0.0 };
          271  +									minvel = { x = -vel, y = -0.0, z = -vel };
          272  +									maxvel = { x =  vel, y =  0.0, z =  vel };
          273  +									minacc = { x = -0.2, y = -0.0, z = -0.2 };
          274  +									maxacc = { x =  0.2, y =  0.0, z =  0.2 };
          275  +									minexptime = ttns, maxexptime = ttns * 2;
          276  +									minsize = 0.2, maxsize = 4.5;
          277  +									animation = {
          278  +										type = 'vertical_frames', length = 0.1 + ttns*2;
          279  +										aspect_w = 16, aspect_h = 16;
          280  +									}
          281  +								}
          282  +							end;
          283  +							[{whence=0,secs=0.8}] = function(s,te,tl)
          284  +								local range = s.range
          285  +								sparkle_region {
          286  +									spell = s, amt = 150*tl, time = tl;
          287  +									minsize = 1, maxsize = 8.4;
          288  +									minpos = { x = 0-range, y = -0.5, z = 0-range };
          289  +									maxpos = { x =   range, y = h,    z =   range };
          290  +									img = sorcery.lib.image('sorcery_flicker.png'):glow(sorcery.lib.color(120,255,30));
          291  +								}
          292  +							end;
   254    293   						};
   255    294   						sounds = {
   256         -							[0] = { sound = 'sorcery_disjoin',   pos = 'caster' };
   257         -							[1] = { sound = 'sorcery_powerdown', pos = 'caster' };
          295  +							[0.00] = {sound='sorcery_disjoin',   where='caster'};
          296  +							[{whence=0,secs=0.8}] = {
          297  +								sound='sorcery_disjoin_bg', where='subjects';
          298  +								gain=0.5, stop = {whence=1,secs=-1.5}
          299  +							};
          300  +							[1.00] = {sound='sorcery_powerdown', where='caster'};
   258    301   						};
   259    302   					}
   260    303   				end
   261    304   			};
   262    305   			diamond = {
   263    306   				name = 'Mundanity';
   264    307   				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';
................................................................................
   271    314   			};
   272    315   		}
   273    316   	};
   274    317   	repulse = {
   275    318   		name = 'Repulse';
   276    319   		tone = {0,180,235};
   277    320   		minpower = 1;
   278         -		rarity = 7;
          321  +		rarity = 5;
   279    322   		amulets = {
   280    323   			amethyst = {
   281    324   				name = 'Hurling';
   282    325   				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    326   				cast = function(ctx)
   284    327   					if not (ctx.target and ctx.target.type == 'object') then return false end
   285    328   					local tgt = ctx.target.ref
   286    329   					local line = vector.subtract(ctx.caster:get_pos(), tgt:get_pos())
   287    330   					-- direction vector from target to caster
   288         -					print('line',dump(line))
   289    331   					local dir,mag = sorcery.lib.math.vsep(line)
   290    332   					if mag > 6 then return false end -- no cheating!
   291    333   					local force = 20 + (ctx.stats.power * 2.5)
   292    334   					minetest.sound_play('sorcery_hurl',{pos=tgt:get_pos()},true)
   293    335   					local immortal = tgt:get_luaentity():get_armor_groups().immortal or 0
   294    336   					if minetest.is_player(tgt) or immortal == 0 then
   295    337   						tgt:punch(ctx.caster, 1, {
................................................................................
   309    351   				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    352   				cast = function(ctx)
   311    353   					local power = 14 * (1+(ctx.stats.power * 0.2))
   312    354   					minetest.sound_play('sorcery_hurl',{object=ctx.caster},true)
   313    355   					sorcery.spell.cast {
   314    356   						caster = ctx.caster;
   315    357   						subjects = {{player=ctx.caster}};
   316         -						duration = power * 0.25;
          358  +						duration = power * 0.30;
   317    359   						timeline = {
   318    360   							[0] = function(s,_,tl)
   319    361   								sparktrail(s.visual_subjects,ctx.caster,sorcery.lib.color(255,252,93))
   320    362   								ctx.caster:add_velocity{y=power;x=0,z=0}
   321    363   								s.affect {
   322         -									duration = power * 0.25;
          364  +									duration = power * 0.50;
   323    365   									raise = 2;
   324         -									fall = (power * 0.25) * 0.3;
          366  +									-- fall = (power * 0.25) * 0.3;
   325    367   									impacts = {
   326    368   										gravity = 0.1;
   327    369   									};
   328    370   								}
   329    371   							end;
          372  +						};
          373  +						intervals = {
          374  +							{period = 0.2, after = {whence=0, secs=2}; fn = function(c)
          375  +							-- return gravity to normal once they touch down
          376  +								for si,sub in pairs(c.spell.subjects) do
          377  +									local p = sub.player:get_pos()
          378  +									for i=1,3 do
          379  +										local sum = vector.offset(p,0,-i,0)
          380  +										if not sorcery.lib.node.is_air(sum) then
          381  +											c.spell.release_subject(si)
          382  +											if #c.spell.subjects == 0 then
          383  +												return false
          384  +											end
          385  +											break
          386  +										end
          387  +									end
          388  +								end
          389  +							end};
   330    390   						};
   331    391   					}
   332    392   				end;
   333    393   			};
   334    394   			sapphire = {
   335    395   				name = 'Flinging';
   336    396   				desc = 'Toss an enemy violently into the air, and allow the inevitable impact to do your dirty work for you';
          397  +				cast = function(ctx)
          398  +					if not (ctx.target and ctx.target.type == 'object') then return false end
          399  +					local tgt = ctx.target.ref
          400  +					local power = 16 * (1+(ctx.stats.power * 0.2))
          401  +					minetest.sound_play('sorcery_hurl',{object=ctx.caster},true)
          402  +					sorcery.spell.cast {
          403  +						caster = ctx.caster;
          404  +						subjects = {{player=tgt}};
          405  +						duration = 4;
          406  +						timeline = {
          407  +							[0] = function(s,_,tl)
          408  +								for _,sub in pairs(s.subjects) do
          409  +									local height = (sub.player:get_properties().eye_height or 1)*1.3
          410  +									local scenter = vector.add(sub.player:get_pos(), {x=0,y=height/2,z=0})
          411  +									for i=1,math.random(64,128) do
          412  +										local high = (height+0.8)*math.random() - 0.8
          413  +										local far = (high >= -0.5 and high <= height) and
          414  +											(math.random() * 0.3 + 0.4) or
          415  +											(math.random() * 0.5)
          416  +										local yaw = {x=0, y = math.random()*100, z=0}
          417  +										local po = vector.rotate({x=far,y=high,z=0}, yaw)
          418  +										local ppos = vector.add(po,sub.player:get_pos())
          419  +										local dir = vector.direction(ppos,scenter)
          420  +										local vel = math.random() * 0.8 + 0.4
          421  +										minetest.add_particle {
          422  +											pos = ppos;
          423  +											velocity = vector.multiply(dir,vel);
          424  +											expirationtime = far / vel;
          425  +											size = math.random()*2.4 + 0.6;
          426  +											texture = sorcery.lib.image('sorcery_sputter.png'):glow(sorcery.lib.color{
          427  +												hue = math.random(41,63);
          428  +												saturation = 100;
          429  +												luminosity = 0.5 + math.random()*0.3;
          430  +											}):render();
          431  +											glow = 14;
          432  +											animation = {
          433  +												type = 'vertical_frames', length = far/vel;
          434  +												aspect_w = 16, aspect_h = 16;
          435  +											};
          436  +										}
          437  +									end
          438  +								end
          439  +							end;
          440  +							[0.3] = function(s,te,tl)
          441  +								sparktrail(s.visual_subjects,ctx.caster,sorcery.lib.color(255,252,93))
          442  +								for _,sub in pairs(s.subjects) do
          443  +									sub.player:add_velocity{y=power;x=0,z=0}
          444  +								end
          445  +							end;
          446  +							[1] = (ctx.amulet.frame == 'cobalt') and function(s,te,tl)
          447  +								-- TODO add visuals
          448  +								for _,sub in pairs(s.subjects) do
          449  +									sub.player:add_velocity{y=-power*2;x=0,z=0}
          450  +								end
          451  +							end or nil;
          452  +						};
          453  +						sounds = {
          454  +							[0.3] = {
          455  +								sound = 'sorcery_hurl';
          456  +								where = 'subjects';
          457  +								ephemeral = true;
          458  +							};
          459  +							[1] = (ctx.amulet.frame == 'cobalt') and {
          460  +								sound = 'sorcery_hurl';
          461  +								where = 'subjects';
          462  +								ephemeral = true;
          463  +							} or nil;
          464  +						};
          465  +					};
          466  +				end;
          467  +				frame = {
          468  +					cobalt = {
          469  +						name = 'Crushing';
          470  +						desc = 'Toss an enemy violently into the air, then bring them crashing down to earth with bone-shattering force';
          471  +					};
          472  +					iridium = {
          473  +						name = 'Mass Flinging';
          474  +						desc = 'Send everyone around you hurtling into the sky, and allow the inevitable impact to do your dirty work for you';
          475  +					};
          476  +				};
   337    477   			};
   338    478   			emerald = {
   339    479   				name = 'Shockwave';
   340    480   				desc = 'Let loose a stream of concussive force that slams into everything in your path and sends them hurtling away from you';
   341    481   			};
   342    482   			luxite = {
   343    483   				name = 'Repulsive Aura';
................................................................................
   349    489   			};
   350    490   		};
   351    491   	};
   352    492   	obliterate = {
   353    493   		name = 'Obliterate';
   354    494   		tone = {255,0,10};
   355    495   		minpower = 5;
   356         -		rarity = 30;
          496  +		rarity = 35;
   357    497   		amulets = {
   358    498   			amethyst = {
   359    499   				name = 'Sapping';
   360    500   				desc = 'Punch a hole in enemy fortifications big enough to slip through but small enough to avoid immediate attention';
   361    501   			};
   362    502   			ruby = {
   363    503   				name = 'Shattering';
................................................................................
   375    515   					local bolt = minetest.add_entity(vector.add(heading.pos,vector.multiply(heading.yaw,2.5)),'sorcery:spell_projectile_flamebolt')
   376    516   					bolt:set_rotation(heading.yaw)
   377    517   					bolt:get_luaentity()._blastradius = radius
   378    518   					bolt:set_velocity(vel)
   379    519   				end;
   380    520   			};
   381    521   			luxite = {
   382         -				name = 'Lethal Aura';
   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';
          522  +				name = 'Cataclysmic Aura';
          523  +				desc = 'A storm of destructive force rages about you as you stand untouched, the master of its voracious dark energies';
   384    524   			};
   385    525   			mese = {
   386    526   				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';
          527  +				desc = 'Use this amulet once to pick a target, then visit devastation upon it from afar whenever you so will with a mere snap of your fingers';
   388    528   			};
   389    529   			diamond = {
   390    530   				name = 'Killing';
   391    531   				mingrade = 4;
   392    532   				desc = 'Wield this amulet against a foe to instantly snuff the life out of their mortal form, regardless of their physical protections.';
   393    533   				cast = function(ctx)
   394    534   					if not (ctx.target and ctx.target.type == 'object') then return false end
................................................................................
   408    548   			};
   409    549   		};
   410    550   	};
   411    551   	excavate = {
   412    552   		name = 'Excavate';
   413    553   		tone = {0,68,235};
   414    554   		minpower = 3;
   415         -		rarity = 60;
          555  +		rarity = 30;
   416    556   		amulets = {
          557  +			luxite = {
          558  +				name = 'Stonestride';
          559  +				desc = 'Rock walls will open up before you when you brandish this amulet before them, closing up again behind you without leaving a trace of your passage';
          560  +			};
   417    561   			sapphire = {
   418    562   				name = 'Tunnelling';
   419         -				desc = 'Carve a long tunnel ahead of you into the rock';
          563  +				desc = 'Carve a long tunnel ahead of you into the rock and dirt';
          564  +				cast = function(ctx)
          565  +					if ctx.target.type ~= 'node' then return false end
          566  +					local allowed = {
          567  +						['default:stone'] = true;
          568  +						['default:desert_stone'] = true;
          569  +						['default:dirt'] = true;
          570  +						['default:gravel'] = true;
          571  +					}
          572  +					if allowed[minetest.get_node(ctx.target.under).name] ~= true then
          573  +						return false
          574  +					end
          575  +					local timeline,sounds = {}, {}
          576  +					local tunnel_depth = math.random(5,9) * ctx.stats.power
          577  +					local cname = ctx.caster:get_player_name()
          578  +					local cut = function(step,s,te,tl)
          579  +						local smash = function(pos)
          580  +							if not allowed[minetest.get_node(pos).name] then return end
          581  +							if minetest.is_protected(pos, cname) then return end
          582  +							s.visual {
          583  +								amount = math.random(32,48), time = 0.2, glow = 14;
          584  +								texture = sorcery.lib.image('sorcery_spark.png'):glow(sorcery.lib.color(10,20,255)):render();
          585  +								minpos = vector.subtract(pos, {x=0.5,y=0.5,z=0.5});
          586  +								maxpos = vector.add     (pos, {x=0.5,y=0.5,z=0.5});
          587  +								minvel = {x = -0.3, y = -0.3, z = -0.3};
          588  +								maxvel = {x =  0.3, y =  0.3, z =  0.3};
          589  +								minacc = {x = -0.6, y = -0.6, z = -0.6};
          590  +								maxacc = {x =  0.6, y =  0.6, z =  0.6};
          591  +								minexptime = 0.4, maxexptime = 1.2;
          592  +								minsize = 0.3, maxsize = 1.2;
          593  +								animation = {
          594  +									type = 'vertical_frames', length = 1.3;
          595  +									aspect_w = 16, aspect_h = 16;
          596  +								};
          597  +							}
          598  +							minetest.dig_node(pos)
          599  +							if math.random(5) == 1 then
          600  +								minetest.set_node(pos, {name='sorcery:air_flash_' .. tostring(math.random(10))})
          601  +							end
          602  +							-- TODO visuals
          603  +						end
          604  +						local r = s.tunnel_radius
          605  +						local yaw = {x=0,y=s.tunnel_angle,z=0}
          606  +						s.visual {
          607  +							amount = 16, time = 3, glow = 14;
          608  +							texture = sorcery.lib.image('sorcery_sparking.png'):glow(sorcery.lib.color(20,60,255)):render();
          609  +							minpos = vector.subtract(s.anchor, {x=r,y=r,z=r});
          610  +							maxpos = vector.add     (s.anchor, {x=r,y=r,z=r});
          611  +							minvel = {x = -0.1, y = -0.1, z = -0.1};
          612  +							maxvel = {x =  0.1, y =  0.1, z =  0.1};
          613  +							minexptime = 1.0, maxexptime = 1.4;
          614  +							minsize = 1.5, maxsize = 4;
          615  +							animation = {
          616  +								type = 'vertical_frames', length = 1.5;
          617  +								aspect_w = 64, aspect_h = 64;
          618  +							};
          619  +						}
          620  +						for x=-r,r do for y=-r,r do
          621  +							local xs = x < 0 and -1 or 1
          622  +							local ys = y < 0 and -1 or 1
          623  +							if x^2 + y^2 <= r^2 then
          624  +								if (x+xs)^2 + y^2 > r^2 or
          625  +								   (y+ys)^2 + x^2 > r^2 then
          626  +								   -- we're right at the edge - make a mess
          627  +								   if math.random(5) == 1 then goto skip end
          628  +								end
          629  +								local p = vector.add(s.anchor,vector.rotate({x=x,y=y,z=0},yaw))
          630  +								smash(p)
          631  +							end
          632  +						::skip::end end
          633  +						-- if math.random(1,10) == 1 then
          634  +						-- 	s.tunnel_angle = s.tunnel_angle + math.random(-0.05,0.05)
          635  +						-- 	yaw.y = s.tunnel_angle
          636  +						-- end
          637  +						if math.random(1,21) == 1 then
          638  +							s.tunnel_radius = math.min(6,math.max(3,s.tunnel_radius + math.random(-1,1)))
          639  +						end
          640  +						local dir = vector.rotate({x=0,y=0,z=1},yaw)
          641  +						if sorcery.lib.math.vdcomp(1, dir) < 1 then
          642  +							dir = vector.normalize(dir)
          643  +						end
          644  +						s.anchor = vector.add(s.anchor,dir)
          645  +					end
          646  +					local tp = 0
          647  +					for i=1,tunnel_depth do
          648  +						local now = {whence=0,secs=tp}
          649  +						timeline[now] = function(...) cut(i,...) end
          650  +						sounds[now] = {
          651  +							sound='sorcery_crunch', where='pos';
          652  +							ephemeral=true, gain = math.random(3,10) * 0.1;
          653  +						}
          654  +						tp = tp + (math.random(2,5) * 0.1)
          655  +					end
          656  +					sounds[1] = {sound='sorcery_powerdown', where='pos'}
          657  +					sorcery.spell.cast {
          658  +						caster = ctx.caster;
          659  +						duration = tp;
          660  +						timeline = timeline, sounds = sounds;
          661  +						-- spell state
          662  +						anchor = ctx.target.under;
          663  +						tunnel_angle = ctx.caster:get_look_horizontal();
          664  +						tunnel_radius = math.floor(math.random(3,5) * (ctx.stats.power * 0.1));
          665  +					}
          666  +				end;
   420    667   			};
   421    668   			emerald = {
   422    669   				name = 'Boring';
   423    670   				desc = 'Release the force of this amulet to punch a deep borehole down into the earth below';
   424         -			}
          671  +			};
          672  +			amethyst = {
          673  +				name = 'Shaftcutting';
          674  +				desc = 'Cut a wide shaft up into the ceiling of a cavern';
          675  +			};
   425    676   		};
   426    677   	};
   427    678   	genesis = {
   428    679   		name = 'Genesis';
   429    680   		tone = {235,0,175};
   430    681   		minpower = 5;
   431         -		rarity = 50;
          682  +		rarity = 25;
   432    683   		amulets = {
   433    684   			mese = {
          685  +				mingrade = 4;
   434    686   				name = 'Duplication';
   435    687   				desc = 'Generate a copy of any object or item, no matter how common or rare';
   436    688   			};
   437    689   		};
   438    690   	};
   439    691   	luminate = {
   440    692   		name = 'Luminate';
   441    693   		tone = {255,194,0};
   442    694   		minpower = 1;
   443         -		rarity = 25;
          695  +		rarity = 5;
   444    696   		amulets = {
   445    697   			luxite = {
   446    698   				name = 'Glow';
   447    699   				desc = 'Swathe yourself in an aura of sparkling radiance, casting light upon all the dark places where you voyage';
   448    700   			};
   449    701   			diamond = {
   450    702   				name = 'Radiance';
................................................................................
   459    711   			};
   460    712   		};
   461    713   	};
   462    714   	dominate = {
   463    715   		name = 'Dominate';
   464    716   		tone = {235,0,228};
   465    717   		minpower = 4;
   466         -		rarity = 40;
          718  +		rarity = 20;
   467    719   		amulets = {
   468    720   			amethyst = {
   469    721   				name = 'Suffocation';
   470    722   				desc = 'Wrap this spell tightly around your victim\'s throat, cutting off their oxygen until you release them.';
   471    723   			};
   472    724   			emerald = {
   473    725   				name = 'Caging';
   474    726   				desc = 'Trap your victim in an impenetrable field of force, leaving them with no way out but translocation or waiting for the field to release them';
   475    727   			};
          728  +			luxite = {
          729  +				name = 'Vampiric Aura';
          730  +				desc = 'For a time, anyone who approaches you, whether friend or foe, will suffer immediate retaliation as they are quickly sapped of their vital force in order to replenish your own';
          731  +			};
   476    732   			ruby = {
   477    733   				name = 'Exsanguination';
   478    734   				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 being';
   479    735   				cast = function(ctx)
   480    736   					if not (ctx.target and ctx.target.type == 'object') then return false end
   481    737   					local tgt = ctx.target.ref
   482    738   					local takefac = math.min(99,50 + (ctx.stats.power * 5)) / 100
   483    739   					local dmg = tgt:get_hp() * takefac
   484         -					print("!!! dmg calc",takefac,dmg,tgt:get_hp())
   485    740   
   486    741   					local numhits = math.random(6,10+ctx.stats.power/2)
   487    742   					local function dohit(hitsleft)
   488    743   						if tgt == nil or tgt:get_properties() == nil then return end
   489    744   						tgt:punch(ctx.caster, 1, {
   490    745   							full_punch_interval = 1;
   491    746   							damage_groups = { fleshy = dmg / numhits }

Modified displacer.lua from [e06af858b3] to [ec3123df49].

   120    120   	on_metadata_inventory_put = function(pos)
   121    121   		minetest.get_node_timer(pos):start(1)
   122    122   	end;
   123    123   	on_timer = function(pos,delta)
   124    124   		local meta = minetest.get_meta(pos)
   125    125   		if not meta:contains('active-device') then return false end
   126    126   
          127  +		local probe = sorcery.spell.probe(pos)
          128  +		if probe.disjunction then return true end
          129  +
   127    130   		local inv = meta:get_inventory()
   128    131   		if inv:is_empty('cache') then return false end
   129    132   
   130    133   		local dev = gettxr(pos)
   131    134   		local active = minetest.string_to_pos(meta:get_string('active-device'))
   132    135   
   133    136   		local ad
................................................................................
   143    146   		if ad.partner then
   144    147   			remote = gettxr(ad.partner)
   145    148   		elseif ad.code then
   146    149   			local net = sorcery.farcaster.junction(pos,constants.xmit_wattage)
   147    150   			for _,n in pairs(net) do
   148    151   				for _,d in pairs(n.caps.net.devices.consume) do
   149    152   					if d.id == 'sorcery:displacer' then
   150         -						local t = gettxr(d.pos)
   151         -						for _,d in pairs(t.connections) do
   152         -							if d.mode == 'receive' and d.code then
   153         -								local match = true
   154         -								for i=1,#d.code do
   155         -									if d.code[i] ~= ad.code[i] then
   156         -										match = false break
          153  +						local dp = sorcery.spell.probe(d.pos)
          154  +						if not dp.disjunction then
          155  +							local t = gettxr(d.pos)
          156  +							for _,d in pairs(t.connections) do
          157  +								if d.mode == 'receive' and d.code then
          158  +									local match = true
          159  +									for i=1,#d.code do
          160  +										if d.code[i] ~= ad.code[i] then
          161  +											match = false break
          162  +										end
   157    163   									end
   158         -								end
   159         -								if match then
   160         -									remote = t
   161         -									break
          164  +									if match then
          165  +										remote = t
          166  +										break
          167  +									end
   162    168   								end
   163    169   							end
   164    170   						end
   165    171   					end
   166    172   					if remote then break end
   167    173   				end
   168    174   				if remote then break end

Modified enchanter.lua from [b41a4d81d6] to [5ad2e81269].

   400    400   	if puncher == nil then return end -- i don't know why
   401    401   	-- this is necessary but you get rare crashes without it
   402    402   
   403    403   	-- perform leyline checks and call notify if necessary
   404    404   	if minetest.get_item_group(node.name, 'sorcery_ley_device') ~= 0 then
   405    405   		sorcery.lib.node.notifyneighbors(pos)
   406    406   	end
          407  +
          408  +	-- is there an active disjunction in effect here?
          409  +	-- if so, return immediately and perform no magic
          410  +	local probe = sorcery.spell.probe(pos)
          411  +	if probe.disjunction then return end
   407    412   
   408    413   	-- we're goint to do something VERY evil here and
   409    414   	-- replace the air with a "glow-air" that removes
   410    415   	-- itself after a short period of time, to create
   411    416   	-- a flash of light when an enchanted tool's used
   412    417   	-- to dig out a node
   413    418   	local tool = puncher:get_wielded_item()

Modified forcefield.lua from [a330742505] to [2ecc37ddb4].

   123    123   	};
   124    124   	on_construct = function(pos)
   125    125   		minetest.get_node_timer(pos):start(1)
   126    126   	end;
   127    127   	on_timer = function(pos,delta)
   128    128   		local orientation = math.floor(minetest.get_node(pos).param2 / 4)
   129    129   		local costs = calc_cost(pos,delta)
          130  +		local probe = sorcery.spell.probe(pos)
          131  +		if probe.disjunction then return true end
   130    132   		local l = sorcery.ley.netcaps(pos,delta)
   131    133   		if l.self.powerdraw >= costs.mincost then
   132    134   			local dist = l.self.powerdraw / (constants.cost_per_barrier * delta)
   133    135   			for i=1,math.floor(dist) do
   134    136   				local t = costs.targets[i]
   135    137   				local str = math.min(0xFF,t[2] + 50*delta);
   136         -				minetest.swap_node(t[1], {
   137         -					name = 'sorcery:air_barrier_' .. math.max(1, math.floor(10*(str/0xFF)));
   138         -					param2 = str;
   139         -				})
   140         -				minetest.get_node_timer(t[1]):start(1)
          138  +				local fprobe = sorcery.spell.probe(t[1])
          139  +				if not fprobe.disjunction then
          140  +					minetest.swap_node(t[1], {
          141  +						name = 'sorcery:air_barrier_' .. math.max(1, math.floor(10*(str/0xFF)));
          142  +						param2 = str;
          143  +					})
          144  +					minetest.get_node_timer(t[1]):start(1)
          145  +				end
   141    146   			end
   142    147   
   143    148   			local pn = vector.add(pos, vector.divide(costs.aim,2));
   144    149   			local pp = vector.add(pn, pofstbl[orientation])
   145    150   			pn = vector.subtract(pn,  pofstbl[orientation])
   146    151   			
   147    152   			minetest.add_particlespawner {

Modified gems.lua from [58885d2bb0] to [42c4d86138].

    65     65   			-- implement this logic themselves (for instance to check a range)
    66     66   			if (probe.disjunction and not sp.ignore_disjunction) then return nil end
    67     67   
    68     68   			local ctx = {
    69     69   				caster = user;
    70     70   				target = target;
    71     71   				stats = stats;
    72         -				amulet = stack;
           72  +				wield = stack;
           73  +				amulet = stack:get_definition()._sorcery.amulet;
    73     74   				meta = stack:get_meta(); -- avoid spell boilerplate
    74     75   				color = sorcery.lib.color(sp.tone);
    75     76   				today = minetest.get_day_count();
    76     77   				probe = probe;
    77     78   				heading = {
    78     79   					pos   = user:get_pos();
    79     80   					yaw   = user:get_look_dir();
................................................................................
    81     82   					angle = user:get_look_horizontal();
    82     83   					eyeheight = user:get_properties().eye_height;
    83     84   				};
    84     85   
    85     86   				sound = "xdecor_enchanting"; --FIXME make own sounds
    86     87   				sparkle = true;
    87     88   			}
    88         -			print('casting')
    89     89   			local res = sp.cast(ctx)
    90     90   
    91     91   			if res == nil or res == true then
    92     92   				minetest.sound_play(ctx.sound, { 
    93     93   					pos = user:get_pos();
    94     94   					gain = 1;
    95     95   				})
................................................................................
    99     99   			end
   100    100   			if res == nil then
   101    101   				if not minetest.check_player_privs(user, 'sorcery:infinirune') then
   102    102   					sorcery.amulet.setrune(stack)
   103    103   				end
   104    104   			end
   105    105   
   106         -			return ctx.amulet
          106  +			return ctx.wield
   107    107   		end;
   108    108   		minetest.register_craftitem(amuletname, {
   109    109   			description = sorcery.lib.str.capitalize(name) .. ' amulet';
   110    110   			inventory_image = img_sparkle:blit(img_stone):render();
   111    111   			wield_scale = { x = 0.6, y = 0.6, z = 0.6 };
   112    112   			groups = { sorcery_amulet = 1 };
   113    113   			on_use = useamulet;

Modified gravitator.lua from [8ccd951dd6] to [64c5633477].

    77     77   		};
    78     78   		on_construct = function(pos)
    79     79   			setmeta(pos,'off')
    80     80   		end;
    81     81   		on_timer = function(pos)
    82     82   			if p.color == nil then return false end
    83     83   
    84         -			local vee = {x=0,y=-1,z=0};
           84  +			local probe = sorcery.spell.probe(pos)
           85  +			if probe.disjunction then return true end
           86  +
           87  +			local vee = {x=0,y=-1,z=0}
    85     88   			minetest.add_particlespawner {
    86     89   				amount = 128;
    87     90   				time = 4;
    88     91   				minpos = vector.subtract(pos,radius);
    89     92   				maxpos = vector.add(pos,radius);
    90     93   				minvel = vector.multiply(vee, p.factor*0.5);
    91     94   				maxvel = vector.multiply(vee, p.factor);

Modified harvester.lua from [ae53588867] to [da59e16e73].

    34     34   	};
    35     35   
    36     36   	on_timer = function(pos,elapse)
    37     37   		local meta = minetest.get_meta(pos)
    38     38   		local inv = meta:get_inventory()
    39     39   		if inv:is_empty('charge') then return false end
    40     40   
           41  +		local probe = sorcery.spell.probe(pos)
           42  +		if probe.disjunction then return true end
           43  +
    41     44   		local put_in_hopper = sorcery.lib.node.discharger(pos)
    42     45   		local discharge = function(item,idx)
    43     46   			inv:set_stack('charge',idx,put_in_hopper(item))
    44     47   		end
    45     48   		
    46     49   		local ley = sorcery.ley.estimate(pos)
    47     50   		local charged = false

Modified infuser.lua from [13d8509748] to [c4c14b0c08].

    74     74   local infuser_timer = function(pos, elapsed)
    75     75   	local meta = minetest.get_meta(pos)
    76     76   
    77     77   	local inv = meta:get_inventory()
    78     78   	local infusion = inv:get_list('infusion')
    79     79   	local potions = inv:get_list('potions')
    80     80   	local elixir = infusion[1]:get_definition()
           81  +	local probe = sorcery.spell.probe(pos)
           82  +	if probe.disjunction then return true end
           83  +
    81     84   	local potionct = 0
    82     85   
    83     86   	do
    84     87   		local ingredient -- *eyeroll*
    85     88   		if infusion[1]:is_empty() then goto cancel end
    86     89   		ingredient = infusion[1]:get_name()
    87     90   		for i = 1,#potions do

Modified lib/image.lua from [680d9162e2] to [32e08434b2].

    51     51   		end;
    52     52   
    53     53   		transform = function(self, kind)
    54     54   			return image.change(self, {
    55     55   				fx = sorcery.lib.tbl.append(self.fx, {'transform' .. tostring(kind)})
    56     56   			})
    57     57   		end;
           58  +
           59  +		glow = function(self,color) return self:blit(self:multiply(color)) end;
    58     60   	} end;
    59     61   }
    60     62   return image

Added lib/math.lua version [80f52c4aaf].

            1  +local fn = {}
            2  +
            3  +fn.vsep = function(vec) -- separate a vector into a direction + magnitude
            4  +	local magnitude = math.max(math.abs(vec.x), math.abs(vec.y), math.abs(vec.z))
            5  +	local inv = 1 / magnitude
            6  +	return vector.multiply(vec,inv), magnitude
            7  +end
            8  +
            9  +fn.vdcomp = function(dist,v1,v2) -- compare the distance between two points
           10  +	-- (cheaper than calculating distance outright)
           11  +	local d if v2
           12  +		then d = vector.subtract(v1,v2)
           13  +		else d = v1
           14  +	end
           15  +	local dsq = (d.x ^ 2) + (d.y ^ 2) + (d.z ^ 2)
           16  +	return dsq / (dist^2)
           17  +	-- [0,1) == less then
           18  +	-- 1 == equal
           19  +	-- >1 == greater than
           20  +end
           21  +
           22  +return fn

Modified portal.lua from [daf126a7a3] to [12b64fac56].

   274    274   		local dev = portal_composition(pos)
   275    275   		if not dev then return false end
   276    276   		local dsp = portal_disposition(dev)
   277    277   		local crc = portal_circuit(pos)
   278    278   		local cap = sorcery.ley.netcaps(pos,delta)
   279    279   		local tune = sorcery.attunement.verify(pos)
   280    280   		local partner -- raw position of partner node, if any
          281  +		local probe = sorcery.spell.probe(pos)
   281    282   		if tune and tune.partner then
   282    283   			minetest.load_area(tune.partner)
   283    284   			-- we are attuned to a partner, but is it in the circuit?
   284    285   			for _,v in pairs(crc) do
   285    286   				if vector.equals(v.pos,tune.partner) then
   286    287   					partner = tune.partner
   287    288   					break
................................................................................
   291    292   
   292    293   		if cap.self.minpower ~= cap.self.powerdraw then return true end
   293    294   
   294    295   		-- clean out user table
   295    296   		for name,user in pairs(portal_context.users) do
   296    297   			if user and vector.equals(user.portal, pos) then
   297    298   				local found = false
   298         -				for _,u in pairs(dsp.users) do
   299         -					if u.object:get_player_name() == name then
   300         -						found = true
          299  +				if not probe.disjunction then
          300  +					for _,u in pairs(dsp.users) do
          301  +						if u.object:get_player_name() == name then
          302  +							found = true
          303  +						end
   301    304   					end
   302    305   				end
   303    306   				if not found then
   304    307   					if user.sound then minetest.sound_fade(user.sound,1,0) end
   305    308   					portal_context.users[name] = nil
   306    309   				end
   307    310   			end
   308    311   		end
          312  +		if probe.disjunction then return true end
   309    313   
   310    314   		-- one user per pad only!
   311    315   		for _,n in pairs(dev.nodes) do
   312    316   			for _,u in pairs(dsp.users) do
   313    317   				if u.slot == n then
   314    318   					local pname = u.object:get_player_name()
   315    319   					if not portal_context.users[pname] then

Modified runeforge.lua from [c8e0c3ac03] to [f2cf52d41a].

    47     47   		};
    48     48   		_proto = { id = name, data = rune; };
    49     49   	})
    50     50   end)
    51     51   
    52     52   for name,p in pairs(constants.phial_kinds) do
    53     53   	local f = string.format
    54         -	local color = sorcery.lib.color(204,38,235)
           54  +	local color = sorcery.lib.color(142,232,0)
    55     55   	local fac = p.grade / 6
    56     56   	local id = f('phial_%s', name);
    57     57   	sorcery.register_potion_tbl {
    58     58   		name = id;
    59     59   		label = f('%s Phial',p.name);
    60     60   		desc = "A powerful liquid consumed in the operation of a rune forge. Its quality determines how fast new runes can be constructed and how much energy is required by the process.";
    61     61   		color = color:brighten(1 + fac*0.5);
................................................................................
   226    226   		frame = proto.frame;
   227    227   		framestats = proto.frame and sorcery.data.metals[proto.frame].amulet;
   228    228   		tone = sorcery.lib.color(rd.tone);
   229    229   		base_spell = base_spell;
   230    230   	}
   231    231   end
   232    232   
   233         -
   234    233   local runeforge_update = function(pos,time)
   235    234   	local m = minetest.get_meta(pos)
   236    235   	local i = m:get_inventory()
   237    236   	local l = sorcery.ley.netcaps(pos,time or 1)
          237  +	local probe = sorcery.spell.probe(pos)
   238    238   
   239    239   	local pow_min = l.self.powerdraw >= l.self.minpower
   240    240   	local pow_max = l.self.powerdraw >= l.self.maxpower
   241    241   	local has_phial = function() return not i:is_empty('phial') end
   242    242   
   243         -	if time and has_phial() and pow_min then -- roll for runes
          243  +	if time and has_phial() and pow_min and not probe.disjunction then -- roll for runes
   244    244   		local int, powerfac = calc_phial_props(i:get_stack('phial',1))
   245    245   		local rolls = math.floor(time/int)
   246    246   		local newrunes = {}
   247    247   		for _=1,rolls do
   248    248   			local choices = {}
   249    249   			for name,rune in pairs(sorcery.data.runes) do
   250         -				print('considering',name)
   251         -				print('-- power',rune.minpower,(rune.minpower*powerfac)*time,'//',l.self.powerdraw,l.self.powerdraw/time,'free',l.freepower,'max',l.maxpower)
          250  +				-- print('considering',name)
          251  +				-- print('-- power',rune.minpower,(rune.minpower*powerfac)*time,'//',l.self.powerdraw,l.self.powerdraw/time,'free',l.freepower,'max',l.maxpower)
   252    252   				if (rune.minpower*powerfac)*time <= l.self.powerdraw and math.random(rune.rarity) == 1 then
   253         -					local n = ItemStack(rune.item)
   254         -					choices[#choices + 1] = n
          253  +					choices[#choices + 1] = rune
          254  +				end
          255  +			end
          256  +			if #choices > 0 then
          257  +				-- if multiple runes were rolled up, be nice to the player
          258  +				-- and pick the rarest one to give them
          259  +				local rare, choice = 0
          260  +				for i,c in pairs(choices) do
          261  +					if c.rarity > rare then
          262  +						rare = c.rarity
          263  +						choice = c
          264  +					end
   255    265   				end
          266  +				newrunes[#newrunes + 1] = ItemStack(choice.item)
   256    267   			end
   257         -			if #choices > 0 then newrunes[#newrunes + 1] = choices[math.random(#choices)] end
   258         -			print('rune choices:',dump(choices))
   259         -			print('me',dump(l.self))
          268  +			-- print('rune choices:',dump(choices))
          269  +			-- print('me',dump(l.self))
   260    270   		end
   261    271   
   262    272   		for _,r in pairs(newrunes) do
   263    273   			if i:room_for_item('cache',r) and has_phial() then
   264    274   				local qual = math.random(#constants.rune_grades)
   265    275   				rune_set(r,{grade = qual})
   266    276   				i:add_item('cache',r)
................................................................................
   284    294   		list[context;phial;7.25,1.75;1,1;]
   285    295   		list[context;refuse;8.50,1.75;1,1;]
   286    296   
   287    297   		list[current_player;main;0.25,3;8,4;]
   288    298   
   289    299   		image[0.25,0.50;1,1;sorcery_statlamp_%s.png]
   290    300   	]], (10.5 - constants.rune_cache_max*1.25)/2, constants.rune_cache_max,
   291         -	    ((has_phial and pow_max) and 'green' ) or
   292         -		((has_phial and pow_min) and 'yellow') or 'off')
          301  +		((not (has_phial and pow_min)) and 'off'  ) or
          302  +		( probe.disjunction            and 'blue' ) or
          303  +	    ((has_phial and pow_max)       and 'green') or 'yellow')
   293    304   
   294    305   	local ghost = function(slot,x,y,img)
   295    306   		if i:is_empty(slot) then spec = spec .. string.format([[
   296    307   			image[%f,%f;1,1;%s.png]
   297    308   		]], x,y,img) end
   298    309   	end
   299    310   
................................................................................
   446    457   	allow_metadata_inventory_take = function(pos,list,idx,stack,user)
   447    458   		if list == 'amulet' or list == 'wrench' then return 1 end
   448    459   		if list == 'phial' or list == 'refuse' then return stack:get_count() end
   449    460   		return 0
   450    461   	end;
   451    462   	allow_metadata_inventory_move = function(pos, fl,fi, tl,ti, count, user)
   452    463   		local inv = minetest.get_meta(pos):get_inventory()
          464  +		local probe = sorcery.spell.probe(pos)
   453    465   		local wrench if not inv:is_empty('wrench') then
   454    466   			wrench = inv:get_stack('wrench',1):get_definition()._proto
   455    467   		end
   456    468   		if fl == 'cache' then
          469  +			if probe.disjunction then return 0 end
   457    470   			if tl == 'cache' then return 1 end
   458    471   			if tl == 'active' and inv:is_empty('active') then
   459    472   				print(dump(wrench))
   460    473   				if wrench and wrench.powers.imbue and not inv:is_empty('amulet') then
   461    474   					local amulet = inv:get_stack('amulet',1)
   462    475   					local rune = inv:get_stack(fl,fi)
   463    476   					local runeid = rune:get_definition()._proto.id
................................................................................
   474    487   							return 1
   475    488   						end
   476    489   					end
   477    490   				end
   478    491   			end
   479    492   		end
   480    493   		if fl == 'active' then
          494  +			if probe.disjunction then return 0 end
   481    495   			if tl == 'cache' and wrench and (wrench.powers.extract or wrench.powers.purge) then return 1 end
   482    496   		end
   483    497   		return 0
   484    498   	end;
   485    499   })
   486    500   
   487    501   do local m = sorcery.data.metals

Added sounds/sorcery_disjoin_bg.ogg version [b5199cb6ff].

cannot compute difference between binary files

Added sounds/sorcery_hurl.ogg version [36a1bec900].

cannot compute difference between binary files

Added sounds/sorcery_powerdown.ogg version [12cb1c05f3].

cannot compute difference between binary files

Added spell.lua version [cfeba0d2de].

            1  +-- this file is used to track active spells, for the purposes of metamagic
            2  +-- like disjunction. a "spell" is a table consisting of several properties:
            3  +-- a "disjoin" function that, if present, is called when the spell is
            4  +-- abnormally interrupted, a "terminate" function that calls when the spell
            5  +-- completes, a "duration" property specifying how long the spell lasts in
            6  +-- seconds, and a "timeline" table that maps floats to functions called at 
            7  +-- specific points during the function's activity. it can also have a
            8  +-- 'delay' property that specifies how long to wait until the spell sequence
            9  +-- starts; the spell is however still vulnerable to disjunction during this
           10  +-- period. there can also be a sounds table that maps timepoints to sounds
           11  +-- the same way timeline does. each value should be a table of form {sound,
           12  +-- where}. the `where` field may contain one of 'pos', 'caster', 'subjects', or
           13  +-- a vector specifying a position in the world, and indicate where the sound
           14  +-- should be played. by default 'caster' and 'subjects' sounds will be attached
           15  +-- to the objects they reference; 'attach=false' can be added to prevent this.
           16  +-- by default sounds will be faded out quickly when disjunction occurs; this
           17  +-- can be controlled by the fade parameter.
           18  +--
           19  +-- spells can have various other properties, for instance 'disjunction', which
           20  +-- when true prevents other spells from being cast in its radius while it is
           21  +-- still in effect. disjunction is absolute; there is no way to overwhelm it.
           22  +--
           23  +-- the spell also needs at least one of "anchor", "subjects", or "caster".
           24  +--  * an anchor is a position that, in combination with 'range', specifies the area
           25  +--    where a spell is in effect; this is used for determining whether it
           26  +--    is affected by a disjunction that incorporates part of that position
           27  +--  * subjects is an array of individuals affected by the spell. when
           28  +--    disjunction is cast on one of them, they will be removed from the
           29  +--    table. each entry should have at least a 'player' field; they can
           30  +--    also contain any other data useful to the spell. if a subject has
           31  +--    a 'disjoin' field it must be a function called when they are removed
           32  +--    from the list of spell targets.
           33  +--  * caster is the individual who cast the spell, if any. a disjunction
           34  +--    against their person will totally disrupt the spell.
           35  +local log = function(...) sorcery.log('spell',...) end
           36  +
           37  +-- FIXME saving object refs is iffy, find a better alternative
           38  +sorcery.spell = {
           39  +	active = {}
           40  +}
           41  +
           42  +local get_spell_positions = function(spell)
           43  +	local spellpos
           44  +	if spell.anchor then
           45  +		spellpos = {spell.anchor}
           46  +	elseif spell.attach then
           47  +		if spell.attach == 'caster' then
           48  +			spellpos = {spell.caster:get_pos()}
           49  +		elseif spell.attach == 'subjects' or spell.attach == 'both' then
           50  +			if spell.attach == 'both' then
           51  +				spellpos = {spell.caster:get_pos()}
           52  +			else spellpos = {} end
           53  +			for _,s in pairs(spell.subjects) do
           54  +				spellpos[#spellpos+1] = s.player:get_pos()
           55  +			end
           56  +		else spellpos = {spell.attach:get_pos()} end
           57  +	else assert(false) end
           58  +	return spellpos
           59  +end
           60  +
           61  +local inspellrange = function(spell,pos,range)
           62  +	local spellpos = get_spell_positions(spell)
           63  +
           64  +	for _,p in pairs(spellpos) do
           65  +		if vector.equals(pos,p) or
           66  +			(range       and sorcery.lib.math.vdcomp(range,      pos,p)<=1) or
           67  +			(spell.range and sorcery.lib.math.vdcomp(spell.range,p,pos)<=1) then
           68  +			return true
           69  +		end
           70  +	end
           71  +	return false
           72  +end
           73  +
           74  +sorcery.spell.probe = function(pos,range)
           75  +	-- this should be called before any magical effects are performed.
           76  +	-- other mods can overlay their own functions to e.g. protect areas
           77  +	-- from magic
           78  +	local result = {}
           79  +
           80  +	-- first we need to check if any active injunctions are in effect
           81  +	-- injunctions are registered as spells with a 'disjunction = true'
           82  +	-- property
           83  +	for id,spell in pairs(sorcery.spell.active) do
           84  +		if not (spell.disjunction and (spell.anchor or spell.attach)) then goto skip end
           85  +		if inspellrange(spell,pos,range) then
           86  +			result.disjunction = true
           87  +			break
           88  +		end
           89  +	::skip::end
           90  +	
           91  +	-- at some point we might also check to see if certain anti-magic
           92  +	-- blocks are nearby or suchlike. there should also be regions where
           93  +	-- perhaps certain kinds of magic are unusually empowered or weak
           94  +	-- (perhaps drawing on leyline affinity)
           95  +	return result
           96  +end
           97  +sorcery.spell.disjoin = function(d)
           98  +	local spells,targets = {},{}
           99  +	if d.spell then spells = {{v=d.spell}}
          100  +	elseif d.target then targets = {d.target}
          101  +	elseif d.pos then -- find spells anchored here and people in range
          102  +		for id,spell in pairs(sorcery.spell.active) do
          103  +			if not spell.anchor then goto skip end -- this intentionally excludes attached spells
          104  +			if inspellrange(spell,d.pos,d.range) then
          105  +				spells[#spells+1] = {v=spell,i=id}
          106  +			end
          107  +		::skip::end
          108  +		local ppl = minetest.get_objects_inside_radius(d.pos,d.range)
          109  +		if #targets == 0 then targets = ppl else
          110  +			for _,p in pairs(ppl) do targets[#targets+1] = p end
          111  +		end
          112  +	end
          113  +
          114  +	-- iterate over targets to remove from any spell's influence
          115  +	for _,t in pairs(targets) do
          116  +		for id,spell in pairs(sorcery.spell.active) do
          117  +			if spell.caster == t then spells[#spells+1] = {v=spell,i=id} else
          118  +				for si, sub in pairs(spell.subjects) do
          119  +					if sub.player == t then
          120  +						if sub.disjoin then sub:disjoin(spell) end
          121  +						spell.release_subject(si)
          122  +						break
          123  +					end
          124  +				end
          125  +			end
          126  +		end
          127  +	end
          128  +
          129  +	-- spells to disjoin entirely
          130  +	for _,s in pairs(spells) do local spell = s.v
          131  +		if spell.disjoin then spell:disjoin() end
          132  +		spell.abort()
          133  +		if s.i then sorcery.spell.active[s.i] = nil else
          134  +			for k,v in pairs(sorcery.spell.active) do
          135  +				if v == spell then sorcery.spell.active[k] = nil break end
          136  +			end
          137  +		end
          138  +	end
          139  +end
          140  +
          141  +-- when a new spell is created, we analyze it and make the appropriate calls
          142  +-- to minetest.after to queue up the events. each job returned needs to be
          143  +-- saved in 'jobs' so they can be canceled if the spell is disjoined. no polling
          144  +-- necessary :D
          145  +
          146  +sorcery.spell.cast = function(proto)
          147  +	local s = table.copy(proto)
          148  +	s.jobs = s.jobs or {} s.vfx = s.vfx or {} s.sfx = s.sfx or {}
          149  +	s.impacts = s.impacts or {} s.subjects = s.subjects or {}
          150  +	s.delay = s.delay or 0
          151  +	s.visual = function(def,subj)
          152  +		s.vfx[#s.vfx + 1] = {
          153  +			handle = minetest.add_particlespawner(def);
          154  +			subject = subj;
          155  +		}
          156  +	end
          157  +	s.visual_caster = function(def) -- convenience function
          158  +		local d = table.copy(def)
          159  +		d.attached = s.caster
          160  +		s.visual(d)
          161  +	end
          162  +	s.visual_subjects = function(def)
          163  +		for _,sub in pairs(s.subjects) do
          164  +			local d = table.copy(def)
          165  +			d.attached = sub.player
          166  +			s.visual(d,sub)
          167  +		end
          168  +	end
          169  +	s.affect = function(i)
          170  +		local etbl = {}
          171  +		for _,sub in pairs(s.subjects) do
          172  +			local eff = late.new_effect(sub.player, i)
          173  +			local rec = {
          174  +				effect = eff;
          175  +				subject = sub;
          176  +			}
          177  +			s.impacts[#s.impacts+1] = rec
          178  +			etbl[#etbl+1] = rec
          179  +		end
          180  +		return etbl
          181  +	end
          182  +	s.abort = function()
          183  +		for _,j in ipairs(s.jobs) do j:cancel() end
          184  +		for _,v in ipairs(s.vfx) do minetest.delete_particlespawner(v.handle) end
          185  +		for _,i in ipairs(s.sfx) do s.silence(i) end
          186  +		for _,i in ipairs(s.impacts) do i.effect:stop() end
          187  +	end
          188  +	s.release_subject = function(si)
          189  +		local t = s.subjects[si]
          190  +		print('releasing against',si,t)
          191  +		for _,f in pairs(s.sfx)     do if f.subject == t then s.silence(f) end end
          192  +		for _,f in pairs(s.impacts) do if f.subject == t then f.effect:stop() end end
          193  +		for _,f in pairs(s.vfx) do
          194  +			if f.subject == t then minetest.delete_particlespawner(f.handle) end
          195  +		end
          196  +		s.subjects[si] = nil
          197  +	end
          198  +	local interpret_timespec = function(when)
          199  +		local t if type(when) == 'number' then
          200  +			t = s.duration * when
          201  +		else
          202  +			t = (s.duration * (when.whence or 0)) + when.secs
          203  +		end
          204  +		if t then return math.min(s.duration,math.max(0,t)) end
          205  +
          206  +		log('invalid timespec ' .. dump(when))
          207  +		return 0
          208  +	end
          209  +	s.queue = function(when,fn)
          210  +		local elapsed = s.starttime and minetest.get_server_uptime() - s.starttime or 0
          211  +		local timepast = interpret_timespec(when)
          212  +		if not timepast then timepast = 0 end
          213  +		local timeleft = s.duration - timepast
          214  +		local howlong = (s.delay + timepast) - elapsed
          215  +		if howlong < 0 then
          216  +			log('cannot time-travel! queue() called with `when` specifying timepoint that has already passed')
          217  +			howlong = 0
          218  +		end
          219  +		s.jobs[#s.jobs+1] = minetest.after(howlong, function()
          220  +			-- this is somewhat awkward. since we're using a non-polling approach, we
          221  +			-- need to find a way to account for a caster or subject walking into an
          222  +			-- existing antimagic field, or someone with an existing antimagic aura
          223  +			-- walking into range of the anchor. so every time a spell effect would
          224  +			-- take place, we first check to see if it's in range of something nasty
          225  +			if not s.disjunction and -- avoid self-disjunction
          226  +				(s.caster and sorcery.spell.probe(s.caster:get_pos()).disjunction) or
          227  +				(s.anchor and sorcery.spell.probe(s.anchor,s.range).disjunction) then
          228  +				sorcery.spell.disjoin{spell=s}
          229  +			else
          230  +				if not s.disjunction then for _,sub in pairs(s.subjects) do
          231  +					local sp = sub.player:get_pos()
          232  +					if sorcery.spell.probe(sp).disjunction then
          233  +						sorcery.spell.disjoin{pos=sp}
          234  +					end
          235  +				end end
          236  +				-- spell still exists and we've removed any subjects who have been
          237  +				-- affected by a disjunction spell, it's now time to actually perform
          238  +				-- the queued-up action
          239  +				fn(s,timepast,timeleft)
          240  +			end
          241  +		end)
          242  +	end
          243  +	s.play_now = function(spec)
          244  +		local specs, stbl = {}, {}
          245  +		local addobj = function(obj,sub)
          246  +			if spec.attach == false then specs[#specs+1] = {
          247  +				spec = { pos = obj:get_pos() };
          248  +				obj = obj, subject = sub;
          249  +			} else specs[#specs+1] = {
          250  +				spec = { object = obj };
          251  +				obj = obj, subject = sub;
          252  +			} end
          253  +		end
          254  +
          255  +		if spec.where == 'caster' then addobj(s.caster)
          256  +		elseif spec.where == 'subjects' then
          257  +			for _,sub in pairs(s.subjects) do addobj(sub.player,sub) end
          258  +		elseif spec.where == 'pos' then specs[#specs+1] = { spec = {pos = s.anchor} }
          259  +		else specs[#specs+1] = { spec = {pos = spec.where} } end
          260  +
          261  +		for _,sp in pairs(specs) do
          262  +			sp.spec.gain = spec.gain
          263  +			local so = {
          264  +				handle = minetest.sound_play(spec.sound, sp.spec, spec.ephemeral);
          265  +				ctl = spec;
          266  +				-- object = sp.obj;
          267  +				subject = sp.subject;
          268  +			}
          269  +			stbl[#stbl+1] = so
          270  +			s.sfx[#s.sfx+1] = so
          271  +		end
          272  +		return stbl
          273  +	end
          274  +	s.play = function(when,spec)
          275  +		s.queue(when, function()
          276  +			local snds = s.play_now(spec)
          277  +			if spec.stop then
          278  +				s.queue(spec.stop, function()
          279  +					for _,snd in pairs(snds) do s.silence(snd) end
          280  +				end)
          281  +			end
          282  +		end)
          283  +	end
          284  +	s.silence = function(sound)
          285  +		if sound.ctl.fade == 0 then minetest.sound_stop(sound.handle)
          286  +		else minetest.sound_fade(sound.handle,sound.ctl.fade or 1,0) end
          287  +	end
          288  +	local startqueued, termqueued = false, false
          289  +	local myid = #sorcery.spell.active+1
          290  +	s.cancel = function()
          291  +		s.abort()
          292  +		sorcery.spell.active[myid] = nil
          293  +	end
          294  +	local perform_disjunction_calls = function()
          295  +		local positions = get_spell_positions(s)
          296  +		for _,p in pairs(positions) do
          297  +			sorcery.spell.disjoin{pos = p, range = s.range}
          298  +		end
          299  +	end
          300  +	if s.timeline then
          301  +		for when_raw,what in pairs(s.timeline) do
          302  +			local when = interpret_timespec(when_raw)
          303  +			if s.delay == 0 and when == 0 then
          304  +				startqueued = true
          305  +				if s.disjunction then perform_disjunction_calls() end
          306  +				what(s,0,s.duration)
          307  +			elseif when_raw == 1 or when >= s.duration then -- avoid race conditions
          308  +				if not termqueued then
          309  +					termqueued = true
          310  +					s.queue(1,function(s,...)
          311  +						what(s,...)
          312  +						if s.terminate then s:terminate() end
          313  +						sorcery.spell.active[myid] = nil
          314  +					end)
          315  +				else
          316  +					log('multiple final timeline events not possible, ignoring')
          317  +				end
          318  +			elseif when == 0 and s.disjunction then
          319  +				startqueued = true
          320  +				s.queue(when_raw,function(...)
          321  +					perform_disjunction_calls()
          322  +					what(...)
          323  +				end)
          324  +			else s.queue(when_raw,what) end
          325  +		end
          326  +	end
          327  +	if s.intervals then
          328  +		for _,int in pairs(s.intervals) do
          329  +			local timeleft = s.duration - interpret_timespec(int.after)
          330  +			local iteration, itercount = 0, timeleft / int.period
          331  +			local function iterate(lastreturn)
          332  +				iteration = iteration + 1
          333  +				local nr = int.fn {
          334  +					spell = s;
          335  +					iteration = iteration;
          336  +					iterationcount = itercount;
          337  +					timeleft = timeleft;
          338  +					timeelapsed = s.duration - timeleft;
          339  +					lastreturn = lastreturn;
          340  +				}
          341  +				if nr ~= false and iteration < itercount then
          342  +					s.jobs[#s.jobs+1] = minetest.after(int.period,
          343  +						function() iterate(nr) end)
          344  +				end
          345  +			end
          346  +			if int.after
          347  +				then s.queue(int.after, iterate)
          348  +				else s.queue({whence=0, secs=s.period}, iterate)
          349  +			end
          350  +		end
          351  +	end
          352  +	if s.disjunction and not startqueued then
          353  +		if s.delay == 0 then perform_disjunction_calls() else
          354  +			s.queue(0, function() perform_disjunction_calls() end)
          355  +		end
          356  +	end
          357  +	if s.sounds then
          358  +		for when,what in pairs(s.sounds) do s.play(when,what) end
          359  +	end
          360  +	sorcery.spell.active[myid] = s
          361  +	if not termqueued then
          362  +		s.jobs[#s.jobs+1] = minetest.after(s.delay + s.duration, function()
          363  +			if s.terminate then s:terminate() end
          364  +			sorcery.spell.active[myid] = nil
          365  +		end)
          366  +	end
          367  +	s.starttime = minetest.get_server_uptime()
          368  +	return s
          369  +end

Modified textures/sorcery_crackle.png from [faf84cf96c] to [58d455dd12].

cannot compute difference between binary files

Added textures/sorcery_flicker.png version [d2be38b136].

cannot compute difference between binary files

Added textures/sorcery_fog.png version [f33fe7d904].

cannot compute difference between binary files

Added textures/sorcery_glitter.png version [e4de7bf843].

cannot compute difference between binary files

Added textures/sorcery_poof.png version [01b7e56ad7].

cannot compute difference between binary files

Added textures/sorcery_sparking.png version [6a1bae8d93].

cannot compute difference between binary files

Added textures/sorcery_sputter.png version [3a75e25024].

cannot compute difference between binary files

Modified tnodes.lua from [4064266f0d] to [46cef4271e].

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