sorcery  Diff

Differences From Artifact [4908250df2]:

To Artifact [d5ad4a6740]:

  • File data/runes.lua — part of check-in [147592b8e9] at 2020-10-26 03:58:08 on branch trunk — 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 (user: lexi, size: 30324) [annotate] [blame] [check-ins using]

     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 }