sorcery  Check-in [6e106c135c]

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

Modified data/runes.lua from [d5ad4a6740] to [3a290dc995].

    43     43   		texture = sorcery.vfx.glowspark(color):render();
    44     44   		animation = {
    45     45   			type = 'vertical_frames', length = 5.1;
    46     46   			aspect_w = 16, aspect_h = 16;
    47     47   		};
    48     48   	});
    49     49   end
           50  +
           51  +local teleport = function(ctx,subjects,delay,pos,color)
           52  +	if ctx.amulet.frame == 'tungsten' then delay = delay * 0.5 end
           53  +	color = color or sorcery.lib.color(29,205,247)
           54  +	local center = ctx.caster:get_pos()
           55  +	for _,sub in pairs(subjects) do
           56  +		local s = sub.ref
           57  +		local offset = vector.subtract(s:get_pos(), center)
           58  +		local pt = sorcery.lib.node.get_arrival_point(vector.add(pos,offset))
           59  +		if pt then
           60  +			-- minetest.sound_play('sorcery_stutter', {
           61  +			-- 	object = s, gain = 0.8;
           62  +			-- },true)
           63  +			local mydelay = sub.delay or (delay + math.random(-10,10)*.1);
           64  +			local sh = s:get_properties().eye_height
           65  +			local color = sub.color or color
           66  +			sorcery.lib.node.preload(pt,s)
           67  +			sorcery.spell.cast {
           68  +				name = 'sorcery:translocate';
           69  +				duration = mydelay;
           70  +				caster = ctx.caster;
           71  +				subjects = {{player=s,dest=sub.dest or pt}};
           72  +				timeline = {
           73  +					[0] = function(sp,_,timeleft)
           74  +						sparkle(color,sp,timeleft*100, timeleft, 0.3,1.3, sh)
           75  +						sp.windup = (sp.play_now{
           76  +							sound = 'sorcery_windup';
           77  +							where = 'subjects';
           78  +							gain = 0.4;
           79  +							fade = 1.5;
           80  +						})[1]
           81  +					end;
           82  +					[0.4] = function(sp,_,timeleft)
           83  +						sparkle(color,sp,timeleft*150, timeleft, 0.6,1.8, sh)
           84  +					end;
           85  +					[0.7] = function(sp,_,timeleft)
           86  +						sparkle(color,sp,timeleft*80, timeleft, 2,4, sh)
           87  +					end;
           88  +					[1] = function(sp)
           89  +						sp.silence(sp.windup)
           90  +						minetest.sound_play('sorcery_zap', { pos = pt, gain = 0.4 },true)
           91  +						minetest.sound_play('sorcery_zap', { pos = s:get_pos(), gain = 0.4 },true)
           92  +						sorcery.vfx.body_sparkle(nil,color:brighten(1.3),2,s:get_pos())
           93  +						s:set_pos(pt)
           94  +						sorcery.vfx.body_sparkle(s,color:darken(0.3),2)
           95  +					end;
           96  +				};
           97  +				sounds = {
           98  +					[0] = { sound = 'sorcery_stutter', pos = 'subjects' };
           99  +				};
          100  +			}
          101  +		end
          102  +	end
          103  +end
    50    104   return {
    51    105   	translocate = {
    52    106   		name = 'Translocate';
    53    107   		tone = {0,235,233};
    54    108   		minpower = 3;
    55         -		rarity = 10;
          109  +		rarity = 7;
    56    110   		amulets = {
    57    111   			amethyst = {
    58    112   				name = 'Joining';
    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';
          113  +				desc = 'Give this amulet to another and with a snap of their fingers they can arrive safely at your side from anywhere in the world — though returning whence they came may be a more difficult matter';
    60    114   				apply = function(ctx)
    61    115   					local maker = ctx.user:get_player_name()
    62    116   					ctx.meta:set_string('rune_join_target',maker)
    63    117   				end;
    64    118   				remove = function(ctx) ctx.meta:set_string('rune_join_target','') end;
          119  +				cast = function(ctx)
          120  +					local target = minetest.get_player_by_name(ctx.meta:get_string('rune_join_target'))
          121  +					if not target then return false end
          122  +
          123  +					local subjects if ctx.amulet.frame == 'cobalt' then
          124  +						if ctx.target.type ~= 'object' then return false end
          125  +						subjects = {{ref=ctx.target.ref}}
          126  +					else subjects = {{ref=ctx.caster}} end
          127  +
          128  +					local delay = math.max(5,11 - ctx.stats.power) + 2.3*(math.random()*2-1)
          129  +					local color = sorcery.lib.color(117,38,237)
          130  +					teleport(ctx,subjects,delay,target:get_pos(),color)
          131  +					if ctx.amulet.frame == 'gold' then
          132  +						teleport(ctx,{{ref=target}},delay,ctx.caster:get_pos())
          133  +					else
          134  +						ctx.sparkle = false
          135  +					end
          136  +				end;
    65    137   				frame = {
          138  +					tungsten = {
          139  +						name = 'Quick Joining';
          140  +						desc = 'Give this amulet to another and they can arrive safely at your side almost instantaneously from anywhere in the world — though returning whence they came may be a more difficult matter';
          141  +					};
    66    142   					gold = {
    67    143   						name = 'Exchange';
    68    144   						desc = 'Give this amulet to another and they will be able to trade places with you no matter where in the world each of you might be.'; 
    69    145   					};
    70    146   					cobalt = {
    71    147   						name = 'Sending';
    72    148   						desc = 'Give this amulet to another and by wielding this amulet against another they will be able to transport them instantly to your side';
................................................................................
    91    167   					else
    92    168   						local pos = minetest.string_to_pos(ctx.meta:get_string('rune_return_dest'))
    93    169   						ctx.meta:set_string('rune_return_dest','')
    94    170   						local subjects = { ctx.caster }
    95    171   						local center = ctx.caster:get_pos()
    96    172   						ctx.sparkle = false
    97    173   						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
    99         -						for _,s in pairs(subjects) do
   100         -							local offset = vector.subtract(s:get_pos(), center)
   101         -							local pt = sorcery.lib.node.get_arrival_point(vector.add(pos,offset))
   102         -							if pt then
   103         -								-- minetest.sound_play('sorcery_stutter', {
   104         -								-- 	object = s, gain = 0.8;
   105         -								-- },true)
   106         -								local mydelay = delay + math.random(-10,10)*.1;
   107         -								local sh = s:get_properties().eye_height
   108         -								local color = sorcery.lib.color(29,205,247)
   109         -								sorcery.lib.node.preload(pt,s)
   110         -								sorcery.spell.cast {
   111         -									duration = mydelay;
   112         -									caster = ctx.caster;
   113         -									subjects = {{player=s,dest=pt}};
   114         -									timeline = {
   115         -										[0] = function(sp,_,timeleft)
   116         -											sparkle(color,sp,timeleft*100, timeleft, 0.3,1.3, sh)
   117         -											sp.windup = (sp.play_now{
   118         -												sound = 'sorcery_windup';
   119         -												where = 'subjects';
   120         -												gain = 0.4;
   121         -												fade = 1.5;
   122         -											})[1]
   123         -										end;
   124         -										[0.4] = function(sp,_,timeleft)
   125         -											sparkle(color,sp,timeleft*150, timeleft, 0.6,1.8, sh)
   126         -										end;
   127         -										[0.7] = function(sp,_,timeleft)
   128         -											sparkle(color,sp,timeleft*80, timeleft, 2,4, sh)
   129         -										end;
   130         -										[1] = function(sp)
   131         -											sp.silence(sp.windup)
   132         -											minetest.sound_play('sorcery_zap', { pos = pt, gain = 0.4 },true)
   133         -											minetest.sound_play('sorcery_zap', { pos = s:get_pos(), gain = 0.4 },true)
   134         -											sorcery.vfx.body_sparkle(nil,sorcery.lib.color(20,255,120),2,s:get_pos())
   135         -											s:set_pos(pt)
   136         -											sorcery.vfx.body_sparkle(s,sorcery.lib.color(20,120,255),2)
   137         -										end;
   138         -									};
   139         -									sounds = {
   140         -										[0] = { sound = 'sorcery_stutter', pos = 'subjects' };
   141         -									};
   142         -								}
   143         -							end
   144         -						end
          174  +						teleport(ctx,{{ref=ctx.caster}},delay,pos)
   145    175   					end
   146    176   				end;
   147    177   				frame = {
   148    178   					tungsten = {
   149    179   						name = 'Quick Return';
   150    180   						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    181   					};
................................................................................
   163    193   						name = 'Mass Banishment';
   164    194   						desc = 'Use this amulet once to bind it to a particular point in the world, then use it again to seize up everyone surrounding you in the grip of a fearsome magic that will deport them all in the blink of an eye to whatever destination you have chosen';
   165    195   					};
   166    196   				};
   167    197   			};
   168    198   			ruby = minetest.get_modpath('beds') and {
   169    199   				name = 'Escape';
   170         -				desc = 'Immediately transport yourself out of a dangerous situation back to the last place you slept';
          200  +				desc = 'Immediately transport yourself out of a dangerous situation back to the last place you slept, before anyone has time to net you in a disjunction';
   171    201   				cast = function(ctx)
   172    202   					-- if not beds.spawns then beds.read_spawns() end
   173    203   					local subjects = {ctx.caster}
   174    204   					for _,s in pairs(subjects) do
   175    205   						local spp = beds.spawn[ctx.caster:get_player_name()]
   176    206   						if spp then
   177    207   							local oldpos = s:get_pos()
   178         -							minetest.sound_play('sorcery_splunch', {pos=oldpos}, true)
   179         -							sorcery.vfx.body_sparkle(nil,sorcery.lib.color(244,38,131),2,oldpos)
   180         -							s:set_pos(spp)
   181         -							minetest.sound_play('sorcery_splunch', {pos=spp}, true)
   182         -							sorcery.vfx.body_sparkle(nil,sorcery.lib.color(244,38,89),2,spp)
          208  +							local jump = function()
          209  +								minetest.sound_play('sorcery_splunch', {pos=oldpos}, true)
          210  +								sorcery.vfx.body_sparkle(nil,sorcery.lib.color(244,38,131),2,oldpos)
          211  +								s:set_pos(spp)
          212  +								minetest.sound_play('sorcery_splunch', {pos=spp}, true)
          213  +								sorcery.vfx.body_sparkle(nil,sorcery.lib.color(244,38,89),2,spp)
          214  +							end
          215  +							if ctx.amulet.frame == 'cobalt' then
          216  +								sorcery.spell.cast {
          217  +									name = 'sorcery:escape';
          218  +									caster = s;
          219  +									duration = random() * 0.4 + 0.3;
          220  +									timeline = {
          221  +										[0] = function()
          222  +											sorcery.vfx.imbue(sorcery.lib.color(244,38,131), s, 1.3)
          223  +										end;
          224  +										[1] = function(sp)
          225  +											local radius = 6 * ctx.stats.power
          226  +											local center = sp.caster:get_pos()
          227  +											local targets = minetest.get_objects_inside_radius(center, radius)
          228  +											jump()
          229  +											-- TODO: shockwave visuals
          230  +											for _,o in pairs(targets) do
          231  +												if not o:get_armor_groups().immortal then
          232  +													local distance = vector.distance(o:get_pos(), center)
          233  +													local dmg = (7 * ctx.stats.power) * (distance / radius)
          234  +													minetest.punch(ctx.caster, 1.0, {
          235  +														full_punch_interval = 1.0;
          236  +														damage_groups = { fleshy = dmg };
          237  +													}, vector.direction(o:get_pos(), center));
          238  +												end
          239  +											end 
          240  +										end;
          241  +									}
          242  +								}
          243  +							else jump() end
   183    244   						end
   184    245   						-- TODO decide what happens to the people who don't have
   185    246   						-- respawn points already set
   186    247   					end
   187    248   				end;
   188    249   				frame = {
   189    250   					cobalt = {
................................................................................
   195    256   						desc = 'Break up even the fiercest of quarrels by transporting yourself and everyone around you out of harms\' way and immediately back to the last place each slept';
   196    257   					};
   197    258   				};
   198    259   			};
   199    260   			diamond = {
   200    261   				name = 'Elevation';
   201    262   				desc = 'Lift yourself and everything around you high up into the sky';
          263  +				cast = function(ctx)
          264  +					local center = ctx.caster:get_pos()
          265  +					local up = ((ctx.stats.power * 7) + math.random(6,17)) * (math.random() * 0.4 + 0.4)
          266  +					if center.y > 0 then up = up + center.y end
          267  +					local newcenter = vector.new(center.x,up,center.z)
          268  +					if not sorcery.lib.node.get_arrival_point(newcenter) then return false end
          269  +					sorcery.lib.node.preload(newcenter,ctx.caster)
          270  +					local jmpcolor = sorcery.lib.color(0,255,144)
          271  +
          272  +					if not ctx.amulet.frame == 'iridium' then
          273  +						local where = vector.offset(center,0,1,0)
          274  +						repeat local ok, nx = minetest.line_of_sight(where, newcenter)
          275  +							if ok then break end
          276  +							if minetest.get_node_or_nil(nx) == nil then
          277  +								minetest.load_area(nx)
          278  +								where = nx -- save some time
          279  +							else return false end
          280  +						until false
          281  +					end
          282  +					local lift = function(n)
          283  +						local dest = vector.new(n.pos.x, up + n.h, n.pos.z)
          284  +						if sorcery.lib.node.is_clear(dest) then
          285  +							minetest.set_node(dest, minetest.get_node(n.pos))
          286  +							minetest.get_meta(dest):from_table(minetest.get_meta(n.pos):to_table())
          287  +							if math.random(5) == 1 then
          288  +								minetest.set_node(n.pos, {name='sorcery:air_flash_' .. tostring(math.random(10))})
          289  +							else minetest.remove_node(n.pos) end
          290  +							local obs = minetest.get_objects_inside_radius(n.pos, 1.5)
          291  +							if obs then for _,o in pairs(obs) do
          292  +								local pt = sorcery.lib.node.get_arrival_point(vector.add(dest, vector.subtract(o:get_pos(),n.pos))) 
          293  +								if pt then
          294  +									o:set_pos(pt)
          295  +									sorcery.vfx.body_sparkle(o,jmpcolor:darken(0.3),2)
          296  +								end
          297  +							end end
          298  +							return true
          299  +						else
          300  +							return false
          301  +						end
          302  +					end
          303  +					local nodes,sparkles,tmap = {},{},{}
          304  +					local r = math.ceil((ctx.stats.power * 0.1) * 8 + 3)
          305  +					for x = -r,r do -- lazy hack to select a sphere
          306  +					for z = -r,r do
          307  +						local col = {}
          308  +						for y = -r,r do
          309  +							local ofs = vector.new(x,y,z)
          310  +							if sorcery.lib.math.vdcomp(r,ofs) <= 1 then
          311  +								local pos = vector.add(center, ofs)
          312  +								if sorcery.lib.node.is_air(pos) then
          313  +									if y > 0 then
          314  +										sparkles[#sparkles+1] = pos
          315  +										break -- levitation is a sin
          316  +									end
          317  +								else
          318  +									nodes[#nodes+1] = {pos=pos, h=y}
          319  +									col[#col+1] = {pos=pos, h=y}
          320  +								end
          321  +							end
          322  +						end
          323  +						if #col > 0 then
          324  +							local seq = math.floor(math.sqrt((x^2) + (z^2)))
          325  +							-- TODO find a way to optimise this shitshow
          326  +							if tmap[seq]
          327  +								then tmap[seq][#(tmap[seq])+1] = col
          328  +								else tmap[seq] = {col}
          329  +							end
          330  +						end
          331  +					end end
          332  +
          333  +					-- for _,n in pairs(nodes) do
          334  +					-- 	local dest = vector.new(n.pos.x, up + n.h, n.pos.z)
          335  +					-- 	if sorcery.lib.node.is_clear(dest) then
          336  +					-- 		minetest.set_node(dest, minetest.get_node(n.pos))
          337  +					-- 		minetest.get_meta(dest):from_table(minetest.get_meta(n.pos):to_table())
          338  +					-- 		if math.random(5) == 1 then
          339  +					-- 			minetest.set_node(n.pos, {name='sorcery:air_flash_' .. tostring(math.random(10))})
          340  +					-- 		else minetest.remove_node(n.pos) end
          341  +					-- 	end
          342  +					-- end
          343  +					local timeline, sounds = {
          344  +						[0] = function(s)
          345  +							-- sorcery.vfx.imbue(jmpcolor,s.caster,1)
          346  +						end;
          347  +					}, {};
          348  +					local time = 0;
          349  +					for i=0,#tmap do
          350  +						local cols = tmap[i]
          351  +						if cols ~= nil then
          352  +							time = time + math.random()*0.2 + 0.1
          353  +							local wh = {whence=0,secs=2+time}
          354  +							timeline[wh] = function(sp)
          355  +								for _,col in pairs(cols) do
          356  +									for _,n in pairs(col) do lift(n) end
          357  +								end
          358  +							end
          359  +							sounds[wh] = {
          360  +								sound = 'sorcery_zap';
          361  +								gain = math.random() + 0.1;
          362  +								where = cols[1][1].pos;
          363  +							}
          364  +						end
          365  +					end
          366  +					sorcery.spell.cast {
          367  +						name = 'sorcery:elevate';
          368  +						caster = ctx.caster;
          369  +						anchor = center, radius = r;
          370  +						duration = 2 + time;
          371  +						timeline = timeline, sounds = sounds;
          372  +					}
          373  +				end;
          374  +				frame = {
          375  +					iridium = {
          376  +						name = 'Ascension';
          377  +						desc = 'Transport yourself and your surroundings high into the heavens, even if you are deep in the bowels of the earth';
          378  +					};
          379  +				};
   202    380   			};
   203    381   		};
   204    382   	};
   205    383   	disjoin = {
   206    384   		name = 'Disjoin';
   207    385   		tone = {159,235,0};
   208    386   		minpower = 4;
   209         -		rarity = 40;
          387  +		rarity = 34;
   210    388   		amulets = {
   211    389   			sapphire = {
   212    390   				name = 'Unsealing';
   213    391   				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';
   214    392   			};
   215    393   			amethyst = {
   216    394   				name = 'Purging';
................................................................................
   251    429   			};
   252    430   			luxite = {
   253    431   				name = 'Disjunctive Aura';
   254    432   				desc = 'For a time, all magic undertaken in your vicinity will fail totally — including your own';
   255    433   				cast = function(ctx)
   256    434   					local h = ctx.heading.eyeheight*1.1
   257    435   					sorcery.spell.cast {
          436  +						name = 'sorcery:disjunctive-aura';
   258    437   						caster = ctx.caster, attach = 'caster';
   259    438   						subjects = {{player=ctx.caster}};
   260    439   						disjunction = true, range = 4 + ctx.stats.power;
   261    440   						duration = 10 + ctx.stats.power * 3;
   262    441   						timeline = {
   263    442   							[0] = function(s,_,tl)
   264    443   								local ttns = 0.8
................................................................................
   348    527   			};
   349    528   			ruby = {
   350    529   				name = 'Liftoff';
   351    530   				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';
   352    531   				cast = function(ctx)
   353    532   					local power = 14 * (1+(ctx.stats.power * 0.2))
   354    533   					minetest.sound_play('sorcery_hurl',{object=ctx.caster},true)
          534  +
          535  +					local oldsp = sorcery.spell.ensorcelled(ctx.caster, 'sorcery:liftoff')
          536  +					if oldsp then oldsp:cancel() end
          537  +
   355    538   					sorcery.spell.cast {
          539  +						name = 'sorcery:liftoff';
   356    540   						caster = ctx.caster;
   357    541   						subjects = {{player=ctx.caster}};
   358    542   						duration = power * 0.30;
   359    543   						timeline = {
   360    544   							[0] = function(s,_,tl)
   361    545   								sparktrail(s.visual_subjects,ctx.caster,sorcery.lib.color(255,252,93))
   362         -								ctx.caster:add_velocity{y=power;x=0,z=0}
          546  +								ctx.caster:add_velocity{y=power*1.2;x=0,z=0}
          547  +							end;
          548  +							[{whence=0, secs=1}] = function(s)
   363    549   								s.affect {
   364    550   									duration = power * 0.50;
   365         -									raise = 2;
          551  +									raise = 0.5;
   366    552   									-- fall = (power * 0.25) * 0.3;
   367    553   									impacts = {
   368    554   										gravity = 0.1;
   369    555   									};
   370    556   								}
   371    557   							end;
   372    558   						};
................................................................................
   396    582   				desc = 'Toss an enemy violently into the air, and allow the inevitable impact to do your dirty work for you';
   397    583   				cast = function(ctx)
   398    584   					if not (ctx.target and ctx.target.type == 'object') then return false end
   399    585   					local tgt = ctx.target.ref
   400    586   					local power = 16 * (1+(ctx.stats.power * 0.2))
   401    587   					minetest.sound_play('sorcery_hurl',{object=ctx.caster},true)
   402    588   					sorcery.spell.cast {
          589  +						name = 'sorcery:flinging';
   403    590   						caster = ctx.caster;
   404    591   						subjects = {{player=tgt}};
   405    592   						duration = 4;
   406    593   						timeline = {
   407    594   							[0] = function(s,_,tl)
   408    595   								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         -											};
          596  +									sorcery.vfx.imbue(function() return
          597  +										sorcery.lib.color {
          598  +											hue = math.random(41,63);
          599  +											saturation = 100;
          600  +											luminosity = 0.5 + math.random()*0.3;
   436    601   										}
   437         -									end
          602  +									end, sub.player)
   438    603   								end
   439    604   							end;
   440         -							[0.3] = function(s,te,tl)
          605  +							[{whence=0, secs=1}] = function(s,te,tl)
   441    606   								sparktrail(s.visual_subjects,ctx.caster,sorcery.lib.color(255,252,93))
   442    607   								for _,sub in pairs(s.subjects) do
   443    608   									sub.player:add_velocity{y=power;x=0,z=0}
   444    609   								end
   445    610   							end;
   446    611   							[1] = (ctx.amulet.frame == 'cobalt') and function(s,te,tl)
   447    612   								-- TODO add visuals
................................................................................
   489    654   			};
   490    655   		};
   491    656   	};
   492    657   	obliterate = {
   493    658   		name = 'Obliterate';
   494    659   		tone = {255,0,10};
   495    660   		minpower = 5;
   496         -		rarity = 35;
          661  +		rarity = 30;
   497    662   		amulets = {
   498    663   			amethyst = {
   499    664   				name = 'Sapping';
   500    665   				desc = 'Punch a hole in enemy fortifications big enough to slip through but small enough to avoid immediate attention';
   501    666   			};
   502    667   			ruby = {
   503    668   				name = 'Shattering';
................................................................................
   548    713   			};
   549    714   		};
   550    715   	};
   551    716   	excavate = {
   552    717   		name = 'Excavate';
   553    718   		tone = {0,68,235};
   554    719   		minpower = 3;
   555         -		rarity = 30;
          720  +		rarity = 17;
   556    721   		amulets = {
   557    722   			luxite = {
   558    723   				name = 'Stonestride';
   559    724   				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    725   			};
   561    726   			sapphire = {
   562    727   				name = 'Tunnelling';
................................................................................
   651    816   							sound='sorcery_crunch', where='pos';
   652    817   							ephemeral=true, gain = math.random(3,10) * 0.1;
   653    818   						}
   654    819   						tp = tp + (math.random(2,5) * 0.1)
   655    820   					end
   656    821   					sounds[1] = {sound='sorcery_powerdown', where='pos'}
   657    822   					sorcery.spell.cast {
          823  +						name = 'sorcery:excavate';
   658    824   						caster = ctx.caster;
   659    825   						duration = tp;
   660    826   						timeline = timeline, sounds = sounds;
   661    827   						-- spell state
   662    828   						anchor = ctx.target.under;
   663    829   						tunnel_angle = ctx.caster:get_look_horizontal();
   664    830   						tunnel_radius = math.floor(math.random(3,5) * (ctx.stats.power * 0.1));
................................................................................
   675    841   			};
   676    842   		};
   677    843   	};
   678    844   	genesis = {
   679    845   		name = 'Genesis';
   680    846   		tone = {235,0,175};
   681    847   		minpower = 5;
   682         -		rarity = 25;
          848  +		rarity = 23;
   683    849   		amulets = {
   684    850   			mese = {
   685    851   				mingrade = 4;
   686    852   				name = 'Duplication';
   687         -				desc = 'Generate a copy of any object or item, no matter how common or rare';
          853  +				desc = 'Bring an exact twin of any object or item into existence, no matter how common or rare it might be';
          854  +				cast = function(ctx)
          855  +					local color = sorcery.lib.color(255,61,205)
          856  +					local dup, sndpos, anchor, sbj, ty
          857  +					if ctx.target.type == 'object' and ctx.target.ref:get_luaentity().name == '__builtin:item' then
          858  +						sorcery.vfx.imbue(color, ctx.target.ref)
          859  +						sndpos = 'subjects'
          860  +						sbj = {{player = ctx.target.ref}}
          861  +						local item = ItemStack(ctx.target.ref:get_luaentity().itemstring)
          862  +						local r = function() return math.random() * 2 - 1 end
          863  +						local putpos = vector.offset(ctx.target.ref:get_pos(), r(), 1, r())
          864  +						dup = function()
          865  +							item:set_count(1) -- nice try bouge-san
          866  +							return minetest.add_item(putpos, item), false
          867  +						end
          868  +					elseif ctx.target.type == 'node' then
          869  +						ty = minetest.get_node(ctx.target.under).name
          870  +						sorcery.vfx.imbue(color, ctx.target.under)
          871  +						sndpos = 'pos';
          872  +						anchor = ctx.target.under;
          873  +						dup = function()
          874  +							local origmeta = minetest.get_meta(ctx.target.under):to_table()
          875  +							origmeta.inventory = nil
          876  +							local npos
          877  +							do local vp = {}
          878  +								for _, of in pairs(sorcery.lib.node.offsets.neighbors) do
          879  +									local sum = vector.add(ctx.target.under, of)
          880  +									if sorcery.lib.node.is_clear(sum) then
          881  +										vp[#vp+1] = sum
          882  +									end
          883  +								end
          884  +								if #vp > 0 then npos=vp[math.random(#vp)] end
          885  +							end
          886  +							if npos then
          887  +								minetest.set_node(npos, minetest.get_node(ctx.target.under))
          888  +								minetest.get_meta(npos):from_table(origmeta)
          889  +								return npos, true
          890  +							else
          891  +								local nstack = ItemStack(ty)
          892  +								nstack:get_meta():from_table(origmeta)
          893  +								local leftover = ctx.caster:get_inventory():add_item('main',nstack)
          894  +								if leftover and not leftover.is_empty() then
          895  +									minetest.add_item(ctx.caster:get_pos(), leftover)
          896  +								end
          897  +							end
          898  +						end
          899  +					else
          900  +						return false
          901  +					end
          902  +					if minetest.get_item_group(ty,'do_not_duplicate') ~= 0 then
          903  +						return true
          904  +					end
          905  +
          906  +					sorcery.spell.cast {
          907  +						name = 'sorcery:duplicate';
          908  +						caster = ctx.caster;
          909  +						duration = math.random(10,20) * ((10 - ctx.stats.power)*0.1);
          910  +						anchor = anchor;
          911  +						timeline = {
          912  +							[{whence=0, secs=1}] = function(s,te,tl)
          913  +								local mag = sbj and 0.5 or 0.7
          914  +								local pv = sbj and vector.new(0,0,0) or ctx.target.under
          915  +								local vfn = (sbj and s.visual_subjects or s.visual)
          916  +								vfn {
          917  +									amount = tl * 30, time = tl;
          918  +									minpos = vector.offset(pv,-mag,-mag,-mag);
          919  +									maxpos = vector.offset(pv, mag, mag, mag);
          920  +									minsize = 0.5, maxsize = 2.3;
          921  +									minexptime = 1.0, maxexptime = 1.5;
          922  +									texture = sorcery.lib.image('sorcery_sputter.png'):glow(color):render();
          923  +									animation = {
          924  +										type = 'vertical_frames', length = 1.6;
          925  +										aspect_w = 16, aspect_h = 16;
          926  +									};
          927  +								}
          928  +							end;
          929  +							[1] = function(s,te)
          930  +								local where, node = dup()
          931  +								if where == nil then return end
          932  +								local pv = node and where or vector.new(0,0,0)
          933  +								local mp = (not node) and vector.new(0,0,0) or {
          934  +									x = 0.5, y = 0.5, z = 0.5
          935  +								}
          936  +								minetest.add_particlespawner {
          937  +									amount = 170, time = 0.2;
          938  +									minpos = vector.subtract(pv,mp);
          939  +									maxpos = vector.add(pv,mp);
          940  +									attached = (not node) and where or nil;
          941  +									minvel = {x = -2.0, y = -1.8, z = -2.0};
          942  +									maxvel = {x =  2.0, y =  0.2, z =  2.0};
          943  +									minacc = {x = -0.0, y = -0.1, z = -0.0};
          944  +									maxacc = {x =  0.0, y = -0.3, z =  0.0};
          945  +									minsize = 0.3, maxsize = 2;
          946  +									minexptime = 1, maxexptime = 3.0;
          947  +									texture = sorcery.lib.image('sorcery_spark.png'):glow(color):render();
          948  +									animation = {
          949  +										type = 'vertical_frames', length = 3.1;
          950  +										aspect_w = 16, aspect_h = 16;
          951  +									};
          952  +								}
          953  +							end;
          954  +						};
          955  +						sounds = {
          956  +							[0] = {
          957  +								sound = 'sorcery_duplicate_bg';
          958  +								where = sndpos, stop = 1, fade = 2;
          959  +							};
          960  +							[1] = {
          961  +								sound = 'sorcery_genesis';
          962  +								where = sndpos, ephemeral = true;
          963  +							};
          964  +						};
          965  +					}
          966  +				end;
   688    967   			};
   689    968   		};
   690    969   	};
   691    970   	luminate = {
   692    971   		name = 'Luminate';
   693    972   		tone = {255,194,0};
   694    973   		minpower = 1;
   695    974   		rarity = 5;
   696    975   		amulets = {
   697    976   			luxite = {
   698    977   				name = 'Glow';
   699    978   				desc = 'Swathe yourself in an aura of sparkling radiance, casting light upon all the dark places where you voyage';
          979  +				iridium = {
          980  +					name = 'Aura';
          981  +					desc = 'Dazzling golden luminance emanates from the bodies of all those around you, and you walk in light even amid the darkest depths of the earth';
          982  +				};
   700    983   			};
   701    984   			diamond = {
   702    985   				name = 'Radiance';
   703    986   				desc = 'Set the air around you alight with a mystic luminance, letting you see clearly a great distance in every direction for several minutes';
   704    987   				frame = {
   705    988   					iridium = {
   706    989   						name = 'Sunshine';
................................................................................
   711    994   			};
   712    995   		};
   713    996   	};
   714    997   	dominate = {
   715    998   		name = 'Dominate';
   716    999   		tone = {235,0,228};
   717   1000   		minpower = 4;
   718         -		rarity = 20;
         1001  +		rarity = 13;
   719   1002   		amulets = {
   720   1003   			amethyst = {
   721   1004   				name = 'Suffocation';
   722   1005   				desc = 'Wrap this spell tightly around your victim\'s throat, cutting off their oxygen until you release them.';
   723   1006   			};
   724   1007   			emerald = {
   725   1008   				name = 'Caging';

Modified gems.lua from [42c4d86138] to [68440cd05e].

    55     55   	if not gem.foreign_amulet then
    56     56   		local img = sorcery.lib.image
    57     57   		local img_stone = img('sorcery_amulet.png'):multiply(sorcery.lib.color(gem.tone))
    58     58   		local img_sparkle = img('sorcery_amulet_sparkle.png')
    59     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         -			local stats = sorcery.amulet.stats(stack)
           62  +
           63  +			local usedamulet if stack:get_count() == 1 then
           64  +				usedamulet = stack
           65  +			else
           66  +				usedamulet = ItemStack(stack)
           67  +				usedamulet:set_count(1)
           68  +			end
    63     69   			local probe = sorcery.spell.probe(user:get_pos())
    64     70   			-- amulets don't work in antimagic fields, though some may want to 
    65     71   			-- implement this logic themselves (for instance to check a range)
    66     72   			if (probe.disjunction and not sp.ignore_disjunction) then return nil end
           73  +			local stats = sorcery.amulet.stats(usedamulet)
    67     74   
    68     75   			local ctx = {
    69     76   				caster = user;
    70     77   				target = target;
    71     78   				stats = stats;
    72         -				wield = stack;
           79  +				wield = usedamulet;
    73     80   				amulet = stack:get_definition()._sorcery.amulet;
    74     81   				meta = stack:get_meta(); -- avoid spell boilerplate
    75     82   				color = sorcery.lib.color(sp.tone);
    76     83   				today = minetest.get_day_count();
    77     84   				probe = probe;
    78     85   				heading = {
    79     86   					pos   = user:get_pos();
................................................................................
    93    100   					pos = user:get_pos();
    94    101   					gain = 1;
    95    102   				})
    96    103   			end
    97    104   			if ctx.sparkle then
    98    105   				sorcery.vfx.cast_sparkle(user, ctx.color, stats.power,0.5)
    99    106   			end
          107  +			local infinirune = minetest.check_player_privs(user, 'sorcery:infinirune')
   100    108   			if res == nil then
   101         -				if not minetest.check_player_privs(user, 'sorcery:infinirune') then
   102         -					sorcery.amulet.setrune(stack)
   103         -				end
          109  +				if not infinirune then sorcery.amulet.setrune(usedamulet) end
   104    110   			end
   105    111   
   106         -			return ctx.wield
          112  +			if stack:get_count() == 1 then
          113  +				return ctx.wield
          114  +			else
          115  +				if not infinirune then
          116  +					stack:take_item(1)
          117  +					local leftover = user:get_inventory():add_item('main',usedamulet)
          118  +					if leftover and leftover:get_count() > 0 then
          119  +						minetest.add_item(user:get_pos(), leftover)
          120  +					end
          121  +				end
          122  +				return stack
          123  +			end
   107    124   		end;
   108    125   		minetest.register_craftitem(amuletname, {
   109    126   			description = sorcery.lib.str.capitalize(name) .. ' amulet';
   110    127   			inventory_image = img_sparkle:blit(img_stone):render();
   111    128   			wield_scale = { x = 0.6, y = 0.6, z = 0.6 };
   112    129   			groups = { sorcery_amulet = 1 };
   113    130   			on_use = useamulet;

Modified init.lua from [c1be2dc670] to [98e23bb4d1].

    86     86   	-- convenience
    87     87   	'str', 'math';
    88     88   	-- serialization
    89     89   	'marshal', 'json';
    90     90   	-- data structures
    91     91   	'tbl', 'class';
    92     92   	-- wrappers
    93         -	'color', 'image', 'ui';
           93  +	'color', 'image', 'ui', 'obj';
    94     94   	-- game
    95     95   	'node', 'item';
    96     96   }
    97     97   
    98     98   sorcery.stage('worldbuilding',data,root)
    99     99   root {'compat','matreg'}
   100    100   if not sorcery.stage('loadlore', data, root) then

Modified interop.lua from [913cc0d6da] to [66e6db7c2d].

    41     41   
    42     42   		{'top',    'sorcery:mill', 'output'};
    43     43   		{'side',   'sorcery:mill', 'grinder'};
    44     44   		{'bottom', 'sorcery:mill', 'input'};
    45     45   
    46     46   		{'bottom', 'sorcery:harvester', 'charge'};
    47     47   		-- output handled on our side
           48  +
           49  +		{'bottom', 'sorcery:runeforge', 'amulet'};
           50  +		-- output handled on our side
    48     51   	}
    49     52   end
           53  +
           54  +if minetest.get_modpath('mtg_craftguide') and minetest.get_modpath('sfinv') then
           55  +-- the craft guide is handy, but not only is it glitched to the point of enabling
           56  +-- trivial denial of service attacks against a server, it breaks some of the most
           57  +-- basic mechanics of the sorcery mod. we disable it except for players with a
           58  +-- specific debugging privilege. i suppose we could also add a 'potion of
           59  +-- omniscience' that allows brief access, but i'm disinclined to; it feels gross.
           60  +	local pg = sfinv.pages['mtg_craftguide:craftguide']
           61  +	local cb = pg.is_in_nav
           62  +	-- currently this isn't used by mtgcg, but doing this gives us some future-
           63  +	-- proofing, and keeps us from fucking up any competing access control that
           64  +	-- might be in use.
           65  +	pg.is_in_nav = function(self,player, ...)
           66  +		-- unfortunately, this is a purely cosmetic "access control" mechanism;
           67  +		-- sfinv doesn't actually check if a page is available to a player before
           68  +		-- showing it to them. ironic, given how the author specifically warns
           69  +		-- people in his modding tutorial that the client can submit any form it
           70  +		-- wants at any time… 🙄
           71  +		if not minetest.check_player_privs(player, 'sorcery:omniscience') then
           72  +			return false
           73  +		end
           74  +		if cb
           75  +			then return cb(self,player,...)
           76  +			else return true
           77  +		end
           78  +	end
           79  +end

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

    71     71   	is_air = function(pos)
    72     72   		local n = sorcery.lib.node.force(pos)
    73     73   		if n.name == 'air' then return true end
    74     74   		local d = minetest.registered_nodes[n.name]
    75     75   		if not d then return false end
    76     76   		return not d.walkable
    77     77   	end;
           78  +
           79  +	is_clear = function(pos)
           80  +		if not sorcery.lib.node.is_air(pos) then return false end
           81  +		local ents = minetest.get_objects_inside_radius(pos,0.5)
           82  +		if #ents > 0 then return false end
           83  +		return true
           84  +	end;
    78     85   
    79     86   	get_arrival_point = function(pos)
    80         -		local air = sorcery.lib.node.is_air
    81         -		if air(pos) then
    82         -			local n = {x=0,y=1,z=0}
    83         -			if air(vector.add(pos,n)) then return pos end
    84         -			local down = vector.subtract(pos,n)
    85         -			if air(down) then return down end
    86         -		else return nil end
           87  +		local try = function(p)
           88  +			local air = sorcery.lib.node.is_clear
           89  +			if air(p) then
           90  +				if air(vector.offset(p,0,1,0))  then return p end
           91  +				if air(vector.offset(p,0,-1,0)) then return vector.offset(p,0,-1,0) end
           92  +			end
           93  +			return false
           94  +		end
           95  +		
           96  +		do local t = try(pos) if t then return t end end
           97  +		for _,o in pairs(ofs.neighbors) do
           98  +			local p = vector.add(pos, o)
           99  +			do local t = try(p) if t then return t end end
          100  +		end
    87    101   	end;
    88    102   
    89    103   	amass = function(startpoint,names,directions)
    90    104   		if not directions then directions = ofs.neighbors end
    91    105   		local nodes, positions, checked = {},{},{}
    92    106   		local checkedp = function(pos)
    93    107   			for _,v in pairs(checked) do

Added lib/obj.lua version [42345da92a].

            1  +-- functions for working with entities inexplicably missing
            2  +-- from the game API
            3  +
            4  +local fn = {}
            5  +
            6  +-- WARNING: INEFFICIENT AS FUCK
            7  +fn.identify = function(objref) --> objectid
            8  +	for _, o in pairs(minetest.get_connected_players()) do
            9  +		if objref == o then return o:get_player_name(), 'player' end
           10  +	end
           11  +	for id, le in pairs(minetest.luaentities) do
           12  +		if le.object == objref then return id, 'entity' end
           13  +	end
           14  +end
           15  +
           16  +fn.handle = sorcery.lib.class {
           17  +	__newindex = function(self,key,newval)
           18  +		local hnd if self.player
           19  +			then hnd = minetest.get_player_by_name(self._id)
           20  +			else hnd = minetest.luaentities[self._id]
           21  +		end
           22  +		if key == 'id' then
           23  +			if type(newval) == 'string' then
           24  +				local p = minetest.get_player_by_name(newval)
           25  +				if p then
           26  +					self._id = newval
           27  +					self.player = true
           28  +					return
           29  +				end
           30  +			end
           31  +			if minetest.luaentities[newval] then
           32  +				self._id = newval
           33  +				self.player = false
           34  +			else error('attempted to assign invalid ID to entity handle') end
           35  +		elseif key == 'obj' then
           36  +			local no, kind = fn.identify(newval)
           37  +			if no then
           38  +				self._id = no
           39  +				if kind == 'player'
           40  +					then self.player = true
           41  +					else self.player = false
           42  +				end
           43  +			else error('attempted to assign invalid ObjectRef to entity handle') end
           44  +		elseif key == 'stack' and self.kind == 'item' then
           45  +			hnd:set_item(newval)
           46  +		end
           47  +	end;
           48  +	__index = function(self,key)
           49  +		local hnd if self.player then
           50  +			hnd = minetest.get_player_by_name(self._id)
           51  +		else
           52  +			hnd = minetest.luaentities[self._id]
           53  +		end
           54  +		if key == 'online' then
           55  +			return hnd ~= nil
           56  +		elseif key == 'id' then
           57  +			if self.player then return nil
           58  +			else return self._id end
           59  +		elseif key == 'obj' then
           60  +			if self.player
           61  +				then return hnd
           62  +				else return hnd.object
           63  +			end
           64  +		elseif key == 'kind' then
           65  +			if     self.player                  then return 'player'
           66  +			elseif hnd.name == '__builtin:item' then return 'item'
           67  +			                                    else return 'object' end
           68  +		elseif key == 'name' then
           69  +			if self.player then return self._id
           70  +			elseif self.kind == 'item' then
           71  +				return ItemStack(hnd.itemstring):get_name()
           72  +			else return hnd.name end
           73  +		elseif key == 'stack' and self.kind == 'item' then
           74  +			return ItemStack(hnd.itemstring)
           75  +		elseif key == 'height' then
           76  +			if kind == 'item' then return 0.5
           77  +			elseif kind == 'player' then
           78  +				local eh = hnd.object:get_properties().eye_height
           79  +				return eh and (eh*1.2) or 1
           80  +			else
           81  +				local box = hnd.object:get_properties().collisionbox
           82  +				if box then
           83  +					local miny,maxy = box[2], box[5]
           84  +					return maxy-miny, miny
           85  +				else return 0 end
           86  +			end
           87  +		end
           88  +	end;
           89  +	construct = function(h)
           90  +		local kind, id
           91  +		if type(h) == 'string' and minetest.get_player_by_name(h) ~= nil then
           92  +			kind = 'player';
           93  +			id = h
           94  +		elseif minetest.luaentities[h] then
           95  +			kind = 'entity';
           96  +			id = h
           97  +		else id, kind = fn.identify(h) end
           98  +
           99  +		if not id then
          100  +			error('attempted to construct object handle from invalid value')
          101  +		end
          102  +
          103  +		return {
          104  +			player = kind == 'player';
          105  +			_id = id;
          106  +		}
          107  +	end;
          108  +}
          109  +
          110  +return fn

Modified privs.lua from [62e9e10513] to [7ff60372f5].

     1      1   minetest.register_privilege('sorcery:infinirune', {
     2      2   	description = "runes don't discharge upon use, for debugging use only";
     3      3   	give_to_singleplayer = false;
     4      4   	give_to_admin = false;
     5      5   })
            6  +
            7  +if minetest.get_modpath('mtg_craftguide') then
            8  +	minetest.register_privilege('sorcery:omniscience', {
            9  +		description = "access the all-knowing crafting guide";
           10  +		give_to_singleplayer = false;
           11  +		give_to_admin = false;
           12  +	})
           13  +end

Added sounds/sorcery_duplicate_bg.ogg version [].


Added sounds/sorcery_genesis.ogg version [8332db2096].

cannot compute difference between binary files

Modified spell.lua from [cfeba0d2de] to [92125c4f2c].

   133    133   		if s.i then sorcery.spell.active[s.i] = nil else
   134    134   			for k,v in pairs(sorcery.spell.active) do
   135    135   				if v == spell then sorcery.spell.active[k] = nil break end
   136    136   			end
   137    137   		end
   138    138   	end
   139    139   end
          140  +
          141  +sorcery.spell.ensorcelled = function(player,spell)
          142  +	if type(player) == 'string' then player = minetest.get_player_by_name(player) end
          143  +	for _,s in pairs(sorcery.spell.active) do
          144  +		if spell and (s.name ~= spell) then goto skip end
          145  +		for _,sub in pairs(s.subjects) do
          146  +			if sub.player == player then return s end
          147  +		end
          148  +	::skip::end
          149  +	return false
          150  +end
          151  +
          152  +sorcery.spell.each = function(player,spell)
          153  +	local idx = 0
          154  +	return function()
          155  +		repeat idx = idx + 1
          156  +			local sp = sorcery.spell.active[idx]
          157  +			if sp == nil then return nil end
          158  +			if spell == nil or sp.name == spell then
          159  +				for _,sub in pairs(sp.subjects) do
          160  +					if sub.player == player then return sp end
          161  +				end
          162  +			end
          163  +		until idx >= #sorcery.spell.active
          164  +	end
          165  +end
   140    166   
   141    167   -- when a new spell is created, we analyze it and make the appropriate calls
   142    168   -- to minetest.after to queue up the events. each job returned needs to be
   143    169   -- saved in 'jobs' so they can be canceled if the spell is disjoined. no polling
   144    170   -- necessary :D
   145    171   
   146    172   sorcery.spell.cast = function(proto)
................................................................................
   183    209   		for _,j in ipairs(s.jobs) do j:cancel() end
   184    210   		for _,v in ipairs(s.vfx) do minetest.delete_particlespawner(v.handle) end
   185    211   		for _,i in ipairs(s.sfx) do s.silence(i) end
   186    212   		for _,i in ipairs(s.impacts) do i.effect:stop() end
   187    213   	end
   188    214   	s.release_subject = function(si)
   189    215   		local t = s.subjects[si]
   190         -		print('releasing against',si,t)
   191    216   		for _,f in pairs(s.sfx)     do if f.subject == t then s.silence(f) end end
   192    217   		for _,f in pairs(s.impacts) do if f.subject == t then f.effect:stop() end end
   193    218   		for _,f in pairs(s.vfx) do
   194    219   			if f.subject == t then minetest.delete_particlespawner(f.handle) end
   195    220   		end
   196    221   		s.subjects[si] = nil
   197    222   	end
................................................................................
   363    388   			if s.terminate then s:terminate() end
   364    389   			sorcery.spell.active[myid] = nil
   365    390   		end)
   366    391   	end
   367    392   	s.starttime = minetest.get_server_uptime()
   368    393   	return s
   369    394   end
          395  +
          396  +minetest.register_on_dieplayer(function(player)
          397  +	sorcery.spell.disjoin{target=player}
          398  +end)

Modified vfx.lua from [aaa85eb70f] to [423ee7c3cf].

   104    104   			},
   105    105   			acceleration = {
   106    106   				x = 0,
   107    107   				y = -1,
   108    108   				z = 0
   109    109   			}
   110    110   		}
          111  +	end
          112  +end
          113  +
          114  +-- target can be an entity or a pos vector
          115  +sorcery.vfx.imbue = function(color, target, strength, height)
          116  +	local tpos if target.get_pos then
          117  +		tpos = target:get_pos()
          118  +		if target.get_properties then
          119  +			height = height or ((target:get_properties().eye_height or 1)*1.3)
          120  +		end
          121  +	else
          122  +		tpos = target
          123  +	end
          124  +	height = height or 1
          125  +	local scenter = vector.add(tpos, {x=0,y=height/2,z=0})
          126  +	for i=1,math.random(64*(strength or 1),128*(strength or 1)) do
          127  +		local high = (height+0.8)*math.random() - 0.8
          128  +		local far = (high >= -0.5 and high <= height) and
          129  +			(math.random() * 0.3 + 0.4) or
          130  +			(math.random() * 0.5)
          131  +		local yaw = {x=0, y = math.random()*(2*math.pi), z=0}
          132  +		local po = vector.rotate({x=far,y=high,z=0}, yaw)
          133  +		local ppos = vector.add(po,tpos)
          134  +		local dir = vector.direction(ppos,scenter)
          135  +		local vel = math.random() * 0.8 + 0.4
          136  +		local col if type(color) == 'function'
          137  +			then col = color(i, {high = high, far = far, dir = dir, vel = vel, pos = po})
          138  +			else col = color
          139  +		end
          140  +		minetest.add_particle {
          141  +			pos = ppos;
          142  +			velocity = vector.multiply(dir,vel);
          143  +			expirationtime = far / vel;
          144  +			size = math.random()*2.4 + 0.6;
          145  +			texture = sorcery.lib.image('sorcery_sputter.png'):glow(col):render();
          146  +			glow = 14;
          147  +			animation = {
          148  +				type = 'vertical_frames', length = far/vel;
          149  +				aspect_w = 16, aspect_h = 16;
          150  +			};
          151  +		}
   111    152   	end
   112    153   end